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 UI thread, 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 mengeksekusi blok-blok pekerjaan dari antrean tugas yang aman untuk thread hingga aplikasinya dihentikan. Framework ini menghasilkan beberapa blok pekerjaan 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 dieksekusi 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/Tampilan 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 Tampilan Android tidak aman untuk diakses dari beberapa 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 tampilan, 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 pada thread pekerja dapat berisi referensi ke sebuah
View
; tetapi sebelum pekerjaan selesai, View
akan
dihapus dari hierarki tampilan. 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 ada dua objek Activity
di memori sampai pembersihan sampah memori 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 mengomunikasikan kembali pekerjaan ke thread utama, yang menggerakkan aktivitas atau fragmen teratas dengan tugas memperbarui objek UI yang sebenarnya.
Referensi implisit
Cacat desain kode 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) {...} } }
Kecacatan 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.
Keterlambatan ini kemudian 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 selalu membatalkan dan membersihkan tugas latar belakang dalam callback siklus proses
Activity
yang sesuai, seperti onDestroy
. Namun, pendekatan ini dapat merepotkan dan rentan error. Sebagai aturan umum, Anda tidak boleh menempatkan logika non-UI yang kompleks
secara langsung dalam aktivitas. Selain itu, AsyncTask
kini tidak digunakan lagi dan tidak
direkomendasikan untuk digunakan dalam kode baru. Lihat Threading di Android
untuk detail selengkapnya tentang primitif serentak yang tersedia untuk Anda.
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 tetap berjalan, tanpa gangguan, terlepas dari pembuatan atau penghancuran aktivitas, meskipun akan dihentikan bersama dengan proses aplikasi setelah tidak ada komponen aplikasi yang aktif lagi. Dalam beberapa kasus, persistensi ini baik.
Pertimbangkan kasus saat 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 mengupdate UI yang sudah tidak ada, tidak ada alasan untuk melanjutkan pekerjaan. Misalnya, jika pekerjaan itu adalah memuat informasi pengguna dari database, lalu memperbarui tampilan, thread tidak lagi diperlukan.
Sebaliknya, paket pekerjaan mungkin memiliki beberapa manfaat yang tidak sepenuhnya terkait dengan
UI. Dalam kasus 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
dengan LiveData
, Anda dapat memuat data dan menerima notifikasi saat data berubah tanpa perlu mengkhawatirkan siklus proses.
Objek ViewModel
adalah satu solusi untuk masalah ini. ViewModel dipertahankan di seluruh perubahan konfigurasi sehingga memberikan cara mudah untuk mempertahankan data tampilan Anda. Untuk mengetahui informasi selengkapnya tentang ViewModel, lihat panduan ViewModel, dan untuk mempelajari LiveData lebih lanjut, lihat panduan LiveData. Jika Anda juga ingin mendapatkan informasi selengkapnya 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 mendapatkan 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 menghasilkannya. 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 mengetahui informasi selengkapnya tentang mengelola thread, lihat dokumen referensi tentang class Thread
dan Process
.
Class helper untuk threading
Untuk developer yang menggunakan Kotlin sebagai bahasa utama mereka, sebaiknya gunakan coroutine. Coroutine memberikan sejumlah manfaat, termasuk menulis kode asinkron tanpa callback serta konkurensi terstruktur untuk pencakupan, pembatalan, dan penanganan error.
Framework ini juga menyediakan class dan primitif Java yang sama untuk memfasilitasi threading, seperti class Thread
, Runnable
, dan Executors
, serta class tambahan seperti HandlerThread
.
Untuk mengetahui informasi selengkapnya, lihat Threading di Android.
Class HandlerThread
Thread pengendali sebenarnya adalah thread yang berjalan lama yang mengambil pekerjaan dari antrean dan beroperasi di dalamnya.
Pertimbangkan tantangan umum saat mendapatkan frame pratinjau dari objek Camera
Anda.
Saat mendaftar untuk frame pratinjau Kamera, Anda menerima frame tersebut dalam callback onPreviewFrame()
, yang dipanggil di thread peristiwa tempat panggilan berasal. Jika callback ini dipanggil di UI thread, tugas untuk menangani array piksel yang sangat besar akan mengganggu proses rendering dan pemrosesan peristiwa.
Dalam contoh ini, saat aplikasi Anda mendelegasikan perintah Camera.open()
ke blok pekerjaan di thread pengendali, callback onPreviewFrame()
yang terkait akan diterima di thread pengendali, bukan di UI thread. 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 dibuat olehnya, HandlerThread
bukanlah class yang tepat untuk digunakan.
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. Karena beban kerja yang diberikan kepada
ThreadPoolExecutor
meningkat,
class akan mengambil jumlah thread minimum dan maksimum yang diinisialisasi
dan mempertimbangkan jumlah pekerjaan
yang tertunda yang harus dilakukan. Berdasarkan hal ini
faktor, ThreadPoolExecutor
memutuskan berapa banyak
utas harus aktif
pada waktu tertentu.
Berapa banyak thread yang sebaiknya Anda buat?
Meskipun pada tingkat 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 sebanyak yang diperlukan oleh beban kerja Anda saja.
Dari sisi kepraktisan, ada sejumlah variabel yang memengaruhi hal ini, tetapi menetapkan sebuah nilai (misalnya 4, sebagai permulaan), dan mengujinya dengan Systrace sama konkretnya 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 saat 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.