Menjaga aplikasi Anda tetap responsif

Gambar 1. Dialog ANR yang ditampilkan kepada pengguna.

Anda mungkin saja menulis kode yang mampu memenangkan semua tes performa di dunia, tetapi masih bekerja dengan lamban, tidak responsif, membeku selama periode yang cukup panjang, atau memakan waktu yang terlalu lama untuk memproses input. Masalah terburuk yang dapat terjadi pada daya respons aplikasi Anda adalah dialog "Aplikasi Tidak Merespons" (ANR).

Di Android, sistem melindungi perangkat dari aplikasi yang tidak cukup responsif selama jangka waktu tertentu dengan menampilkan dialog yang memberitahukan bahwa aplikasi berhenti merespons, seperti dialog pada Gambar 1. Pada tahap ini, aplikasi Anda tidak responsif selama jangka waktu yang cukup lama, sehingga sistem menawarkan opsi kepada pengguna untuk keluar dari aplikasi. Perancangan daya respons sangat diperlukan bagi aplikasi Anda agar sistem tidak menampilkan dialog ANR kepada pengguna.

Dokumen ini menjelaskan cara sistem Android menentukan apakah aplikasi merespons atau tidak, dan memberikan panduan untuk memastikan aplikasi akan selalu responsif.

Apa yang memicu ANR?

Secara umum, sistem akan menampilkan ANR jika aplikasi tidak dapat merespons input pengguna. Misalnya, ketika aplikasi memblokir beberapa operasi I/O (sering kali berupa akses jaringan) di thread UI sehingga sistem tidak dapat memproses aktivitas input pengguna yang masuk. Contoh lainnya, aplikasi memakan waktu terlalu lama membangun struktur dalam memori yang rumit atau menghitung langkah game selanjutnya di thread UI. Penting untuk selalu memastikan penghitungan ini bekerja secara efisien, meskipun kode yang paling efisien tetap memerlukan waktu untuk dijalankan.

Di setiap situasi yang mengakibatkan aplikasi melakukan operasi yang mungkin akan berjalan lama, jangan lakukan operasi di thread UI. Sebagai gantinya, Anda dapat membuat thread worker dan melakukan sebagian operasi di sana. Tindakan ini akan menjaga thread UI (yang menggerakkan loop aktivitas antarmuka pengguna) tetap berjalan dan mencegah sistem menyimpulkan bahwa kode Anda telah dibekukan. Hal ini karena threading semacam itu biasanya dilakukan di tingkat kelas, dan Anda dapat menganggap daya respons sebagai masalah kelas. (Bandingkan hal ini dengan performa kode dasar yang merupakan masalah di tingkat metode.)

Di Android, daya respons aplikasi dipantau oleh layanan sistem Activity Manager dan Window Manager. Android akan menampilkan dialog ANR untuk aplikasi tertentu saat mendeteksi salah satu kondisi berikut:

  • Tidak ada respons terhadap aktivitas input (seperti aktivitas penekanan tombol atau sentuhan layar) dalam lima detik.
  • BroadcastReceiver belum selesai dieksekusi dalam sepuluh detik.

Cara menghindari ANR

Secara default, aplikasi Android biasanya berjalan sepenuhnya di satu thread, yaitu "thread UI" atau "thread utama"). Hal ini berarti semua operasi aplikasi Anda di thread UI yang memerlukan waktu penyelesaian yang lama dapat memicu dialog ANR karena aplikasi tidak memiliki kesempatan untuk menangani aktivitas input atau broadcast intent.

Oleh karena itu, setiap metode yang berjalan di thread UI harus melakukan operasi paling sedikit di thread tersebut. Khususnya, aktivitas harus diminimalkan untuk menyiapkan metode siklus proses utama, seperti onCreate() dan onResume(). Operasi yang dapat berjalan lama, seperti operasi jaringan atau database, maupun penghitungan rumit secara komputasi, seperti mengubah ukuran bitmap, harus dilakukan dalam thread worker (atau untuk operasi database, melalui permintaan asinkron).

Cara pembuatan thread worker yang paling efektif untuk operasi yang lebih lama adalah dengan kelas AsyncTask. Cukup perluas AsyncTask dan terapkan metode doInBackground() untuk menjalankan operasi. Untuk memposting perubahan progres kepada pengguna, Anda dapat memanggil publishProgress(), yang akan meminta metode callback onProgressUpdate(). Dari implementasi onProgressUpdate() yang berjalan di thread UI, Anda dapat memberi tahu pengguna. Contoh:

Kotlin

    private class DownloadFilesTask : AsyncTask<URL, Int, Long>() {

        // Do the long-running work in here
        override fun doInBackground(vararg urls: URL): Long? {
            val count: Float = urls.size.toFloat()
            var totalSize: Long = 0
            urls.forEachIndexed { index, url ->
                totalSize += Downloader.downloadFile(url)
                publishProgress((index / count * 100).toInt())
                // Escape early if cancel() is called
                if (isCancelled) return totalSize
            }
            return totalSize
        }

        // This is called each time you call publishProgress()
        override fun onProgressUpdate(vararg progress: Int?) {
            setProgressPercent(progress.firstOrNull() ?: 0)
        }

        // This is called when doInBackground() is finished
        override fun onPostExecute(result: Long?) {
            showNotification("Downloaded $result bytes")
        }
    }
    

Java

    private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
        // Do the long-running work in here
        protected Long doInBackground(URL... urls) {
            int count = urls.length;
            long totalSize = 0;
            for (int i = 0; i < count; i++) {
                totalSize += Downloader.downloadFile(urls[i]);
                publishProgress((int) ((i / (float) count) * 100));
                // Escape early if cancel() is called
                if (isCancelled()) break;
            }
            return totalSize;
        }

        // This is called each time you call publishProgress()
        protected void onProgressUpdate(Integer... progress) {
            setProgressPercent(progress[0]);
        }

        // This is called when doInBackground() is finished
        protected void onPostExecute(Long result) {
            showNotification("Downloaded " + result + " bytes");
        }
    }
    

Untuk mengeksekusi thread worker ini, cukup buat instance dan panggil execute():

Kotlin

    DownloadFilesTask().execute(url1, url2, url3)
    

Java

    new DownloadFilesTask().execute(url1, url2, url3);
    

Meskipun lebih rumit daripada AsyncTask, Anda mungkin ingin membuat Thread atau kelas HandlerThread sendiri. Dengan begitu, Anda harus menetapkan prioritas thread ke prioritas "background" dengan memanggil Process.setThreadPriority() dan meneruskan THREAD_PRIORITY_BACKGROUND. Jika thread tidak diatur ke prioritas yang lebih rendah dengan cara ini, thread masih dapat memperlambat aplikasi karena thread beroperasi dengan prioritas yang sama seperti thread UI secara default.

Jika Anda mengimplementasikan Thread atau HandlerThread, pastikan thread UI tidak memblokir selagi menunggu thread worker selesai—jangan panggil Thread.wait() atau Thread.sleep(). Daripada memblokir selagi menunggu thread worker selesai, thread utama Anda sebaiknya memberikan Handler ke thread lainnya untuk memposting kembali setelah selesai. Mendesain aplikasi Anda dengan cara ini akan memungkinkan thread UI aplikasi tetap responsif terhadap input, sehingga menghindari dialog ANR yang disebabkan oleh waktu tunggu aktivitas input selama 5 detik.

Batasan khusus pada waktu eksekusi BroadcastReceiver menekankan hal yang seharusnya dilakukan oleh penerima broadcast: operasi dalam jumlah kecil dan terpisah di background, seperti menyimpan setelan atau mendaftarkan Notification. Seperti halnya metode lain yang dipanggil di thread UI, aplikasi harus menghindari operasi atau penghitungan yang dapat berjalan lama di penerima broadcast. Namun, daripada melakukan tugas intensif melalui thread worker, aplikasi Anda harus memulai IntentService jika tindakan yang dapat berjalan lama perlu dilakukan sebagai respons terhadap broadcast intent.

Masalah umum lainnya dengan objek BroadcastReceiver terjadi jika objek tersebut terlalu sering dieksekusi. Eksekusi latar belakang yang sering dapat mengurangi jumlah memori yang tersedia untuk aplikasi lain. Untuk informasi lebih lanjut tentang cara mengaktifkan dan menonaktifkan objek BroadcastReceiver secara efisien, lihat Memanipulasi Penerima Broadcast sesuai Permintaan.

Tips: Anda dapat menggunakan StrictMode untuk membantu menemukan operasi yang dapat berjalan lama, seperti operasi jaringan atau database yang mungkin tidak sengaja dilakukan di thread utama Anda.

Memperkuat daya respons

Umumnya, 100 hingga 200 ms adalah nilai minimum yang akan membuat pengguna mulai mengalami kelambatan dalam suatu aplikasi. Oleh karena itu, berikut beberapa tips tambahan yang sebaiknya dilakukan untuk menghindari ANR dan membuat aplikasi Anda tampak responsif terhadap pengguna:

  • Jika aplikasi melakukan operasi di latar belakang sebagai respons terhadap input pengguna, tunjukkan bahwa progres sedang berlangsung (seperti dengan ProgressBar di UI Anda).
  • Khususnya untuk game, lakukan penghitungan gerakan dalam thread worker.
  • Jika aplikasi memiliki fase penyiapan awal yang menghabiskan waktu, cobalah untuk menampilkan layar pembuka atau merender tampilan utama secepat mungkin, lalu tunjukkan bahwa pemuatan sedang berlangsung dan isi informasi secara asinkron. Dalam hal ini, sebaiknya tunjukkan bahwa progres sedang berlangsung. Jangan sampai pengguna merasa bahwa aplikasi dibekukan.
  • Gunakan fitur performa, seperti Systrace dan Traceview untuk menentukan hambatan dalam daya respons aplikasi Anda.