Rendering lambat

Rendering UI adalah tindakan menghasilkan frame dari aplikasi dan menampilkannya di layar. Untuk memastikan bahwa interaksi pengguna dengan aplikasi lancar, aplikasi harus merender frame dalam waktu di bawah 16 md untuk mencapai 60 frame per detik (mengapa 60 fps?). Jika aplikasi mengalami rendering UI yang lambat, sistem akan dipaksa untuk melewati frame dan pengguna akan melihat ketersendatan di aplikasi. Kami menyebut hal ini sebagai jank.

Untuk membantu meningkatkan kualitas aplikasi, Android otomatis memantau aplikasi untuk menemukan jank dan menampilkan informasi di dasbor Android vitals. Untuk mengetahui informasi tentang cara data dikumpulkan, lihat dokumen Konsol Play.

Jika aplikasi mengalami jank, halaman ini menyediakan panduan untuk mendiagnosis dan memperbaiki masalah.

Mengidentifikasi jank

Menemukan kode di aplikasi yang menyebabkan jank dapat menjadi 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 akan dibanjiri begitu banyak data sehingga sulit untuk dianalisis. Pemeriksaan visual dan systrace mendeteksi jank di perangkat lokal. Jika jank tidak dapat direproduksi di perangkat lokal, Anda dapat mem-build pemantauan performa kustom untuk mengukur bagian tertentu aplikasi di perangkat yang sedang berjalan di lapangan.

Dengan pemeriksaan visual

Pemeriksaan visual membantu Anda mengidentifikasi kasus penggunaan yang menghasilkan jank. Untuk melakukan pemeriksaan visual, buka aplikasi dan secara manual periksa bagian-bagian aplikasi yang berbeda serta cari UI yang membuat jank. Berikut beberapa tips saat melakukan pemeriksaan visual:

  • Jalankan versi rilis (atau setidaknya versi yang tidak dapat di-debug) dari aplikasi Anda. Runtime 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, jadi Anda dapat melihat porsi mana yang memerlukan waktu terlama. Misalnya, jika frame menghabiskan banyak waktu dalam menangani input, sebaiknya lihat kode aplikasi yang menangani input pengguna.
  • Ada komponen tertentu, seperti RecyclerView, yang merupakan sumber umum jank. Jika aplikasi menggunakan komponen tersebut, sebaiknya periksa bagian-bagian aplikasi tersebut.
  • Terkadang jank hanya dapat direproduksi saat aplikasi diluncurkan dari cold start.
  • Coba jalankan aplikasi di perangkat yang lebih lambat untuk memperuncing masalah.

Setelah menemukan kasus penggunaan yang menyebabkan jank, Anda mungkin telah mengetahui penyebab jank di aplikasi. Namun, jika memerlukan informasi selengkapnya, Anda dapat menggunakan Systrace untuk memeriksa lebih lanjut.

Dengan Systrace

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

Catat pelacakan dengan Systrace saat melakukan kasus penggunaan jank di perangkat. Lihat Panduan Systrace untuk petunjuk terkait cara menggunakan Systrace. Systrace diuraikan menurut proses dan thread. Cari proses aplikasi di Systrace, yang terlihat seperti gambar 1.

Gambar 1: systrace

Systrace di gambar 1 berisi informasi berikut untuk mengidentifikasi jank:

  1. Systrace menunjukkan kapan tiap frame digambar dan memberi kode warna tiap 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 informasi selengkapnya, lihat Memeriksa peringatan dan frame UI.
  2. Systrace mendeteksi masalah di aplikasi dan menampilkan peringatan di tiap frame dan panel peringatan. Mengikuti petunjuk di peringatan adalah opsi terbaik.
  3. Bagian-bagian dari framework dan library Android, seperti RecyclerView, berisi penanda trace. Jadi, linimasa systrace menunjukkan kapan metode tersebut dieksekusi di UI thread dan 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 penanda Rekaman Aktivitas ke kode yang relevan dan menjalankan ulang systrace untuk informasi selengkapnya. Di systrace baru, linimasa akan menunjukkan kapan metode aplikasi dipanggil dan waktu yang diperlukan untuk mengeksekusinya.

Jika systrace tidak menunjukkan detail tentang alasan pekerjaan UI thread memerlukan waktu lama, Anda harus menggunakan 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 mengetahui kapan thread berjalan atau diblokir. Namun, pelacakan metode dapat membantu Anda mengidentifikasi metode di aplikasi yang memerlukan waktu paling banyak. Setelah mengidentifikasi metode tersebut, tambahkan penanda Pelacakan dan jalankan ulang systrace untuk melihat apakah metode tersebut menyebabkan jank.

Untuk informasi selengkapnya, lihat Memahami Systrace.

Dengan 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 di lapangan.

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 Menggunakan Firebase Performance Monitoring dengan Android Vitals.

Memperbaiki jank

Untuk memperbaiki jank, periksa frame mana yang tidak selesai dalam 16,7 md, dan cari tahu apa yang salah. Apakah Record View#draw memerlukan waktu yang sangat lama di beberapa frame, atau mungkin Layout? Lihat Sumber umum jank di bawah untuk mengetahui masalah ini, dan lainnya.

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

Jika Anda memiliki UI primer yang kompleks dan penting untuk aplikasi (mungkin daftar scroll pusat), pertimbangkan untuk menulis uji instrumentasi yang dapat otomatis mendeteksi waktu render lambat dan sering jalankan pengujian untuk mencegah regresi. Untuk informasi selengkapnya, lihat Codelab Pengujian Performa Otomatis.

Sumber umum jank

Bagian berikut menjelaskan sumber umum jank di aplikasi dan praktik terbaik untuk mengatasinya.

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 mencari tahu apakah keduanya berkontribusi terhadap jank di aplikasi. Pastikan untuk meneruskan argumen command line -a <your-package-name> agar bagian rekaman aktivitas di RecyclerView (serta semua penanda rekaman aktivitas yang Anda tambahkan) muncul. Jika tersedia, ikuti pedoman notifikasi yang dibuat di output systrace. Di dalam Systrace, Anda dapat mengklik bagian-bagian yang direkam dengan RecyclerView untuk melihat penjelasan pekerjaan yang sedang dilakukan oleh 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 tersebut menandakan bahwa seluruh konten daftar telah berubah, dan akan muncul di Systrace sebagai FullInvalidate RV. Sebagai gantinya, gunakan SortedList atau DiffUtil untuk menghasilkan update minimal saat konten berubah atau ditambahkan.

Misalnya, pertimbangkan aplikasi yang menerima versi baru daftar konten berita dari server. Jika memposting info tersebut ke Adaptor, Anda dapat memanggil notifyDataSetChanged() seperti yang ditunjukkan di bawah:

Kotlin

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

Java

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

Akan tetapi ini memiliki kelemahan besar – jika perubahannya kecil (mungkin satu item ditambahkan ke atas), RecyclerView tidak akan mengetahuinya – dan diberi tahu untuk melepaskan semua status item yang disimpan dalam cache, sehingga perlu melakukan binding ulang semua item.

Jauh lebih baik menggunakan DiffUtil, yang akan menghitung dan mengirim 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);
}

Cukup tentukan MyCallback sebagai implementasi DiffUtil.Callback untuk memberi tahu DiffUtil cara memeriksa daftar.

RecyclerView: Nested RecyclerViews

Membuat RecyclerView menjadi bertingkat adalah hal yang umum dilakukan, khususnya dengan daftar vertikal dari daftar yang di-scroll secara horizontal (seperti petak aplikasi di halaman utama Play Store). Ini dapat berfungsi dengan baik, tetapi juga melibatkan banyak tampilan yang bergerak. Jika melihat banyak item bagian dalam meng-inflate saat pertama kali men-scroll halaman ke bawah, pastikan bahwa Anda membagikan RecyclerView.RecycledViewPool antara RecyclerViews (horizontal) bagian dalam. Secara default, tiap RecyclerView akan memiliki kumpulan itemnya sendiri. Akan tetapi, 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 lanjut, Anda juga dapat memanggil setInitialPrefetchItemCount(int) di LinearLayoutManager RecyclerView bagian dalam. Misalnya, jika Anda selalu memiliki 3,5 item yang terlihat dalam satu baris, panggil innerLLM.setInitialItemPrefetchCount(4);. Ini akan memberikan tanda kepada RecyclerView bahwa saat baris horizontal akan muncul di layar, baris tersebut harus berupaya mengambil data item di dalam terlebih dahulu, jika ada waktu luang di UI thread.

RecyclerView: Terlalu banyak inflation/Membuat memerlukan waktu terlalu lama

Fitur pengambilan data di RecyclerView akan membantu mengatasi biaya inflation di sebagian besar kasus dengan melakukan pekerjaan terlebih dahulu, saat tidak ada aktivitas UI thread. Jika Anda melihat inflation selama frame (dan bukan di bagian yang berlabel Pengambilan Data RV), pastikan Anda menguji di perangkat terbaru (Saat ini Pengambilan Data hanya didukung di Android 5.0 API Level 21 dan yang lebih tinggi) dan menggunakan Support Library versi terbaru.

Jika Anda sering melihat inflation yang menyebabkan jank saat item baru muncul di layar, pastikan Anda tidak memiliki lebih banyak jenis tampilan daripada yang dibutuhkan. Makin sedikit jenis tampilan di konten RecyclerView, makin sedikit inflation yang perlu dilakukan saat jenis item baru muncul di layar. Jika memungkinkan, gabungkan jenis tampilan – jika hanya ikon, warna, atau potongan teks yang berubah antar-jenis, Anda dapat membuat perubahan tersebut pada waktu binding, dan menghindari inflation (di saat bersamaan mengurangi jejak memori aplikasi).

Jika jenis tampilan terlihat bagus, usahakan untuk mengurangi biaya inflation. Mengurangi container dan Tampilan struktural yang tidak perlu dapat membantu – pertimbangkan untuk mem-build itemViews dengan ConstraintLayout, yang dapat memudahkan pengurangan Tampilan struktural. Jika Anda ingin benar-benar mengoptimalkan performa, hierarki item Anda sederhana, dan Anda tidak memerlukan fitur tema dan gaya yang kompleks, pertimbangkan untuk memanggil konstruktor sendiri – tetapi perlu diperhatikan bahwa sering kali tidak ada manfaatnya jika kesederhanaan dan fitur XML hilang.

RecyclerView: Binding memerlukan waktu terlalu lama

Binding (yaitu, onBindViewHolder(VH, int)) seharusnya sangat sederhana, dan memerlukan waktu kurang dari satu milidetik untuk semua item, kecuali item yang paling kompleks. Binding seharusnya mengambil item POJO dari data item internal adaptor, dan memanggil penyetel pada tampilan di ViewHolder. Jika RV OnBindView memerlukan waktu yang terlalu lama, pastikan Anda sedang 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 dengan menggunakan library Data Binding.

RecyclerView atau ListView: tata letak/menggambar memerlukan waktu terlalu lama

Untuk masalah pada menggambar dan tata letak, lihat bagian tentang Tata Letak dan Performa Rendering.

ListView: Inflation

Anda dapat dengan mudah menonaktifkan daur ulang secara tidak sengaja di ListView jika Anda 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 di bawah:

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 melakukan terlalu banyak pekerjaan, atau melakukan pekerjaan terlalu sering, itu artinya Anda mengalami masalah performa tata letak. Performa tata letak aplikasi bergantung pada porsi hierarki Tampilan yang memiliki parameter atau input tata letak.

Performa tata letak: Biaya

Jika segmen lebih lama daripada beberapa milidetik, ada kemungkinan bahwa Anda mengalami performa tingkatan terburuk untuk RelativeLayouts, atau weighted-LinearLayouts. Tiap tata letak ini dapat memicu beberapa penerusan ukuran/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. Ada beberapa cara untuk melakukannya:

  • Anda dapat mengatur ulang tampilan struktural.
  • Anda dapat 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, 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, yang dapat menyebabkan penurunan frame. Umumnya, animasi harus dijalankan di properti menggambar View (misalnya setTranslationX/Y/Z(), setRotation(), setAlpha(), dll…). Ini semua dapat diubah dengan jauh lebih murah daripada properti tata letak (seperti padding, atau margin). Juga jauh lebih murah untuk mengubah properti menggambar tampilan, umumnya 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, dan DrawFrame di RenderThread. Yang pertama menjalankan draw(Canvas) di setiap View yang tidak divalidasi, dan dapat menyebabkan panggilan ke dalam tampilan kustom atau ke dalam kode Anda. Yang kedua berjalan di RenderThread native, tetapi akan 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 Anda sebaiknya menghindarinya jika memungkinkan. Anda dapat menggunakan pelacakan metode dengan Android CPU Profiler untuk mengetahui apakah ini masalahnya.

Proses menggambar ke bitmap biasanya dilakukan saat aplikasi ingin mendekorasi bitmap sebelum menampilkannya. Kadang dekorasi seperti menambahkan sudut bundar:

Kotlin

val paint = Paint().apply {
    isAntiAlias = true
}
Canvas(roundedOutputBitmap).apply {
    // draw a round rect to define 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 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 ini, 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);
}

Perlu diperhatikan bahwa ini juga biasanya dapat dilakukan untuk perlindungan latar belakang (menggambar gradien di atas Bitmap), dan pemfilteran gambar (dengan ColorMatrixColorFilter), dua operasi umum lain yang dilakukan untuk mengubah bitmap.

Jika Anda menggambar ke bitmap karena alasan lain, mungkin menggunakannya sebagai cache, coba gambar ke Canvas yang diakselerasi oleh hardware, yang diteruskan ke Tampilan atau Drawable secara langsung, dan jika perlu, pertimbangkan untuk memanggil setLayerType() dengan LAYER_TYPE_HARDWARE guna menyimpan cache output rendering yang kompleks, dan tetap memanfaatkan rendering GPU.

Performa rendering: RenderThread

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

Canvas.saveLayer()

Hindari Canvas.saveLayer(), karena dapat memicu rendering yang mahal, tidak disimpan dalam cache, dan berada di luar layar pada setiap frame. Meskipun performa telah ditingkatkan di Android 6.0 (saat pengoptimalan dilakukan untuk menghindari peralihan target render di GPU), sebaiknya hindari API mahal ini jika memungkinkan, atau setidaknya pastikan Anda meneruskan Canvas.CLIP_TO_LAYER_SAVE_FLAG (atau memanggil varian yang tidak menggunakan tanda).

Menganimasi Jalur besar

Saat Canvas.drawPath() dipanggil di Canvas yang diakselerasi oleh hardware dan diteruskan ke Tampilan, Android menggambar jalur ini terlebih dahulu di CPU, dan 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 – lebih baik menggunakannya meskipun pada akhirnya 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 non-persegi panjang. Performanya lebih baik dan mendukung anti-aliasing. Misalnya, panggilan clipPath berikut:

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();

Dapat dinyatakan sebagai:

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 (lihat Gambar 2), tetapi perlu menampilkan gambar dengan GPU.

Jika tindakan ini memerlukan waktu lama, terlebih dahulu periksa jumlah lebar dan tinggi di pelacakan. Pastikan bahwa bitmap yang sedang ditampilkan tidak lebih besar secara signifikan daripada area di layar yang menampilkannya. Jika ya, maka ini menghabiskan waktu upload dan memori. Umumnya, library pemuatan Bitmap menyediakan sarana yang 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, sedangkan RenderThread tidak ada aktivitas. Ini dapat dilakukan 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.

Gambar 2: Aplikasi menghabiskan waktu yang signifikan dalam sebuah frame yang mengupload bitmap besar. Kurangi ukurannya atau picu lebih awal saat didekode dengan prepareToDraw().

Keterlambatan penjadwalan thread

Penjadwal thread adalah bagian dari sistem operasi Android yang bertanggung jawab untuk menentukan thread mana di sistem yang harus berjalan, kapan, dan berapa lama. Kadang, jank muncul karena UI Thread aplikasi diblokir atau tidak berjalan. Systrace menggunakan warna-warna yang berbeda (lihat gambar 3) untuk menandakan kapan thread Tidur (abu-abu), Dapat dijalankan (biru: dapat berjalan, tetapi penjadwal belum memilihnya untuk berjalan), Aktif berjalan (Hijau), atau dalam Tidur yang tidak dapat diganggu (Merah atau Oranye). Warna sangat berguna untuk mendebug masalah jank yang disebabkan oleh keterlambatan penjadwalan thread.

Gambar 3: menandai periode saat UI Thread tidur.

Biasanya, jeda lama dalam eksekusi aplikasi disebabkan oleh panggilan binder, yaitu mekanisme komunikasi antarproses (IPC) di Android. Di versi terbaru Android, ini adalah salah satu alasan paling umum UI Thread berhenti berjalan. Umumnya, perbaikannya adalah menghindari pemanggilan fungsi yang melakukan panggilan binder; jika tidak dapat dihindari, Anda harus menyimpan nilai di cache, atau memindahkan pekerjaan ke thread latar belakang. Dengan semakin besarnya codebase, sangat mudah untuk menambahkan panggilan binder secara tidak sengaja dengan memanggil beberapa metode tingkat rendah, jika Anda tidak berhati-hati – tetapi mudah juga untuk menemukan dan memperbaikinya dengan melakukan pelacakan.

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. Pelacakan secara berkala dapat membantu Anda menemukan dan memperbaiki dengan cepat begitu masalah tersebut muncul.

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

Jika Anda tidak melihat aktivitas binder, tetapi masih 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 runtime default di Android 5.0, tetapi masih mungkin untuk membebani thread dengan pekerjaan ekstra ini. Boleh saja mengalokasikan untuk merespons peristiwa langka yang tidak sering terjadi dalam satu detik (seperti pengguna mengklik tombol), tetapi perlu diingat bahwa tiap alokasi menimbulkan biaya. Jika alokasi berada dalam loop padat yang sering dipanggil, pertimbangkan untuk menghindari alokasi guna meringankan beban pada GC.

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

Gambar 5: menunjukkan 94 md GC di thread HeapTaskDaemon

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