Rendering lambat

Rendering UI adalah tindakan menghasilkan frame dari aplikasi dan menampilkannya di layar. Untuk membantu memastikan bahwa interaksi pengguna dengan aplikasi berjalan lancar, aplikasi harus merender frame dalam waktu di bawah 16 md untuk mencapai 60 frame per detik (fps). Untuk memahami alasan pemilihan 60 fps, lihat Pola Performa Android: Mengapa 60 fps?. Jika Anda mencoba mencapai 90 fps, periode ini akan turun menjadi 11 md, dan untuk 120 fps, periodenya akan turun menjadi 8 md.

Jika Anda mengganti periode ini selama 1 md, bukan berarti frame ditampilkan terlambat 1 md, tetapi Choreographer akan menghapus frame sepenuhnya. Jika aplikasi mengalami rendering UI yang lambat, sistem akan dipaksa untuk melewati frame dan pengguna akan mengalami ketersendatan di aplikasi. Hal ini disebut jank. Halaman ini menunjukkan cara mendiagnosis dan memperbaiki jank.

Jika Anda mengembangkan game yang tidak menggunakan sistem View, Anda akan mengabaikan Choreographer. Dalam hal ini, Library Frame Pacing akan membantu game OpenGL dan Vulkan mencapai rendering yang lancar dan memperbaiki kecepatan frame di Android.

Untuk membantu meningkatkan kualitas aplikasi, Android otomatis memantau aplikasi untuk menemukan jank dan menampilkan informasi di dasbor Android vitals. Untuk mengetahui informasi tentang cara mengumpulkan data, lihat Memantau kualitas teknis aplikasi dengan Android vitals.

Mengidentifikasi jank

Menemukan kode di aplikasi yang menyebabkan jank bisa jadi sulit. Bagian ini menjelaskan tiga metode untuk mengidentifikasi jank:

Pemeriksaan visual memungkinkan Anda memeriksa semua kasus penggunaan dengan cepat di aplikasi dalam beberapa menit, tetapi pemeriksaan ini tidak memberikan detail sebanyak Systrace. Systrace menyediakan lebih banyak detail, tetapi jika Anda menjalankan Systrace untuk semua kasus penggunaan di aplikasi, Anda bisa dibanjiri begitu banyak data yang sulit dianalisis. Pemeriksaan visual dan Systrace mendeteksi jank di perangkat lokal. Jika tidak dapat mereproduksi jank di perangkat lokal, Anda dapat mem-build pemantauan performa kustom untuk mengukur bagian tertentu aplikasi di perangkat yang berjalan di kolom.

Pemeriksaan visual

Pemeriksaan visual membantu Anda mengidentifikasi kasus penggunaan yang menghasilkan jank. Untuk melakukan pemeriksaan visual, buka aplikasi dan periksa secara manual bagian lain dari aplikasi, lalu cari jank di UI.

Berikut beberapa tips saat melakukan pemeriksaan visual:

  • Jalankan versi rilis—atau setidaknya versi yang tidak dapat di-debug—dari aplikasi Anda. Waktu proses ART menonaktifkan beberapa pengoptimalan penting untuk mendukung fitur proses debug. Jadi, pastikan apa yang Anda lihat mirip dengan apa yang akan dilihat oleh pengguna.
  • Aktifkan Rendering GPU Profil. Rendering GPU Profil menampilkan grafik batang di layar yang memberi Anda representasi visual cepat terkait durasi yang diperlukan untuk merender frame jendela UI yang terkait dengan tolok ukur 16 md per frame. Setiap batang memiliki komponen berwarna yang dipetakan ke suatu tahap dalam pipeline rendering sehingga Anda dapat melihat porsi mana yang memerlukan waktu terlama. Misalnya, jika frame menghabiskan banyak waktu dalam menangani input, lihat kode aplikasi yang menangani input pengguna.
  • Jalankan komponen yang merupakan sumber umum jank, seperti RecyclerView.
  • Luncurkan aplikasi dari cold start.
  • Jalankan aplikasi di perangkat yang lebih lambat untuk memperburuk masalah.

Saat menemukan kasus penggunaan yang menyebabkan jank, Anda mungkin telah mengetahui penyebab jank di aplikasi. Jika memerlukan informasi selengkapnya, Anda dapat menggunakan Systrace untuk mencari tahu penyebabnya lebih lanjut.

Systrace

Meskipun Systrace adalah alat yang menunjukkan performa seluruh aplikasi, alat ini dapat bermanfaat untuk mengidentifikasi jank di aplikasi. Systrace memiliki overhead sistem minimal sehingga Anda dapat mengalami jank yang realistis selama instrumentasi.

Rekam aktivitas menggunakan Systrace saat melakukan kasus penggunaan jank di perangkat. Untuk mengetahui petunjuk cara menggunakan Systrace, lihat Merekam pelacakan sistem di command line. Systrace dibagi menurut proses dan thread. Cari proses aplikasi di Systrace yang terlihat seperti gambar 1.

Contoh Systrace
Gambar 1. Contoh Systrace.

Contoh Systrace di gambar 1 berisi informasi berikut untuk mengidentifikasi jank:

  1. Systrace menunjukkan waktu penggambaran setiap frame dan memberi kode warna setiap frame untuk menandai waktu render yang lambat. Ini akan membantu Anda menemukan frame jank satu per satu dengan lebih akurat daripada saat melakukan pemeriksaan visual. Untuk mengetahui informasi selengkapnya, lihat Memeriksa peringatan dan frame UI.
  2. Systrace mendeteksi masalah di aplikasi dan menampilkan peringatan di setiap frame dan panel peringatan. Sebaiknya ikuti petunjuk di peringatan.
  3. Bagian-bagian dari framework dan library Android, seperti RecyclerView, berisi penanda rekaman aktivitas. Jadi, linimasa systrace menunjukkan kapan metode tersebut dieksekusi di UI thread dan durasi waktu yang diperlukan untuk mengeksekusinya.

Setelah melihat output Systrace, mungkin ada metode di aplikasi yang Anda duga menyebabkan jank. Misalnya, jika linimasa menunjukkan bahwa frame lambat disebabkan oleh RecyclerView yang memerlukan waktu lama, Anda dapat menambahkan peristiwa rekaman aktivitas kustom ke kode yang relevan dan menjalankan ulang Systrace untuk mendapatkan informasi selengkapnya. Di Systrace baru, linimasa menunjukkan waktu pemanggilan metode aplikasi dan durasi waktu yang diperlukan untuk mengeksekusinya.

Jika Systrace tidak menampilkan detail tentang alasan pekerjaan UI thread memerlukan waktu lama, gunakan CPU Profiler Android untuk mencatat pelacakan metode yang dijadikan sampel atau yang diinstrumentasikan. Umumnya, pelacakan metode tidak tepat untuk mengidentifikasi jank karena menghasilkan jank positif palsu akibat overhead yang berat, dan tidak dapat melihat kapan thread berjalan atau diblokir. Namun, pelacakan metode dapat membantu Anda mengidentifikasi metode di aplikasi yang memerlukan waktu paling banyak. Setelah mengidentifikasi metode ini, tambahkan penanda Rekaman Aktivitas lalu jalankan kembali Systrace untuk melihat apakah metode ini menyebabkan jank atau tidak.

Untuk mengetahui informasi selengkapnya, lihat Memahami Systrace.

Pemantauan performa kustom

Jika tidak dapat mereproduksi jank di perangkat lokal, Anda dapat mem-build pemantauan performa kustom di aplikasi untuk membantu mengidentifikasi sumber jank di perangkat yang ada di kolom.

Untuk melakukannya, kumpulkan waktu render frame dari bagian-bagian tertentu aplikasi dengan FrameMetricsAggregator lalu catat dan analisis data menggunakan Firebase Performance Monitoring.

Untuk mempelajari lebih lanjut, lihat Memulai Performance Monitoring untuk Android.

Periode frozen

Periode frozen adalah frame UI yang memerlukan waktu lebih dari 700 md untuk dirender. Ini menjadi masalah karena aplikasi tampak macet dan tidak responsif terhadap input pengguna selama hampir satu detik saat frame sedang dirender. Sebaiknya optimalkan aplikasi untuk merender frame dalam waktu 16 md untuk memastikan UI yang lancar. Namun, selama aplikasi dimulai atau saat bertransisi ke layar yang berbeda, normal saja jika memerlukan waktu lebih dari 16 md untuk menggambar frame awal karena aplikasi harus meng-inflate tampilan, membuat tata letak layar, dan melakukan penggambaran awal dari nol. Itu sebabnya Android melacak periode frozen secara terpisah dari rendering lambat. Tidak boleh ada frame di aplikasi yang memerlukan waktu lebih dari 700 md untuk dirender.

Untuk membantu meningkatkan kualitas aplikasi, Android otomatis memantau aplikasi untuk menemukan periode frozen dan menampilkan informasi di dasbor Android Vitals. Untuk mengetahui informasi tentang cara mengumpulkan data, lihat Memantau kualitas teknis aplikasi dengan Android vitals.

Periode frozen adalah bentuk ekstrem dari rendering lambat, sehingga prosedur untuk mendiagnosis dan memperbaiki masalahnya sama.

Melacak jank

FrameTimeline di Perfetto dapat membantu melacak periode lambat atau periode frozen.

Hubungan antara periode lambat, periode frozen, dan ANR

Periode lambat, periode frozen, dan ANR adalah berbagai bentuk jank yang mungkin ditemui aplikasi Anda. Lihat tabel di bawah untuk memahami perbedaannya.

Periode lambat Periode frozen ANR
Waktu rendering Antara 16 md dan 700 md Antara 700 md dan 5 d Lebih dari 5 d
Area dampak yang terlihat oleh pengguna
  • Scroll RecyclerView berperilaku tiba-tiba
  • Pada layar dengan animasi kompleks yang tidak dianimasikan dengan benar
  • Selama startup aplikasi
  • Berpindah dari satu layar ke layar lainnya—misalnya, transisi layar
  • Saat aktivitas berada di latar depan, aplikasi tidak merespons peristiwa input atau BroadcastReceiver—seperti peristiwa menekan tombol atau mengetuk layar—dalam waktu lima detik.
  • Meskipun Anda tidak memiliki aktivitas di latar depan, BroadcastReceiver belum selesai mengeksekusi dalam waktu yang cukup lama.

Melacak periode lambat dan periode frozen secara terpisah

Selama aplikasi dimulai atau saat bertransisi ke layar yang berbeda, normal saja jika memerlukan waktu lebih dari 16 md untuk menggambar frame awal karena aplikasi harus meng-inflate tampilan, membuat tata letak layar, dan melakukan penggambaran awal dari nol.

Praktik terbaik untuk memprioritaskan dan me-resolve jank

Ingatlah praktik terbaik berikut saat mencoba me-resolve jank di aplikasi Anda:

  • Identifikasi dan selesaikan instance jank yang paling mudah direproduksi.
  • Prioritaskan ANR. Meskipun periode lambat atau periode frozen dapat membuat aplikasi tampak lambat, ANR menyebabkan aplikasi berhenti merespons.
  • Rendering lambat sulit direproduksi, tetapi Anda dapat memulai dengan mengakhiri periode frozen 700 md. Hal ini paling umum terjadi saat aplikasi memulai atau mengubah layar.

Memperbaiki jank

Untuk memperbaiki jank, periksa frame mana yang tidak selesai dalam waktu 16 md, lalu cari bagian yang salah. Periksa apakah Record View#draw atau Layout memerlukan waktu yang sangat lama di beberapa frame. Lihat Sumber umum jank untuk masalah ini dan juga masalah lainnya.

Untuk menghindari jank, jalankan tugas yang berjalan lama secara asinkron di luar UI thread. Selalu waspadai thread tempat kode dijalankan dan berhati-hatilah saat memposting tugas yang sulit ke thread utama.

Jika Anda memiliki UI primer yang kompleks dan penting untuk aplikasi—seperti daftar scroll pusat—sebaiknya tulis uji instrumentasi yang dapat secara otomatis mendeteksi waktu render lambat dan sering menjalankan pengujian untuk mencegah regresi.

Sumber umum jank

Bagian berikut menjelaskan sumber umum jank di aplikasi menggunakan sistem View dan praktik terbaik untuk mengatasinya. Untuk mengetahui informasi tentang cara memperbaiki masalah performa pada Jetpack Compose, lihat Performa Jetpack Compose.

Daftar yang dapat di-scroll

ListView—dan terutama RecyclerView—biasanya digunakan untuk daftar scroll kompleks yang sangat rentan terhadap jank. Keduanya berisi penanda Systrace sehingga Anda dapat menggunakan Systrace untuk melihat apakah keduanya penyebab jank di aplikasi atau bukan. Teruskan argumen command line -a <your-package-name> agar bagian rekaman aktivitas di RecyclerView—serta penanda rekaman aktivitas yang Anda tambahkan—dapat muncul. Jika tersedia, ikuti pedoman notifikasi yang dibuat di output Systrace. Di dalam Systrace, Anda dapat mengklik bagian yang direkam RecyclerView untuk melihat penjelasan mengenai pekerjaan yang dilakukan RecyclerView.

RecyclerView: notifyDataSetChanged()

Jika Anda melihat setiap item di RecyclerView di-binding ulang—dan, oleh karena itu, diletakkan ulang dan digambar ulang dalam satu frame—pastikan bahwa Anda tidak sedang memanggil notifyDataSetChanged(), setAdapter(Adapter), atau swapAdapter(Adapter, boolean) untuk update kecil. Metode ini menandakan bahwa ada perubahan pada seluruh konten daftar dan muncul di Systrace sebagai RV FullInvalidate. Sebagai gantinya, gunakan SortedList atau DiffUtil untuk menghasilkan update minimal saat konten diubah atau ditambahkan.

Misalnya, pertimbangkan aplikasi yang menerima versi baru daftar konten berita dari server. Jika memposting informasi ini ke Adaptor, Anda dapat memanggil notifyDataSetChanged(), seperti yang ditunjukkan dalam contoh berikut:

Kotlin

fun onNewDataArrived(news: List<News>) {
    myAdapter.news = news
    myAdapter.notifyDataSetChanged()
}

Java

void onNewDataArrived(List<News> news) {
    myAdapter.setNews(news);
    myAdapter.notifyDataSetChanged();
}

Kelemahannya adalah jika ada perubahan kecil, seperti satu item yang ditambahkan ke atas, RecyclerView tidak akan mengetahuinya. Oleh karena itu, Anda diberi tahu untuk melepaskan seluruh status item yang disimpan dalam cache dan harus di-bind ulang semuanya.

Sebaiknya gunakan DiffUtil yang menghitung dan mengirimkan update minimal untuk Anda:

Kotlin

fun onNewDataArrived(news: List<News>) {
    val oldNews = myAdapter.items
    val result = DiffUtil.calculateDiff(MyCallback(oldNews, news))
    myAdapter.news = news
    result.dispatchUpdatesTo(myAdapter)
}

Java

void onNewDataArrived(List<News> news) {
    List<News> oldNews = myAdapter.getItems();
    DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news));
    myAdapter.setNews(news);
    result.dispatchUpdatesTo(myAdapter);
}

Untuk memberi tahu cara memeriksa daftar Anda ke DiffUtil, tentukan MyCallback sebagai implementasi Callback.

RecyclerView: Nested RecyclerViews

Bukan hal yang aneh untuk membuat beberapa instance RecyclerView, terutama dengan daftar vertikal dari daftar scroll horizontal. Contohnya adalah petak aplikasi di halaman utama Play Store. Ini dapat berfungsi dengan baik, tetapi juga melibatkan banyak tampilan yang bergerak.

Jika Anda melihat banyak item bagian dalam meng-inflate saat pertama kali men-scroll halaman ke bawah, sebaiknya periksa apakah Anda membagikan RecyclerView.RecycledViewPool di antara instance RecyclerView bagian dalam (horizontal). Secara default, setiap RecyclerView memiliki kumpulan itemnya sendiri. Namun, pada kasus dengan belasan itemViews di layar sekaligus, akan menjadi masalah jika itemViews tidak dapat dibagikan oleh daftar horizontal yang berbeda jika semua baris menunjukkan jenis tampilan yang serupa.

Kotlin

class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() {

    ...

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // Inflate inner item, find innerRecyclerView by ID.
        val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false)
        innerRv.apply {
            layoutManager = innerLLM
            recycledViewPool = sharedPool
        }
        return OuterAdapter.ViewHolder(innerRv)
    }
    ...

Java

class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
    RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool();

    ...

    @Override
    public void onCreateViewHolder(ViewGroup parent, int viewType) {
        // Inflate inner item, find innerRecyclerView by ID.
        LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(),
                LinearLayoutManager.HORIZONTAL);
        innerRv.setLayoutManager(innerLLM);
        innerRv.setRecycledViewPool(sharedPool);
        return new OuterAdapter.ViewHolder(innerRv);

    }
    ...

Jika ingin mengoptimalkan lebih jauh, Anda juga dapat memanggil setInitialPrefetchItemCount(int) di LinearLayoutManager dari RecyclerView bagian dalam. Misalnya, jika Anda selalu memiliki 3,5 item yang terlihat dalam satu baris, panggil innerLLM.setInitialItemPrefetchCount(4). Tindakan ini akan memberi tahu RecyclerView bahwa saat baris horizontal akan muncul di layar, baris tersebut harus mencoba mengambil data item di dalam terlebih dahulu jika ada waktu luang di UI thread.

RecyclerView: Terlalu banyak inflation atau Create memerlukan waktu terlalu lama

Pada umumnya, fitur pengambilan data di RecyclerView dapat membantu mengatasi biaya inflation dengan melakukan pekerjaan terlebih dahulu saat tidak ada aktivitas di UI thread. Jika Anda melihat inflation selama frame dan bukan di bagian yang diberi label RV Prefetch, pastikan Anda melakukan pengujian pada perangkat yang didukung dan menggunakan Support Library versi terbaru. RV Prefetch hanya didukung di Android 5.0 Level API 21 dan yang lebih tinggi.

Jika Anda sering melihat inflation yang menyebabkan jank saat item baru muncul di layar, verifikasi bahwa jenis tampilan yang Anda miliki tidak melebihi jumlah yang dibutuhkan. Makin sedikit jenis tampilan dalam konten RecyclerView, makin sedikit inflation yang perlu dilakukan saat jenis item baru muncul di layar. Jika memungkinkan dan wajar, gabungkan jenis tampilan. Jika hanya ikon, warna, atau potongan teks yang berubah antarjenis, Anda dapat membuat perubahan tersebut pada waktu binding dan menghindari inflation sehingga akan mengurangi jejak memori aplikasi secara bersamaan.

Jika jenis tampilan terlihat bagus, usahakan untuk mengurangi biaya inflation. Mengurangi container dan tampilan struktural yang tidak diperlukan dapat membantu. Sebaiknya build itemViews dengan ConstraintLayout sehingga dapat membantu mengurangi tampilan struktural.

Jika ingin mengoptimalkan performa lebih jauh, hierarki item Anda sederhana, dan Anda tidak memerlukan fitur tema serta gaya yang kompleks, sebaiknya panggil konstruktor sendiri. Namun, sering kali tidak ada manfaatnya jika kesederhanaan dan fitur XML hilang.

RecyclerView: Binding memerlukan waktu terlalu lama

Binding—yaitu, onBindViewHolder(VH, int)— harus mudah dan menghabiskan waktu kurang dari satu milidetik (md) untuk semuanya kecuali item yang paling kompleks. Binding harus mengambil item objek Java lama (POJO) biasa dari data item internal adaptor dan memanggil penyetel pada tampilan di ViewHolder. Jika RV OnBindView memerlukan waktu yang terlalu lama, pastikan Anda melakukan pekerjaan minimal di kode binding.

Jika menggunakan objek POJO sederhana untuk menyimpan data di adaptor, Anda dapat benar-benar menghindari penulisan kode binding di onBindViewHolder menggunakan Library Data Binding

RecyclerView atau ListView: Tata letak atau menggambar memerlukan waktu terlalu lama

Untuk masalah pada menggambar dan tata letak, lihat bagian Performa tata letak dan Performa rendering.

ListView: Inflation

Anda dapat menonaktifkan daur ulang di ListView secara tidak sengaja jika tidak berhati-hati. Jika Anda melihat inflation setiap kali item muncul di layar, pastikan bahwa implementasi Adapter.getView() menggunakan, melakukan binding ulang, dan menampilkan parameter convertView. Jika implementasi getView() Anda selalu meng-inflate, aplikasi tidak akan mendapatkan manfaat dari daur ulang di ListView. Struktur getView() Anda harus hampir selalu mirip dengan implementasi berikut:

Kotlin

fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply {
        // Bind content from position to convertView.
    }
}

Java

View getView(int position, View convertView, ViewGroup parent) {

    if (convertView == null) {
        // Only inflate if no convertView passed.
        convertView = layoutInflater.inflate(R.layout.my_layout, parent, false)
    }
    // Bind content from position to convertView.
    return convertView;
}

Performa tata letak

Jika Systrace menunjukkan bahwa segmen Tata Letak Choreographer#doFrame bekerja terlalu banyak atau terlalu sering, ini artinya Anda mengalami masalah performa tata letak. Performa tata letak aplikasi bergantung pada bagian hierarki tampilan yang memiliki parameter atau input tata letak yang berubah.

Performa tata letak: Biaya

Jika segmen lebih lama beberapa milidetik, ada kemungkinan bahwa Anda mengalami skenario performa penyusunan bertingkat terburuk untuk RelativeLayouts, atau weighted-LinearLayouts. Setiap tata letak ini dapat memicu beberapa penerusan ukuran dan tata letak turunannya. Jadi, membuatnya bertingkat dapat menyebabkan perilaku O(n^2) pada kedalaman tingkatan.

Coba hindari RelativeLayout atau fitur berat LinearLayout di semua node daun hierarki, kecuali yang terendah. Berikut adalah cara untuk melakukannya:

  • Mengatur ulang tampilan struktural.
  • Menentukan logika tata letak kustom. Lihat Mengoptimalkan hierarki tata letak untuk mengetahui contoh yang spesifik. Anda dapat mencoba mengonversi ke ConstraintLayout yang menyediakan fitur serupa, tetapi tanpa kelemahan performa.

Performa tata letak: Frekuensi

Tata letak diharapkan terjadi saat konten baru muncul di layar, misalnya saat item baru di-scroll ke tampilan di RecyclerView. Jika tata letak yang signifikan terjadi di setiap frame, ada kemungkinan Anda menganimasi tata letak sehingga menyebabkan penurunan frame.

Umumnya, animasi harus berjalan di properti gambar View, seperti berikut:

Anda dapat mengubah semua ini menjadi jauh lebih murah daripada properti tata letak, seperti padding, atau margin. Umumnya, juga jauh lebih murah untuk mengubah properti menggambar tampilan dengan memanggil penyetel yang memicu invalidate(), diikuti dengan draw(Canvas) di frame berikutnya. Tindakan ini akan mencatat ulang operasi menggambar untuk tampilan yang tidak divalidasi, dan juga umumnya jauh lebih murah daripada tata letak.

Performa rendering

UI Android bekerja dalam dua fase:

  • Record View#draw di UI thread, yang menjalankan draw(Canvas) di setiap tampilan yang tidak divalidasi, dan dapat memanggil panggilan ke dalam tampilan kustom atau ke dalam kode Anda.
  • DrawFrame pada RenderThread, yang berjalan di RenderThread native, tetapi beroperasi berdasarkan pekerjaan yang dihasilkan oleh fase Record View#draw.

Performa rendering: UI Thread

Jika Record View#draw memerlukan waktu terlalu lama, biasanya bitmap sedang digambar di UI thread. Proses menggambar ke bitmap menggunakan rendering CPU. Jadi. secara umum sebaiknya hindari hal ini jika memungkinkan. Anda dapat menggunakan pelacakan metode dengan Android CPU Profiler untuk mengetahui apakah ini masalahnya atau bukan.

Proses menggambar ke bitmap biasanya dilakukan saat aplikasi ingin mendekorasi bitmap sebelum menampilkannya—terkadang dekorasi bisa seperti menambahkan sudut membulat:

Kotlin

val paint = Paint().apply {
    isAntiAlias = true
}
Canvas(roundedOutputBitmap).apply {
    // Draw a round rect to define the shape:
    drawRoundRect(
            0f,
            0f,
            roundedOutputBitmap.width.toFloat(),
            roundedOutputBitmap.height.toFloat(),
            20f,
            20f,
            paint
    )
    paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
    // Multiply content on top to make it rounded.
    drawBitmap(sourceBitmap, 0f, 0f, paint)
    setBitmap(null)
    // Now roundedOutputBitmap has sourceBitmap inside, but as a circle.
}

Java

Canvas bitmapCanvas = new Canvas(roundedOutputBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
// Draw a round rect to define the shape:
bitmapCanvas.drawRoundRect(0, 0,
        roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
// Multiply content on top to make it rounded.
bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint);
bitmapCanvas.setBitmap(null);
// Now roundedOutputBitmap has sourceBitmap inside, but as a circle.

Jika ini adalah jenis pekerjaan yang sedang Anda lakukan di UI thread, Anda dapat melakukannya di thread decoding di latar belakang. Dalam beberapa kasus, seperti contoh sebelumnya, Anda bahkan dapat melakukan pekerjaan pada waktu menggambar. Jadi, jika kode Drawable atau View terlihat seperti ini:

Kotlin

fun setBitmap(bitmap: Bitmap) {
    mBitmap = bitmap
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawBitmap(mBitmap, null, paint)
}

Java

void setBitmap(Bitmap bitmap) {
    mBitmap = bitmap;
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, null, paint);
}

Anda dapat menggantinya dengan ini:

Kotlin

fun setBitmap(bitmap: Bitmap) {
    shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawRoundRect(0f, 0f, width, height, 20f, 20f, shaderPaint)
}

Java

void setBitmap(Bitmap bitmap) {
    shaderPaint.setShader(
            new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint);
}

Anda juga dapat melakukannya untuk perlindungan latar belakang, seperti saat menggambar gradien di atas bitmap, dan pemfilteran gambar dengan ColorMatrixColorFilter—dua operasi umum lainnya dilakukan dengan memodifikasi bitmap.

Jika Anda menggambar ke bitmap karena alasan lain—mungkin menggunakannya sebagai cache—coba gambar ke Canvas dengan akselerasi hardware yang diteruskan langsung ke View atau Drawable Anda. Jika perlu, sebaiknya pertimbangkan juga untuk memanggil setLayerType() dengan LAYER_TYPE_HARDWARE untuk menyimpan cache output rendering yang kompleks dan masih memanfaatkan rendering GPU.

Performa rendering: RenderThread

Beberapa operasi Canvas murah untuk direkam, tetapi memicu komputasi mahal di RenderThread. Systrace biasanya memanggilnya dengan notifikasi.

Menganimasi Jalur besar

Saat Canvas.drawPath() dipanggil di Canvas yang diakselerasi oleh hardware dan diteruskan ke View, Android akan menggambar jalur ini terlebih dahulu di CPU lalu menguploadnya ke GPU. Jika Anda memiliki jalur besar, jangan mengeditnya dari frame ke frame agar jalur besar tersebut dapat disimpan dalam cache dan digambar secara efisien. drawPoints(), drawLines(), dan drawRect/Circle/Oval/RoundRect() lebih efisien dan lebih baik digunakan meskipun Anda menggunakan lebih banyak panggilan gambar.

Canvas.clipPath

clipPath(Path) memicu perilaku clipping yang mahal, dan secara umum harus dihindari. Jika memungkinkan, pilih menggambar bentuk, dan bukan clipping ke bentuk selain persegi panjang. Performanya lebih baik dan mendukung anti-aliasing. Misalnya, panggilan clipPath berikut dapat dinyatakan dengan cara yang berbeda:

Kotlin

canvas.apply {
    save()
    clipPath(circlePath)
    drawBitmap(bitmap, 0f, 0f, paint)
    restore()
}

Java

canvas.save();
canvas.clipPath(circlePath);
canvas.drawBitmap(bitmap, 0f, 0f, paint);
canvas.restore();

Sebagai gantinya, nyatakan contoh sebelumnya sebagai berikut:

Kotlin

paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
// At draw time:
canvas.drawPath(circlePath, mPaint)

Java

// One time init:
paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
// At draw time:
canvas.drawPath(circlePath, mPaint);
Upload bitmap

Android menampilkan bitmap sebagai tekstur OpenGL, dan saat pertama kali ditampilkan di frame, bitmap diupload ke GPU. Anda dapat melihatnya di Systrace sebagai lebar x tinggi upload Tekstur(id). Tindakan ini memerlukan waktu beberapa milidetik, seperti yang ditunjukkan pada gambar 2, tetapi perlu menampilkan gambar dengan GPU.

Jika tindakan ini memerlukan waktu lama, sebaiknya terlebih dahulu periksa jumlah lebar dan tinggi di rekaman aktivitas. Pastikan bahwa ukuran bitmap yang sedang ditampilkan tidak terlampau besar daripada area di layar yang menampilkannya. Jika ya, ukuran file yang terlampau besar ini akan menghabiskan waktu upload dan memori. Umumnya, library pemuatan Bitmap menyediakan cara mudah untuk meminta bitmap yang berukuran sesuai.

Di Android 7.0, kode pemuatan bitmap—umumnya dilakukan oleh library—dapat memanggil prepareToDraw() untuk memicu upload lebih awal sebelum diperlukan. Dengan cara ini, upload terjadi lebih awal saat RenderThread tidak ada aktivitas. Anda dapat melakukannya setelah melakukan dekode atau saat melakukan binding bitmap ke tampilan selama Anda mengetahui bitmap tersebut. Idealnya, library pemuatan bitmap akan melakukannya untuk Anda, tetapi jika Anda mengelola library sendiri, atau ingin memastikan bahwa Anda tidak mengenai upload di perangkat yang lebih baru, Anda dapat memanggil prepareToDraw() dalam kode Anda sendiri.

Aplikasi menghabiskan waktu yang cukup lama dalam
  frame yang mengupload bitmap berukuran besar
Gambar 2. Aplikasi menghabiskan waktu yang cukup lama dalam sebuah frame yang mengupload bitmap berukuran besar. Kurangi ukurannya atau picu lebih awal saat Anda mendekodenya dengan prepareToDraw().

Keterlambatan penjadwalan thread

Penjadwal thread adalah bagian dari sistem operasi Android yang bertanggung jawab menentukan thread mana di sistem yang harus berjalan, kapan berjalannya, dan durasinya.

Kadang, jank muncul karena UI Thread aplikasi diblokir atau tidak berjalan. Systrace menggunakan berbagai warna, seperti yang ditunjukkan pada gambar 3, untuk menunjukkan kapan thread tidur (abu-abu), dapat dijalankan (biru: dapat berjalan, tetapi tidak dipilih oleh penjadwal untuk dijalankan), aktif berjalan (hijau), atau dalam tidur tanpa gangguan (merah atau oranye). Penggunaan berbagai warna ini sangat berguna untuk mendebug masalah jank yang disebabkan oleh keterlambatan penjadwalan thread.

Menyorot periode saat UI thread
  sedang tidur
Gambar 3. Sorotan periode saat UI thread sedang tidur.

Sering kali, panggilan binder—mekanisme komunikasi antarproses (IPC) di Android—menyebabkan jeda yang lama dalam eksekusi aplikasi. Di versi terbaru Android, hal tersebut menjadi salah satu alasan paling umum UI thread berhenti berjalan. Umumnya, perbaikannya adalah menghindari pemanggilan fungsi yang melakukan panggilan binder. Jika tidak dapat dihindari, simpan nilai di cache atau pindahkan pekerjaan ke thread latar belakang. Dengan makin besarnya codebase, Anda dapat secara tidak sengaja menambahkan panggilan binder dengan memanggil beberapa metode level rendah jika tidak berhati-hati. Namun, Anda dapat menemukan dan memperbaikinya dengan melakukan perekaman.

Jika memiliki transaksi binder, Anda dapat merekam stack panggilannya dengan perintah adb berikut:

$ adb shell am trace-ipc start
… use the app - scroll/animate ...
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt

Kadang panggilan yang tampak tidak berbahaya, seperti getRefreshRate(), dapat memicu transaksi binder dan menyebabkan masalah besar jika sering dipanggil. Perekaman secara berkala dapat membantu Anda menemukan dan memperbaiki masalah tersebut jika muncul.

Menampilkan UI thread yang tidur karena transaksi
  binder dalam RV fling. Buat logika binding sederhana, dan gunakan trace-ipc untuk
  melacak dan menghapus panggilan binder.
Gambar 4. UI thread sedang tidur karena transaksi binder dalam RV fling. Buat logika binding sederhana, dan gunakan trace-ipc untuk melacak dan menghapus panggilan binder.

Jika Anda tidak melihat aktivitas binder, tetapi masih juga tidak melihat UI thread berjalan, pastikan bahwa Anda tidak menunggu penguncian atau operasi lain dari thread lain. Biasanya, UI thread tidak perlu menunggu hasil dari thread lain. Thread lain harus memposting informasi ke UI thread.

Alokasi objek dan pembersihan sampah memori

Alokasi objek dan pembersihan sampah memori (GC) telah menjadi masalah yang tidak begitu signifikan karena ART diperkenalkan sebagai waktu proses default di Android 5.0, tetapi masih mungkin untuk membebani thread dengan pekerjaan ekstra ini. Boleh saja mengalokasikan dengan tujuan merespons peristiwa langka yang tidak sering terjadi dalam satu detik—seperti pengguna mengetuk tombol—tetapi perlu diingat bahwa setiap alokasi menimbulkan biaya. Jika alokasi berada dalam loop padat yang sering dipanggil, sebaiknya hindari alokasi untuk meringankan beban pada GC.

Systrace akan menunjukkan kepada Anda apakah GC sering berjalan, dan Memory Profiler Android dapat menunjukkan asal alokasi. Anda tidak akan mengalami kesulitan jika dapat menghindari alokasi, terutama dalam loop yang ketat.

Menampilkan GC 94 md di HeapTaskDaemon
Gambar 5. GC 94 md di thread HeapTaskDaemon.

Di versi terbaru Android, GC umumnya berjalan di thread latar belakang yang bernama HeapTaskDaemon. Jumlah alokasi yang signifikan dapat berarti lebih banyak resource CPU yang dihabiskan di GC, seperti yang ditunjukkan pada gambar 5.