Performa yang lebih baik melalui threading

Memanfaatkan thread di Android secara pintar dapat membantu meningkatkan performa aplikasi Anda. Halaman ini membahas beberapa aspek menangani thread: menangani thread UI, atau thread utama; hubungan antara siklus proses aplikasi dengan prioritas thread; dan metode yang disediakan platform untuk membantu mengelola kerumitan thread. Untuk setiap area tersebut, artikel ini menjelaskan potensi masalah dan strategi untuk menghindarinya.

Thread utama

Saat pengguna meluncurkan aplikasi Anda, Android akan membuat proses Linux baru beserta thread eksekusi. Thread utama ini, yang disebut juga thread UI, menentukan apa yang terjadi di layar. Memahami cara kerjanya dapat membantu Anda merancang aplikasi agar menggunakan thread utama untuk memberikan performa terbaik.

Internal

Thread utama memiliki desain yang sangat sederhana: Satu-satunya tugas yang ditanganinya adalah mengambil dan menjalankan blok-blok kerja dari antrean tugas yang aman untuk thread hingga aplikasinya dihentikan. Framework ini menghasilkan beberapa blok kerja ini dari berbagai tempat. Tempat tersebut meliputi callback yang terkait dengan informasi siklus proses, peristiwa pengguna seperti input, atau peristiwa yang berasal dari aplikasi dan proses lain. Selain itu, aplikasi dapat secara eksplisit mengantrekan bloknya sendiri, tanpa menggunakan framework.

Hampir setiap blok kode yang dijalankan aplikasi Anda terikat dengan callback peristiwa, seperti input, perluasan tata letak, atau operasi menggambar. Saat sesuatu memicu peristiwa, thread tempat peristiwa itu terjadi akan mendorong peristiwa tersebut keluar, dan masuk ke antrean pesan thread utama. Selanjutnya, thread utama dapat melayani peristiwa tersebut.

Sewaktu animasi atau pembaruan layar berlangsung, sistem akan mencoba menjalankan sebuah blok pekerjaan (yang bertanggung jawab menggambar layar) setiap sekitar 16 milidetik, untuk merender gambar dengan lancar pada frekuensi 60 frame per detik. Agar sistem dapat mencapai sasaran ini, hierarki UI/View di thread utama harus diperbarui. Namun, jika antrean pesan di thread utama berisi tugas yang terlalu banyak atau terlalu panjang sehingga tidak dapat diperbarui oleh thread utama dengan cukup cepat, aplikasi harus memindahkan pekerjaan ini ke thread pekerja. Jika thread utama tidak dapat menyelesaikan eksekusi blok pekerjaan dalam 16 milidetik, pengguna mungkin melihat adanya sendatan, keterlambatan, atau kurangnya respons UI terhadap input. Jika thread utama terblokir selama sekitar lima detik, sistem akan menampilkan dialog Aplikasi Tidak Merespons (ANR), sehingga pengguna dapat menutup aplikasi secara langsung.

Memindahkan tugas yang banyak atau panjang dari thread utama, agar tidak menghambat rendering atau responsivitas terhadap input pengguna, adalah alasan terbesar untuk menerapkan threading dalam aplikasi Anda.

Referensi objek Thread dan UI

Secara desain, objek Android View tidak aman bagi thread. Aplikasi diharapkan membuat, menggunakan, dan menghancurkan objek UI, semuanya di thread utama. Jika Anda mencoba mengubah atau bahkan mereferensikan objek UI dalam thread selain thread utama, hasilnya dapat berupa pengecualian, kegagalan senyap, error, dan perilaku lain yang tidak dapat ditentukan.

Masalah terkait referensi dibagi menjadi dua kategori: referensi eksplisit dan referensi implisit.

Referensi eksplisit

Banyak tugas di thread non-utama yang memiliki tujuan akhir memperbarui objek UI. Namun, jika salah satu thread ini mengakses objek dalam hierarki view, ketidakstabilan aplikasi dapat terjadi: Jika thread pekerja mengubah properti objek pada saat yang sama ketika thread lain mereferensikan objek tersebut, hasilnya tidak dapat ditentukan.

Misalnya, bayangkan sebuah aplikasi yang memiliki referensi langsung ke sebuah objek UI di thread pekerja. Objek di thread pekerja dapat berisi referensi ke View; tetapi sebelum pekerjaan itu selesai, View akan dihapus dari hierarki view. Jika kedua tindakan ini terjadi secara bersamaan, referensi akan mempertahankan objek View di memori dan menetapkan properti di dalamnya. Namun, pengguna tidak akan melihat objek ini, dan aplikasi akan menghapusnya setelah referensi ke objek tersebut hilang.

Dalam contoh lain, objek View berisi referensi ke aktivitas yang memiliki objek tersebut. Jika aktivitas itu dihancurkan, tetapi masih ada blok thread yang mereferensikannya—langsung maupun tidak langsung—pembersih sampah memori tidak akan mengambil aktivitas itu sampai blok pekerjaan tersebut selesai dijalankan.

Skenario ini dapat menyebabkan masalah dalam situasi ketika pekerjaan thread mungkin sedang berlangsung sementara peristiwa siklus proses aktivitas lainnya, seperti rotasi layar, terjadi. Sistem tidak akan dapat menjalankan pembersihan sampah memori sampai pekerjaan yang sedang berlangsung tersebut selesai. Akibatnya, mungkin akan ada dua objek Activity di memori sampai pembersihan sampah dapat dilakukan.

Dengan skenario seperti ini, sebaiknya aplikasi Anda tidak menyertakan referensi eksplisit ke objek UI dalam tugas kerja thread. Menghindari referensi semacam itu akan membantu Anda menghindari jenis kebocoran memori ini, sekaligus menghindari thread yang berebut memori.

Apa pun kasusnya, sebaiknya aplikasi Anda hanya memperbarui objek UI di thread utama. Ini berarti Anda harus membuat kebijakan negosiasi yang memungkinkan beberapa thread untuk mengomunikasikan kembali pekerjaan ke thread utama, yang menggerakkan aktivitas atau fragmen teratas dengan tugas memperbarui objek UI yang sebenarnya.

Referensi implisit

Cacat kode desain yang umum pada objek thread dapat dilihat dalam cuplikan kode di bawah:

Kotlin

    class MainActivity : Activity() {
        // ...
        inner class MyAsyncTask : AsyncTask<Unit, Unit, String>() {
            override fun doInBackground(vararg params: Unit): String {...}
            override fun onPostExecute(result: String) {...}
        }
    }

Java

    public class MainActivity extends Activity {
      // ...
      public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
        @Override protected String doInBackground(Void... params) {...}
        @Override protected void onPostExecute(String result) {...}
      }
    }
    

Cacat dalam cuplikan ini adalah bahwa kode di atas mendeklarasikan objek threading MyAsyncTask sebagai inner class non-statis untuk beberapa aktivitas (atau inner class di Kotlin). Deklarasi ini menghasilkan referensi implisit ke instance Activity yang mencakupnya. Akibatnya, objek ini berisi referensi ke aktivitas hingga pekerjaan thread selesai, yang menyebabkan keterlambatan dalam penghancuran aktivitas yang direferensikan. Pada gilirannya, keterlambatan ini memberikan lebih banyak tekanan pada memori.

Solusi langsung atas masalah ini adalah dengan menentukan instance class overload sebagai class statis, atau dalam filenya sendiri, sehingga menghapus referensi implisit.

Solusi lainnya adalah dengan mendeklarasikan objek AsyncTask sebagai class statis bertingkat (atau menghapus inner qualifier di Kotlin). Cara ini menghilangkan masalah referensi implisit karena adanya perbedaan antara class statis bertingkat dengan inner class: Sebuah instance inner class memerlukan dibuatnya instance outer class, serta memiliki akses langsung ke metode dan kolom-kolom dalam instance yang mencakupnya. Sebaliknya, class statis bertingkat tidak memerlukan referensi ke instance class pencakup, sehingga tidak berisi referensi ke anggota outer class.

Kotlin

    class MainActivity : Activity() {
        // ...
        class MyAsyncTask : AsyncTask<Unit, Unit, String>() {
            override fun doInBackground(vararg params: Unit): String {...}
            override fun onPostExecute(result: String) {...}
        }
    }
    

Java

    public class MainActivity extends Activity {
      // ...
      static public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
        @Override protected String doInBackground(Void... params) {...}
        @Override protected void onPostExecute(String result) {...}
      }
    }
    

Thread dan siklus proses aktivitas aplikasi

Siklus proses aplikasi dapat memengaruhi cara kerja threading dalam aplikasi Anda. Anda mungkin perlu menentukan bahwa sebuah thread harus dipertahankan, atau tidak dipertahankan, setelah aktivitas dihancurkan. Anda juga perlu memahami hubungan antara prioritas thread dan apakah aktivitas berjalan di latar depan atau latar belakang.

Thread yang bertahan

Thread akan bertahan selama aktivitas yang melahirkannya masih ada. Thread terus dijalankan, tanpa gangguan, terlepas dari apakah aktivitas dibuat atau dihancurkan. Dalam beberapa kasus, persistensi ini baik.

Pertimbangkan kasus di mana sebuah aktivitas melahirkan sekumpulan blok pekerjaan thread, lalu aktivitas itu dihancurkan sebelum thread pekerja dapat mengeksekusi blok tersebut. Apa yang harus dilakukan aplikasi untuk blok yang sedang berjalan?

Jika blok tersebut akan memperbarui UI yang sudah tidak ada, tidak ada alasan untuk melanjutkan pekerjaan. Misalnya, jika pekerjaan itu adalah memuat informasi pengguna dari database, lalu memperbarui tampilan, maka thread tidak lagi diperlukan.

Sebaliknya, paket pekerjaan mungkin memiliki manfaat yang tidak sepenuhnya terkait dengan UI. Dalam hal ini, Anda perlu mempertahankan thread. Misalnya, paket mungkin menunggu untuk mendownload gambar, meng-cache gambar itu ke disk, dan memperbarui objek View yang terkait. Meskipun objek itu tidak ada lagi, tindakan mendownload dan menyimpan gambar ke cache mungkin tetap berguna, jika pengguna kembali ke aktivitas yang dihancurkan.

Mengelola respons siklus proses secara manual untuk semua objek threading dapat menjadi pekerjaan yang sangat kompleks. Jika Anda tidak mengelolanya dengan benar, aplikasi Anda dapat mengalami masalah perebutan dan performa memori. Dengan menggabungkan ViewModel dan LiveData, Anda akan dapat memuat data dan menerima notifikasi saat data berubah tanpa perlu mengkhawatirkan siklus proses. Objek ViewModel adalah salah satu solusi untuk masalah ini. ViewModel dipertahankan di seluruh perubahan konfigurasi sehingga memberikan cara mudah untuk mempertahankan data tampilan Anda. Untuk informasi lebih lanjut tentang ViewModel, lihat panduan ViewModel, dan untuk mempelajari LiveData lebih lanjut, lihat panduan LiveData. Jika Anda juga ingin mendapatkan informasi lebih lanjut tentang arsitektur aplikasi, baca Panduan Arsitektur Aplikasi.

Prioritas thread

Seperti yang dijelaskan dalam Proses dan Siklus Proses Aplikasi, prioritas yang diterima thread aplikasi Anda dipengaruhi sebagian oleh lokasi aplikasi dalam siklus proses aplikasi. Saat Anda membuat dan mengelola thread dalam aplikasi, penting kiranya untuk menetapkan prioritas agar thread yang tepat mendapatkan prioritas yang tepat pada waktu yang tepat. Jika ditetapkan terlalu tinggi, thread Anda dapat mengganggu thread UI dan RenderThread, yang menyebabkan aplikasi Anda kehilangan frame. Jika ditetapkan terlalu rendah, Anda dapat memperlambat tugas-tugas asinkron (seperti pemuatan gambar).

Setiap kali membuat thread, Anda harus memanggil setThreadPriority(). Penjadwal thread sistem memberikan preferensi kepada thread dengan prioritas tinggi, sehingga menyeimbangkan prioritas tersebut dengan kebutuhan untuk menyelesaikan semua pekerjaan. Umumnya, thread di grup latar depan mendapatkan sekitar 95% total waktu eksekusi dari perangkat, sementara grup latar belakang mendapat sekitar 5%.

Sistem juga menetapkan nilai prioritasnya sendiri untuk setiap thread, menggunakan class Process.

Secara default, sistem menetapkan prioritas thread ke prioritas dan keanggotaan grup yang sama dengan thread yang melahirkannya. Namun, aplikasi Anda dapat menyesuaikan prioritas thread secara eksplisit menggunakan setThreadPriority().

Class Process membantu mengurangi kerumitan dalam menetapkan nilai prioritas dengan menyediakan sekumpulan konstanta yang dapat digunakan aplikasi Anda untuk menetapkan prioritas thread. Misalnya, THREAD_PRIORITY_DEFAULT mewakili nilai default untuk sebuah thread. Aplikasi Anda harus menetapkan prioritas thread ke THREAD_PRIORITY_BACKGROUND untuk thread yang menjalankan pekerjaan yang tidak terlalu mendesak.

Aplikasi Anda dapat menggunakan konstanta THREAD_PRIORITY_LESS_FAVORABLE dan THREAD_PRIORITY_MORE_FAVORABLE sebagai incrementer untuk menetapkan prioritas relatif. Untuk daftar prioritas thread, lihat konstanta THREAD_PRIORITY di class Process.

Untuk cara mengelola thread lebih lanjut, lihat dokumentasi referensi tentang class Thread dan Process.

Class helper untuk threading

Framework ini menyediakan class dan primitive Java yang sama untuk memudahkan threading, seperti class Thread, Runnable, dan Executors. Untuk membantu mengurangi beban kognitif yang terkait dengan pengembangan aplikasi threaded untuk Android, framework ini menyediakan seperangkat helper yang dapat memudahkan pengembangan, seperti AsyncTaskLoader dan AsyncTask. Setiap class helper memiliki seperangkat nuansa performa tertentu yang menjadikannya unik untuk masalah threading tertentu. Penggunaan class yang salah untuk situasi yang salah dapat menyebabkan masalah performa.

Class AsyncTask

Class AsyncTask adalah primitive sederhana tapi berguna untuk aplikasi yang perlu memindahkan pekerjaan dari thread utama ke thread pekerja dengan cepat. Misalnya, peristiwa input dapat memicu kebutuhan untuk memperbarui UI di bitmap yang telah dimuat. Objek AsyncTask dapat meng-offload pemuatan dan dekoding bitmap ke thread alternatif. Setelah proses ini selesai, objek AsyncTask dapat mengelola penerimaan kembali pekerjaan di thread utama untuk memperbarui UI.

Saat menggunakan AsyncTask, ada beberapa aspek performa penting yang perlu diperhatikan. Pertama, secara default, aplikasi mengirim semua objek AsyncTask yang dibuatnya ke thread tunggal. Oleh karena itu, objek ini berjalan secara serial, dan—seperti pada thread utama—paket pekerjaan yang sangat panjang dapat memblokir antrean. Karena alasan ini, sebaiknya Anda hanya menggunakan AsyncTask untuk menangani item pekerjaan dengan durasi kurang dari 5 milidetik.

Objek AsyncTask juga merupakan penghambat paling umum untuk masalah referensi implisit. Objek AsyncTask memunculkan risiko yang terkait dengan referensi eksplisit, tetapi terkadang masalah ini mudah diatasi. Misalnya, AsyncTask mungkin memerlukan referensi ke objek UI untuk mengupdate objek UI dengan benar setelah AsyncTask menjalankan callback-nya di thread utama. Dalam situasi seperti ini, Anda dapat menggunakan WeakReference untuk menyimpan referensi ke objek UI yang diperlukan, dan mengakses objek itu setelah AsyncTask beroperasi di thread utama. Untuk lebih jelasnya, menyimpan WeakReference ke sebuah objek tidak menjadikan objek tersebut aman untuk thread; WeakReference hanya menyediakan metode untuk menangani masalah terkait referensi eksplisit dan pembersihan sampah memori.

Class HandlerThread

Meskipun berguna, AsyncTask tidak selalu merupakan solusi tepat untuk masalah threading Anda. Sebagai gantinya, Anda mungkin memerlukan pendekatan yang lebih tradisional untuk menjalankan blok pekerjaan di thread yang berdurasi lebih lama, serta kemampuan untuk mengelola alur kerja tersebut secara manual.

Pertimbangkan tantangan umum saat mendapatkan frame pratinjau dari objek Camera. Saat mendaftar untuk frame pratinjau Kamera, Anda menerima frame tersebut dalam callback onPreviewFrame(), yang dipicu di thread peristiwa tempat panggilan berasal. Jika callback ini dipicu di thread UI, tugas untuk menangani array piksel yang sangat besar akan mengganggu proses rendering dan pemrosesan peristiwa. Masalah yang sama berlaku pada AsyncTask, yang juga menjalankan tugas secara serial dan rentan terhadap pemblokiran.

Dalam situasi inilah thread handler akan sesuai: Thread handler adalah thread berdurasi panjang yang mengambil pekerjaan dari antrean, dan beroperasi di antrean itu. Dalam contoh ini, saat aplikasi Anda mendelegasikan perintah Camera.open() ke blok pekerjaan di thread handler, callback onPreviewFrame() yang terkait akan mendarat di thread handler, bukan di thread UI atau AsyncTask. Jadi, jika Anda ingin melakukan pekerjaan berdurasi panjang untuk menangani piksel, solusi ini mungkin tepat bagi Anda.

Saat aplikasi Anda membuat thread menggunakan HandlerThread, jangan lupa untuk menetapkan prioritas thread berdasarkan jenis pekerjaan yang dilakukannya. Ingat, CPU hanya dapat menangani sejumlah kecil thread secara paralel. Menetapkan prioritas membantu sistem mengetahui cara tepat untuk menjadwalkan pekerjaan ini saat semua thread lain berebut perhatian.

Class ThreadPoolExecutor

Ada beberapa jenis pekerjaan yang dapat direduksi menjadi tugas terdistribusi yang sangat paralel. Salah satu tugas tersebut, misalnya, menghitung filter untuk setiap blok 8x8 dari sebuah gambar berukuran 8 megapiksel. Dengan banyaknya paket pekerjaan yang dihasilkan, AsyncTask dan HandlerThread bukanlah class yang tepat. Sifat thread tunggal dari AsyncTask akan mengubah semua pekerjaan yang terkumpul di thread menjadi sistem linear. Di sisi lain, penggunaan class HandlerThread mengharuskan programmer untuk mengelola load balancing antara sekumpulan thread.

ThreadPoolExecutor adalah class helper untuk mempermudah proses ini. Class ini mengelola pembuatan sekumpulan thread, menetapkan prioritasnya, dan mengelola pendistribusian pekerjaan di antara thread tersebut. Saat beban kerja meningkat atau menurun, class berproses atau menghancurkan lebih banyak thread agar selaras dengan beban kerja.

Class ini juga membantu aplikasi Anda menghasilkan jumlah thread yang optimal. Saat membuat objek ThreadPoolExecutor, aplikasi akan menetapkan jumlah minimum dan maksimum thread. Seiring peningkatan beban kerja yang diberikan ke ThreadPoolExecutor, class ini akan memperhitungkan jumlah thread minimum dan maksimum yang diinisialisasi, dan mempertimbangkan banyaknya pekerjaan tertunda yang harus dilakukan. Berdasarkan faktor-faktor ini, ThreadPoolExecutor menentukan banyaknya thread yang harus aktif pada suatu waktu tertentu.

Berapa banyak thread yang sebaiknya Anda buat?

Meskipun dari segi software, kode Anda mampu membuat ratusan thread, melakukan hal itu dapat menimbulkan masalah performa. Aplikasi Anda berbagi resource CPU yang terbatas dengan layanan latar belakang, perender, mesin audio, jaringan, dan sebagainya. CPU hanya mampu menangani sejumlah kecil thread secara paralel; jika jumlah itu terlampaui, masalah prioritas dan penjadwalan dapat terjadi. Karena itu, sebaiknya buatlah thread hanya sebanyak yang diperlukan beban kerja Anda.

Dari sudut pandang praktis, ada sejumlah variabel yang memengaruhi hal ini, tetapi menetapkan sebuah nilai (misalnya 4, sebagai permulaan), dan mengujinya dengan Systrace sama solidnya dengan strategi lainnya. Anda dapat menggunakan trial-and-error untuk mengetahui jumlah minimum thread yang dapat Anda gunakan tanpa mengalami masalah.

Pertimbangan lain dalam memutuskan banyaknya thread yang sebaiknya dimiliki adalah bahwa thread tidaklah bebas, melainkan memerlukan memori. Setiap thread memerlukan setidaknya 64k memori. Angka ini meningkat dengan cepat di banyak aplikasi yang terinstal di perangkat, terutama dalam situasi di mana stack panggilan meningkat signifikan.

Ada banyak proses sistem dan library pihak ketiga yang sering menangani sendiri kumpulan thread-nya. Jika aplikasi Anda dapat menggunakan kembali kumpulan thread yang sudah ada, penggunaan ulang ini dapat meningkatkan performa dengan mengurangi perebutan memori dan resource pemrosesan.