Bagaimana layar pesan VK ditampilkan?

Apa yang VK lakukan untuk mengurangi rendering lag? Bagaimana cara menampilkan pesan yang sangat besar dan tidak membunuh UiThread? Bagaimana cara mengurangi penundaan bergulir di RecyclerView?



Pengalaman saya didasarkan pada pekerjaan menggambar layar pesan di aplikasi VK Android, di mana perlu untuk menunjukkan sejumlah besar informasi dengan rem minimal pada UI.

Saya telah memprogram untuk Android selama hampir sepuluh tahun, saya sebelumnya freelance untuk PHP / Node.js. Sekarang - pengembang senior Android VKontakte.

Di bawah potongan video dan transkrip laporan saya dari konferensi Mobius 2019 Moscow.


Laporan ini mengungkapkan tiga topik



Lihatlah layar:


Pesan ini ada di suatu tempat di lima layar. Dan mereka mungkin bersama kami (dalam hal meneruskan pesan). Alat standar tidak lagi berfungsi. Bahkan di perangkat teratas, semuanya bisa ketinggalan.
Selain itu, UI itu sendiri cukup beragam:

  • memuat tanggal dan indikator,
  • pesan layanan
  • teks (emoji, tautan, email, tagar),
  • keyboard bot
  • ~ 40 cara untuk menampilkan lampiran,
  • pohon pesan yang diteruskan.

Muncul pertanyaan: bagaimana cara membuat jumlah kelambatan sekecil mungkin? Baik dalam hal pesan sederhana, dan dalam kasus pesan massal (tepi-kasus dari video di atas).


Solusi Standar


RecyclerView dan add-on-nya


Ada berbagai add-on untuk RecyclerView.

  • setHasFixedSize ( boolean)

Banyak orang berpikir bahwa bendera ini diperlukan ketika item daftar berukuran sama. Namun faktanya, dilihat dari dokumentasi, yang terjadi adalah sebaliknya. Ini terjadi ketika ukuran RecyclerView konstan dan independen dari elemen (kira-kira, tidak wrap_content). Mengatur bendera membantu sedikit meningkatkan kecepatan RecyclerView sehingga menghindari perhitungan yang tidak perlu.

  • setNestedScrollingEnabled ( boolean)

Optimalisasi minor yang menonaktifkan dukungan NestedScroll. Kami tidak memiliki CollapsingToolbar atau fitur lainnya tergantung pada NestedScroll di layar ini, sehingga kami dapat dengan aman mengatur flag ini ke false.

  • setItemViewCacheSize ( cache_size)

Menyiapkan cache RecyclerView internal.

Banyak orang berpikir bahwa mekanisme RecyclerView adalah:

  • ada ViewHolder yang ditampilkan di layar;
  • ada RecycledViewPool menyimpan ViewHolder;
  • ViewHolder — RecycledViewPool.

Dalam praktiknya, semuanya sedikit lebih rumit, karena ada cache antara antara dua hal ini. Ini disebut ItemViewCache. Apa esensinya? Ketika ViewHolder meninggalkan layar, itu tidak ditempatkan di RecycledViewPool, tetapi di cache perantara (ItemViewCache). Semua perubahan pada adaptor berlaku untuk ViewHolder yang terlihat dan ViewHolder di dalam ItemViewCache. Dan untuk ViewHolder di dalam RecycledViewPool, perubahan tidak diterapkan.

Melalui setItemViewCacheSize kita dapat mengatur ukuran cache perantara ini.
Semakin besar, semakin cepat gulir akan jarak pendek, tetapi operasi pembaruan akan lebih lama (karena ViewHolder.onBind, dll.).

Bagaimana RecyclerView diimplementasikan dan bagaimana cache terstruktur adalah topik yang agak besar dan kompleks. Anda dapat membaca artikel yang bagus, di mana mereka berbicara secara detail tentang segalanya.

Optimasi OnCreate / OnBind


Solusi klasik lainnya adalah mengoptimalkan onCreateViewHolder / onBindViewHolder:

  • tata letak yang mudah (kami mencoba menggunakan FrameLayout atau Custom ViewGroup sebanyak mungkin),
  • operasi berat (parsing links / emoji) dilakukan secara tidak sinkron pada tahap pemuatan pesan,
  • StringBuilder untuk memformat nama, tanggal, dll.,
  • dan solusi lain yang mengurangi waktu kerja metode ini.

Pelacakan Adapter.onFailedToRecyclerView ()




Anda memiliki daftar di mana beberapa elemen (atau bagiannya) dianimasikan dengan alpha. Pada saat View, sedang dalam proses animasi, meninggalkan layar, maka itu tidak menuju ke RecycledViewPool. Mengapa? RecycledViewPool melihat bahwa View sekarang dianimasikan oleh flag View.hasTransientState, dan abaikan saja. Oleh karena itu, lain kali Anda menggulir ke atas dan ke bawah, gambar tidak akan diambil dari RecycledViewPool, tetapi akan dibuat lagi.

Keputusan yang paling benar adalah ketika ViewHolder meninggalkan layar, Anda harus membatalkan semua animasi.



Jika Anda memerlukan perbaikan terbaru sesegera mungkin atau Anda adalah pengembang yang malas, maka dalam metode onFailedToRecycle Anda dapat selalu mengembalikan true dan semuanya akan berfungsi, tetapi saya tidak akan menyarankan melakukannya.



Pelacakan Overdraw dan Profiler


Cara klasik untuk mendeteksi masalah adalah penarikan berlebih dan pelacakan profiler.

Overdraw - jumlah redraws piksel: semakin sedikit layer dan semakin sedikit redraw pixel, semakin cepat. Tetapi menurut pengamatan saya, dalam kenyataan modern, ini tidak begitu mempengaruhi kinerja.



Profiler - alias Monitor Android, yang ada di Android Studio. Di dalamnya, Anda dapat menganalisis semua metode yang disebut. Misalnya, buka pesan, gulir ke atas dan ke bawah dan lihat metode apa yang dipanggil dan berapa lama.



Semua yang ada di bagian kiri adalah panggilan sistem Android yang diperlukan untuk membuat / menyajikan View / ViewHolder. Kami tidak dapat memengaruhi mereka, atau kami harus menghabiskan banyak upaya.

Setengah bagian kanan adalah kode kami yang berjalan di ViewHolder.

Blok panggilan di nomor 1 adalah panggilan ke ekspresi reguler: di suatu tempat mereka mengabaikan dan lupa untuk menempatkan operasi pada utas latar belakang, sehingga memperlambat gulir dengan ~ 20%.

Blokir panggilan di nomor 2 - Fresco, perpustakaan untuk menampilkan gambar. Ini tidak optimal di beberapa tempat. Belum jelas apa yang harus dilakukan dengan lag ini, tetapi jika kita bisa menyelesaikannya, kita akan menghemat ~ 15% lagi.

Yaitu, memperbaiki masalah ini, kita bisa mendapatkan peningkatan ~ 35%, yang cukup keren.

Diffiff


Banyak dari Anda menggunakan DiffUtil dalam bentuk standar: ada dua daftar - yang disebut, dibandingkan dan didorong perubahan. Melakukan semua ini pada utas utama agak mahal karena daftarnya bisa sangat besar. Jadi biasanya perhitungan DiffUtil berjalan pada utas latar belakang.

ListAdapter dan AsyncListDiffer melakukan ini untuk Anda. ListAdapter memperluas Adaptor biasa dan memulai semuanya secara tidak sinkron - cukup buat submitList dan seluruh kalkulasi perubahannya terbang ke utas latar belakang internal. ListAdapter dapat mempertimbangkan kasus pembaruan yang sering terjadi: jika Anda menyebutnya tiga kali berturut-turut, itu hanya akan mengambil hasil terakhir.

DiffUtil sendiri kami gunakan hanya untuk beberapa perubahan struktural - tampilan pesan, perubahan dan penghapusannya. Untuk beberapa data perubahan cepat, itu tidak cocok. Misalnya, ketika kita mengunggah foto atau memutar audio. Peristiwa semacam itu sering terjadi - beberapa kali per detik, dan jika Anda menjalankan DiffUtil setiap kali, Anda akan mendapatkan banyak pekerjaan tambahan.

Animasi


Sekali waktu ada kerangka Animasi - agak sedikit, tetapi masih sesuatu. Kami bekerja dengannya seperti ini:

view.startAnimation(TranslateAnimation(fromX = 0, toX = 300))

Masalahnya adalah parameter getTranslationX () akan mengembalikan nilai yang sama sebelum dan sesudah animasi. Ini karena Animasi mengubah representasi visual, tetapi tidak mengubah sifat fisik.



Di Android 3.0, kerangka Animator muncul, yang lebih tepat karena mengubah properti fisik spesifik objek.



Kemudian, ViewPropertyAnimator muncul dan semua orang masih tidak benar-benar memahami perbedaannya dari Animator.

Saya akan menjelaskan. Katakanlah Anda perlu melakukan terjemahan secara diagonal - menggeser Tampilan di sepanjang sumbu x, y. Kemungkinan besar Anda akan menulis kode khas:

val animX = ObjectAnimator.ofFloat(view, “translationX”, 100f)
val animY = ObjectAnimator.ofFloat(view, “translationY”, 200f)
AnimatorSet().apply {
    playTogether(animX, animY)
    start()
}

Dan Anda bisa membuatnya lebih pendek:

view.animate().translationX(100f).translationY(200f) 

Ketika Anda menjalankan view.animate (), Anda secara implisit meluncurkan ViewPropertyAnimator.

Mengapa itu dibutuhkan?

  1. Lebih mudah membaca dan memelihara kode.
  2. Animasi operasi batch.

Dalam kasus terakhir kami, kami mengubah dua properti. Ketika kita melakukan ini melalui animator, kutu animasi akan dipanggil secara terpisah untuk setiap Animator. Yaitu, setTranslationX dan setTranslationY akan dipanggil secara terpisah, dan View akan melakukan operasi pembaruan secara terpisah.

Dalam kasus ViewPropertyAnimator, perubahan terjadi pada saat yang sama, sehingga ada penghematan karena operasi yang lebih sedikit dan perubahan properti itu sendiri lebih baik dioptimalkan.

Anda dapat mencapai ini dengan Animator, tetapi Anda harus menulis lebih banyak kode. Selain itu, menggunakan ViewPropertyAnimator, Anda dapat yakin bahwa animasinya akan dioptimalkan sebanyak mungkin. Mengapa? Android memiliki RenderNode (DisplayList). Sangat kasar, mereka men-cache hasil onDraw dan menggunakannya saat menggambar ulang. ViewPropertyAnimator bekerja langsung dengan RenderNode dan menerapkan animasi untuk itu, menghindari panggilan onDraw.

Banyak properti Tampilan juga dapat secara langsung mempengaruhi RenderNode, tetapi tidak semua. Artinya, saat menggunakan ViewPropertyAnimator, Anda dijamin menggunakan cara yang paling efisien. Jika Anda tiba-tiba memiliki semacam animasi yang tidak dapat dilakukan dengan ViewPropertyAnimator, maka mungkin Anda harus memikirkannya dan mengubahnya.

Animasi: TransitionManager


Biasanya, orang mengasosiasikan bahwa kerangka kerja ini digunakan untuk berpindah dari satu Kegiatan ke Kegiatan lainnya. Bahkan, dapat digunakan secara berbeda dan sangat menyederhanakan implementasi animasi perubahan struktural. Misalkan kita memiliki layar tempat pesan suara diputar. Kami menutupnya dengan salib, dan mati naik. Bagaimana cara melakukannya? Animasi ini cukup rumit: pemain ditutup dengan alfa, sementara bergerak bukan melalui terjemahan, tetapi mengubah ketinggiannya. Pada saat yang sama, daftar kami naik dan juga mengubah ketinggian.



Jika pemain adalah bagian dari daftar, maka animasinya akan cukup sederhana. Tetapi bersama kami, pemain bukanlah elemen dari daftar, tetapi pandangan yang sepenuhnya independen.

Mungkin kita akan mulai menulis semacam Animator, kemudian kita akan menghadapi masalah, crash, mulai menggergaji kruk dan menggandakan kode. Dan akan mendapatkan sesuatu seperti layar di bawah ini.



Dengan TransitionManager, Anda dapat membuat semuanya lebih sederhana:

TransitionManager.beginDelayedTransition(
        viewGroup = <LinearLayoutManager>,
        transition = AutoTransition())
playerView.visibility = View.GONE

Semua animasi terjadi secara otomatis di bawah tenda. Ini terlihat seperti sulap, tetapi jika Anda masuk jauh ke dalam dan melihat cara kerjanya, ternyata TransitionManager hanya berlangganan semua Tampilan, menangkap perubahan dalam propertinya, menghitung diff, membuat animator atau ViewPropertyAnimator yang diperlukan di mana diperlukan, dan melakukan segala sesuatu seefisien mungkin. TransitionManager memungkinkan kita membuat animasi di bagian pesan dengan cepat dan mudah diimplementasikan.

Solusi khusus




Ini adalah hal yang paling mendasar yang menjadi dasar kinerja dan masalah yang mengikuti. Apa yang harus dilakukan ketika pesan Anda ada di 10 layar? Jika Anda memperhatikan, maka semua elemen kami terletak persis di bawah satu sama lain. Jika kami menerima bahwa ViewHolder bukan satu pesan, tetapi puluhan ViewHolder yang berbeda, maka semuanya menjadi lebih sederhana.

Bagi kami bukan masalah bahwa pesannya telah menjadi 10 layar, karena sekarang kami hanya menampilkan enam ViewHolders dalam contoh nyata. Kami mendapat tata letak yang mudah, kode lebih mudah dipelihara, dan tidak ada masalah khusus, kecuali satu - bagaimana melakukan ini?



Ada ViewHolders sederhana - ini adalah pemisah tanggal klasik, Memuat lebih banyak, dan sebagainya. Dan BaseViewHolder - ViewHolder dasar bersyarat untuk pesan. Ini memiliki implementasi dasar dan beberapa yang spesifik - TextViewHolder, PhotoViewHolder, AudioViewHolder, ReplyViewHolder dan sebagainya. Ada sekitar 70 di antaranya.

Apa yang menjadi tanggung jawab BaseViewHolder?


BaseViewHolder hanya bertanggung jawab untuk menggambar avatar dan potongan gelembung yang diinginkan, serta garis untuk pesan yang diteruskan - biru ke kiri.



Implementasi konkret konten sudah dilakukan oleh pewaris BaseViewHolder lainnya: TextViewHolder hanya menampilkan teks, FwdSenderViewHolder - penulis pesan yang diteruskan, AudioMsgViewHolder - pesan suara, dan sebagainya.



Ada masalah: apa yang harus dilakukan dengan lebar? Bayangkan sebuah pesan di setidaknya dua layar. Tidak terlalu jelas lebar yang harus diatur, karena setengah terlihat, setengah tidak terlihat (dan bahkan belum dibuat). Benar-benar semuanya tidak dapat diukur, karena ada. Saya harus sedikit kruk, sayangnya. Ada kasus sederhana ketika pesannya sangat sederhana: teks atau suara murni - secara umum, terdiri dari satu Item.



Dalam hal ini, gunakan konten klasik wrap_content. Untuk kasus kompleks, ketika pesan terdiri dari beberapa bagian, kami mengambil dan memaksa setiap ViewHolder lebar tetap. Khususnya di sini - 220 dp.



Jika teksnya sangat pendek dan pesan diteruskan, ada ruang kosong di sebelah kanan. Tidak ada jalan keluar dari ini, karena kinerja lebih penting. Selama beberapa tahun, tidak ada keluhan - mungkin seseorang memperhatikan, tetapi secara umum, semua orang terbiasa dengan itu.



Ada kasus tepi. Jika kami menanggapi pesan dengan stiker, maka kami dapat menentukan lebar khusus untuk kasus tersebut, sehingga terlihat lebih cantik.

Kami berpisah menjadi ViewHolders pada tahap memuat pesan: kami mulai memuat latar belakang pesan, mengubahnya menjadi item, mereka langsung ditampilkan di ViewHolders.



Global RecycledViewPool


Mekanisme menggunakan messenger kita sedemikian rupa sehingga orang tidak duduk dalam obrolan yang sama, tetapi terus-menerus pergi di antara mereka. Dalam pendekatan standar, ketika kami masuk ke obrolan dan meninggalkannya, RecycledViewPool (dan ViewHolder di dalamnya) dihancurkan, dan setiap kali kami menghabiskan sumber daya menciptakan ViewHolder.

Ini dapat dipecahkan oleh global RecycledViewPool:

  • dalam kerangka Aplikasi, RecycledViewPool hidup sebagai singleton;
  • digunakan kembali pada layar pesan ketika pengguna berjalan di antara layar;
  • atur sebagai RecyclerView.setRecycledViewPool (pool).

Ada jebakan, penting untuk mengingat dua hal:

  • Anda pergi ke layar, klik kembali, keluar. Masalahnya adalah bahwa ViewHolders yang ada di layar dibuang, dan tidak dikembalikan ke kolam. Ini diperbaiki sebagai berikut:
    LinearLayoutManager.recycleChildrenOnDetach = true
  • RecycledViewPool memiliki batasan: tidak lebih dari lima ViewHolders yang dapat disimpan untuk setiap tipe tampilan.

Jika 9 TextViews ditampilkan di layar, hanya lima item yang akan dikembalikan ke RecycledViewPool, dan sisanya akan dibuang. Anda dapat mengubah ukuran RecycledViewPool:
RecycledViewPool.setMaxRecycledViews (viewType, size)

Tapi entah bagaimana sedih untuk meresepkan setiap ViewType dengan tangan Anda, karena Anda dapat menulis RecycledViewPool Anda, memperluas yang standar, dan menjadikannya NoLimit. Dengan tautan, Anda dapat mengunduh implementasi yang telah selesai.

Kesulitan tidak selalu berguna


Ini adalah kasing klasik - unduh, putar trek audio, dan pesan suara. Dalam hal ini, DiffUtil memanggil spam.



BaseViewHolder kami memiliki metode pemutakhiranUploadProgress abstrak.

abstract class BaseViewHolder : ViewHolder {
    fun updateUploadProgress(attachId: Int, progress: Float)
    …        
}

Untuk mengadakan acara, kita perlu melewati semua ViewHolder yang terlihat:

fun onUploadProgress(attachId: Int, progress: Float) {
    forEachActiveViewHolder {
        it.updateUploadProgress(attachId, progress)
    }
}

Ini adalah operasi yang sederhana, tidak mungkin kita memiliki lebih dari sepuluh ViewHolder di layar. Pendekatan seperti itu tidak bisa ketinggalan pada prinsipnya. Bagaimana menemukan ViewHolder yang terlihat? Implementasi yang naif akan seperti ini:

val firstVisiblePosition = <...>
val lastVisiblePosition = <...>
for (i in firstVisiblePosition.. lastVisiblePosition) {
    val viewHolder = recycler.View.findViewHolderForAdapterPosition(i)
    viewHolder.updateUploadProgress(..)
}

Tapi ada masalah. Cache antara yang saya sebutkan sebelumnya, ItemViewCache, berisi ViewHolders aktif yang tidak muncul di layar. Kode di atas tidak akan memengaruhi mereka. Secara langsung, kami juga tidak bisa mengatasinya. Dan kemudian kruk membantu kami. Buat WeakSet yang menyimpan tautan ke ViewHolder. Selanjutnya cukup bagi kita untuk hanya memotong WeakSet ini.

class Adapter : RecyclerView.Adapter {
    val activeViewHolders = WeakSet<ViewHolder>()
        
    fun onBindViewHolder(holder: ViewHolder, position: Int) {
        activeViewHolders.add(holder)
    }

    fun onViewRecycled(holder: ViewHolder) {
        activeViewHolders.remove(holder)
    }
}

Hamparan ViewHolder


Perhatikan contoh cerita. Sebelumnya, jika seseorang bereaksi terhadap sebuah cerita dengan stiker, kami menampilkannya seperti ini:



Itu terlihat sangat jelek. Saya ingin melakukan yang lebih baik, karena ceritanya sangat menarik, dan kami memiliki kotak kecil di sana. Tapi kami ingin mendapatkan sesuatu seperti ini:



Ada masalah: pesan kami dipecah menjadi ViewHolder, mereka terletak di bawah satu sama lain, tetapi di sini mereka tumpang tindih. Segera tidak jelas bagaimana menyelesaikannya. Anda dapat membuat ViewType lainnya "riwayat + stiker" atau "riwayat + pesan suara". Jadi, alih-alih 70 ViewType, kita akan memiliki 140 ... Tidak, kita perlu membuat sesuatu yang lebih nyaman.



Salah satu kruk favorit Anda di Android muncul di benak Anda. Misalnya, kami melakukan sesuatu, tetapi Pixel Perfect tidak bertemu. Untuk mengatasinya, Anda harus menghapus semuanya dan menulis dari awal, tetapi kemalasan. Sebagai hasilnya, kita dapat membuat margin = -2dp (negatif), dan sekarang semuanya masuk ke tempatnya. Tetapi pendekatan seperti itu tidak bisa digunakan di sini. Jika Anda menetapkan margin negatif, stiker akan bergerak, tetapi tempat yang ditempati akan tetap kosong. Tapi kami memiliki ItemDecoration, di mana itemOffset kami dapat membuat angka negatif. Dan itu berhasil! Akibatnya, kami mendapatkan hamparan yang diharapkan dan pada saat yang sama masih ada paradigma di mana setiap ViewHolder adalah teman.

Solusi yang indah dalam satu baris.

class OffsetItemDecoration : RecyclerViewItemDecoration() {
    overrride fun getItemOffsets(offset: Rect, …) {
        offset.top = -100dp
    }
}

Idlehandler


Ini adalah kasus dengan tanda bintang, itu kompleks dan tidak begitu sering diperlukan dalam praktik, tetapi penting untuk mengetahui tentang keberadaan metode semacam itu.

Pertama, saya akan memberi tahu Anda bagaimana utas utama UiThread bekerja. Skema umum: ada antrian acara tugas di mana tugas ditetapkan melalui handler.post, dan loop tak terbatas yang melewati antrian ini. Yaitu, UiThread hanya sementara (benar). Jika ada tugas, kami menjalankannya, jika tidak, kami menunggu sampai tugas itu muncul.



Dalam realitas kita yang biasa, Handler bertanggung jawab untuk menempatkan tugas ke dalam antrian, dan Looper tanpa henti melewati antrian. Ada tugas yang tidak terlalu penting untuk UI. Misalnya, pengguna membaca pesan - tidak begitu penting bagi kami ketika kami menampilkannya di UI, sekarang atau setelah 20 ms. Pengguna tidak akan melihat perbedaannya. Lalu, mungkin layak menjalankan tugas ini di utas utama hanya saat gratis? Artinya, alangkah baiknya bagi kita untuk mengetahui kapan garis awaitNewTask dipanggil. Untuk kasus ini, Looper memiliki addIdleHandler yang menyala ketika kode task.isEmpty diaktifkan.

Looper.myQueue (). AddIdleHandler ()

Dan implementasi IdleHandler yang paling sederhana akan terlihat seperti ini:

@AnyThread
class IdleHandler {
    private val handler = Handler(Looper.getMainLooper())

    fun post(task: Runnable) {
        handler.post {
            Looper.myQueue().addIdleHandler {
                task.run()
                return@addIdleHandler false
            }
        }
    }
}

Dengan cara yang sama, Anda dapat mengukur awal yang dingin dari aplikasi ini.

Emoji


Kami menggunakan emoji khusus kami alih-alih yang sistem. Berikut adalah contoh bagaimana emoji terlihat pada platform berbeda di tahun yang berbeda. Emoji kiri dan kanan cukup bagus, tetapi di tengah ...



Ada masalah kedua:



Setiap baris adalah emoji yang sama, tetapi emosi yang mereka hasilkan berbeda. Saya paling suka bagian kanan bawah, saya masih tidak mengerti apa artinya.

Ada sepeda dari VKontakte. Di ~ 2014, kami sedikit mengubah satu emoji. Mungkin seseorang ingat - "Marshmallow" tadi. Setelah perubahannya, kerusuhan kecil dimulai. Tentu saja, dia tidak mencapai level "kembalilah ke tembok", tetapi reaksinya cukup menarik. Dan ini memberitahu kita tentang pentingnya menafsirkan emoji.

Bagaimana emoji dibuat: kita memiliki bitmap besar, di mana mereka semua dikumpulkan dalam satu "atlas" besar. Ada beberapa dari mereka - di bawah DPI berbeda. Dan ada EmojiSpan yang berisi informasi: Saya menggambar emoji "ini-dan-itu", itu berada dalam bitmap ini-dan-itu di lokasi ini-dan-itu (x, y).
Dan ada ReplacementSpan yang memungkinkan Anda untuk menampilkan sesuatu, bukan teks di bawah Span.
Yaitu, Anda menemukan emoji dalam teks, membungkusnya dengan EmojiSpan, dan sistem menggambar emoji yang diinginkan daripada sistem satu.



Alternatif


Memompa


Seseorang mungkin mengatakan bahwa karena mengembang lambat, mengapa tidak hanya membuat tata letak dengan tangan Anda, menghindari mengembang. Dan dengan demikian mempercepat semuanya dengan menghindari 100500 ViewHolder. Itu hanya khayalan. Sebelum Anda melakukan sesuatu, ada baiknya mengukurnya.

Android memiliki kelas Debug, ia memiliki startMethodTracing dan stopMethodTracing.

Debug.startMethodTracing(“trace»)
inflate(...)
Debug.stopMethodTracing()

Ini akan memungkinkan kami untuk mengumpulkan informasi tentang waktu eksekusi potongan kode tertentu.



Dan kita melihat bahwa di sini mengembang seperti itu bahkan tidak terlihat. Seperempat dari waktu dihabiskan memuat gambar, seperempatnya memuat warna. Dan hanya di suatu tempat di bagian dll yang mengembang kami.

Saya mencoba menerjemahkan tata letak XML ke dalam kode dan menyimpan sekitar 0,5 ms. Sebenarnya, peningkatan itu bukan yang paling mengesankan. Dan kodenya menjadi jauh lebih rumit. Artinya, menulis ulang tidak masuk akal.

Selain itu, dalam praktiknya, banyak yang tidak akan menemui masalah ini sama sekali, karena inflasi yang lama biasanya terjadi hanya ketika aplikasi menjadi sangat besar. Dalam aplikasi VKontakte kami, misalnya, ada sekitar 200-300 layar berbeda, dan pemuatan semua sumber daya macet. Apa yang harus dilakukan dengan ini masih belum jelas. Kemungkinan besar, Anda harus menulis manajer sumber daya Anda sendiri.

Anko


Anko baru-baru ini menjadi usang. Bagaimanapun, Anko bukanlah sihir, tetapi gula sintaksis sederhana. Ini menerjemahkan semuanya ke View baru bersyarat () dengan cara yang sama. Karena itu, tidak ada manfaat dari Anko.

Litho / Bergetar


Mengapa saya menggabungkan dua hal yang sama sekali tidak terkait? Karena ini bukan tentang teknologi, tetapi tentang kompleksitas migrasi ke sana. Anda tidak bisa hanya meminjam dan pindah ke perpustakaan baru.

Tidak jelas apakah ini akan memberi kita peningkatan kinerja. Dan kami tidak akan mendapatkan masalah baru, karena jutaan orang dengan perangkat yang sangat berbeda menggunakan aplikasi kami setiap menit (Anda mungkin belum pernah mendengar tentang seperempat dari mereka). Selain itu, pesan adalah basis kode yang sangat besar. Tidak mungkin untuk menulis ulang semuanya secara instan. Dan melakukannya karena hype teknologi itu bodoh. Terutama ketika Jetpack Compose tampak di suatu tempat yang jauh.

Menulis Jetpack


Google semua menjanjikan kita manna dari surga dalam bentuk perpustakaan ini, tetapi masih dalam alfa. Dan kapan akan di rilis - tidak jelas. Apakah kita bisa mendapatkannya dalam bentuk saat ini juga tidak jelas. Masih terlalu dini untuk bereksperimen. Biarkan keluar dengan stabil, biarkan bug utama tutup. Dan baru kemudian kita akan melihat ke arahnya.

Satu Tampilan Kustom Besar


Ada pendekatan lain yang dibicarakan oleh para pengirim pesan instan: "ambil dan tulis satu Tampilan Kustom yang besar, tidak ada hierarki yang rumit."

Apa kerugiannya?

  • Sulit dipertahankan.
  • Itu tidak masuk akal dalam kenyataan saat ini.

Dengan Android 4.3, sistem caching internal di dalam View dipompa. Misalnya, onMeasure tidak dipanggil jika tampilan tidak berubah. Dan hasil pengukuran sebelumnya digunakan.

Dengan Android 4.3-4.4, RenderNode (DisplayList) muncul, render caching. Mari kita lihat sebuah contoh. Misalkan ada sel dalam daftar dialog: avatar, judul, subtitle, status baca, waktu, avatar lain. Persyaratan - 10 elemen. Dan kami menulis Tampilan Kustom. Dalam hal ini, ketika mengubah satu properti, kami akan mengukur ulang semua elemen. Artinya, hanya menghabiskan sumber daya tambahan. Dalam kasus ViewGroup, di mana setiap elemen adalah Tampilan terpisah, ketika Anda mengubah satu Tampilan, kami hanya akan membatalkan satu Tampilan (kecuali bila Tampilan ini memengaruhi ukuran yang lain).

Ringkasan


Jadi, Anda telah belajar bahwa kami menggunakan RecyclerView klasik dengan optimisasi standar. Ada bagian yang tidak standar, di mana yang paling penting dan mendasar adalah membagi pesan menjadi ViewHolder. Tentu saja, Anda dapat mengatakan bahwa ini berlaku secara sempit, tetapi pendekatan ini juga dapat diproyeksikan ke hal-hal lain, misalnya, pada teks besar 10 ribu karakter. Ini dapat dibagi menjadi paragraf, di mana setiap paragraf adalah ViewHolder terpisah.

Ini juga layak memaksimalkan segala sesuatu di @WorkerThread: tautan parsing, DiffUtils - dengan demikian membongkar @UiThead sebanyak mungkin.

Global RecycledViewPool memungkinkan Anda berjalan di antara layar pesan dan tidak membuat ViewHolder setiap kali.

Tetapi ada hal-hal penting lainnya yang belum kami putuskan, misalnya, inflasi yang lama, atau lebih tepatnya, memuat data dari sumber daya.

, Mobius 2019 Piter , . , , SQLite, . Mobius 2020 Piter .

All Articles