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 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 mengganggu rendering yang lancar dan responsivitas yang cepat terhadap input pengguna, adalah alasan terbesar bagi Anda untuk menerapkan threading di aplikasi.

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 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 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) {...}
  }
}

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 untuk masalah ini adalah dengan menentukan instance class yang kelebihan beban 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 informasi selengkapnya tentang mengelola thread, lihat dokumentasi 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. Seiring bertambahnya beban kerja yang diberikan ke ThreadPoolExecutor, class ini akan memperhitungkan jumlah thread minimum dan maksimum yang diinisialisasi, dan mempertimbangkan jumlah pekerjaan tertunda yang harus dilakukan. Berdasarkan faktor-faktor ini, ThreadPoolExecutor akan memutuskan jumlah thread yang 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.