Panduan API asinkron dan non-pemblokiran Android

API non-pemblokiran meminta pekerjaan untuk dilakukan, lalu memberikan kontrol kembali ke thread panggilan sehingga dapat melakukan pekerjaan lain sebelum menyelesaikan operasi yang diminta. API ini berguna untuk kasus saat pekerjaan yang diminta mungkin sedang berlangsung atau mungkin memerlukan penantian hingga I/O atau IPC selesai, ketersediaan resource sistem yang sangat diperebutkan, atau input pengguna sebelum pekerjaan dapat dilanjutkan. API yang dirancang dengan baik menyediakan cara untuk membatalkan operasi yang sedang berlangsung dan menghentikan pekerjaan agar tidak dilakukan atas nama pemanggil asli, sehingga menjaga kesehatan sistem dan masa pakai baterai saat operasi tidak lagi diperlukan.

API asinkron adalah salah satu cara untuk mencapai perilaku non-pemblokiran. API asinkron menerima beberapa bentuk kelanjutan atau callback yang diberi tahu saat operasi selesai, atau peristiwa lain selama progres operasi.

Ada dua motivasi utama untuk menulis API asinkron:

  • Menjalankan beberapa operasi secara serentak, dengan operasi ke-N harus dimulai sebelum operasi ke-N-1 selesai.
  • Menghindari pemblokiran thread panggilan hingga operasi selesai.

Kotlin sangat mendukung konkurensi terstruktur, serangkaian prinsip dan API yang dibuat berdasarkan fungsi penangguhan yang memisahkan eksekusi kode sinkron dan asinkron dari perilaku pemblokiran thread. Fungsi penangguhan bersifat non-pemblokiran dan sinkron.

Fungsi penangguhan:

  • Jangan memblokir thread panggilannya, tetapi hasilkan thread eksekusinya sebagai detail implementasi sambil menunggu hasil operasi yang dijalankan di tempat lain.
  • Jalankan secara sinkron dan tidak mengharuskan pemanggil API non-blocking untuk melanjutkan eksekusi secara serentak dengan tugas non-blocking yang dimulai oleh panggilan API.

Halaman ini menjelaskan dasar minimum ekspektasi yang dapat dipegang developer dengan aman saat menggunakan API asinkron dan non-blocking, diikuti dengan serangkaian resep untuk menulis API yang memenuhi ekspektasi ini dalam bahasa Kotlin atau Java, di platform Android atau library Jetpack. Jika ragu, pertimbangkan ekspektasi developer sebagai persyaratan untuk platform API baru.

Ekspektasi developer untuk API asinkron

Perkiraan berikut ditulis dari sudut pandang API yang tidak ditangguhkan kecuali jika dinyatakan lain.

API yang menerima callback biasanya asinkron

Jika API menerima callback yang tidak didokumentasikan untuk hanya dipanggil di tempat, (yaitu, hanya dipanggil oleh thread panggilan sebelum panggilan API itself ditampilkan,) API diasumsikan asinkron dan API tersebut harus memenuhi semua ekspektasi lainnya yang didokumentasikan di bagian berikut.

Contoh callback yang hanya pernah dipanggil di tempat adalah fungsi peta atau filter tingkat tinggi yang memanggil pemetaan atau predikat pada setiap item dalam koleksi sebelum ditampilkan.

API asinkron harus ditampilkan secepat mungkin

Developer mengharapkan API asinkron bersifat non-blocking dan ditampilkan dengan cepat setelah memulai permintaan untuk operasi. Panggilan API asinkron harus selalu aman kapan saja, dan memanggil API asinkron tidak boleh menghasilkan frame yang janky atau ANR.

Banyak operasi dan sinyal siklus proses dapat dipicu oleh platform atau library on-demand, dan mengharapkan developer memiliki pengetahuan global tentang semua situs panggilan potensial untuk kode mereka tidak dapat dipertahankan. Misalnya, Fragment dapat ditambahkan ke FragmentManager dalam transaksi sinkron sebagai respons terhadap pengukuran dan tata letak View saat konten aplikasi harus diisi untuk mengisi ruang yang tersedia (seperti RecyclerView). LifecycleObserver yang merespons callback siklus proses onStart fragmen ini dapat melakukan operasi startup satu kali di sini, dan ini mungkin berada di jalur kode kritis untuk menghasilkan frame animasi yang bebas jank. Developer harus selalu yakin bahwa memanggil setiap API asinkron sebagai respons terhadap jenis callback siklus proses ini tidak akan menyebabkan frame yang lambat.

Hal ini menyiratkan bahwa pekerjaan yang dilakukan oleh API asinkron sebelum ditampilkan harus sangat ringan; membuat catatan permintaan dan callback terkait, serta mendaftarkannya ke mesin eksekusi yang paling banyak melakukan pekerjaan. Jika pendaftaran untuk operasi asinkron memerlukan IPC, implementasi API harus mengambil tindakan apa pun yang diperlukan untuk memenuhi ekspektasi developer ini. Hal ini dapat mencakup satu atau beberapa hal berikut:

  • Mengimplementasikan IPC yang mendasarinya sebagai panggilan binder satu arah
  • Melakukan panggilan binder dua arah ke server sistem tempat menyelesaikan pendaftaran tidak memerlukan kunci yang sangat diperebutkan
  • Memposting permintaan ke thread pekerja dalam proses aplikasi untuk melakukan pendaftaran pemblokiran melalui IPC

API asinkron harus menampilkan void dan hanya menampilkan argumen yang tidak valid

Async API harus melaporkan semua hasil operasi yang diminta ke callback yang disediakan. Hal ini memungkinkan developer menerapkan satu jalur kode untuk penanganan error dan keberhasilan.

API asinkron dapat memeriksa argumen untuk null dan menampilkan NullPointerException, atau memeriksa apakah argumen yang diberikan berada dalam rentang yang valid dan menampilkan IllegalArgumentException. Misalnya, untuk fungsi yang menerima float dalam rentang 0 hingga 1f, fungsi tersebut dapat memeriksa apakah parameter berada dalam rentang ini dan menampilkan IllegalArgumentException jika berada di luar rentang, atau String singkat dapat diperiksa kesesuaiannya dengan format yang valid seperti alfanumerik saja. (Ingat bahwa server sistem tidak boleh memercayai proses aplikasi. Setiap layanan sistem harus menduplikasi pemeriksaan ini di layanan sistem itu sendiri.)

Semua error lainnya harus dilaporkan ke callback yang disediakan. Hal ini mencakup, tetapi tidak terbatas pada:

  • Kegagalan terminal operasi yang diminta
  • Pengecualian keamanan untuk otorisasi atau izin yang tidak ada dan diperlukan untuk menyelesaikan operasi
  • Melebihi kuota untuk melakukan operasi
  • Proses aplikasi tidak cukup "latar depan" untuk melakukan operasi
  • Hardware yang diperlukan telah terputus
  • Kegagalan jaringan
  • Waktu tunggu
  • Penghentian binder atau proses jarak jauh tidak tersedia

API asinkron harus menyediakan mekanisme pembatalan

API asinkron harus menyediakan cara untuk menunjukkan kepada operasi yang sedang berjalan bahwa pemanggil tidak lagi peduli dengan hasilnya. Operasi pembatalan ini akan menandakan dua hal:

Referensi hard ke callback yang disediakan oleh pemanggil harus dirilis

Callback yang disediakan ke API asinkron dapat berisi referensi hard ke grafik objek besar, dan pekerjaan yang sedang berlangsung yang menyimpan referensi hard ke callback tersebut dapat mencegah grafik objek tersebut di-garbage collection. Dengan merilis referensi callback ini saat pembatalan, grafik objek ini dapat memenuhi syarat untuk pengumpulan sampah jauh lebih cepat daripada jika pekerjaan diizinkan untuk berjalan hingga selesai.

Mesin eksekusi yang melakukan pekerjaan untuk pemanggil dapat menghentikan pekerjaan tersebut

Pekerjaan yang dimulai oleh panggilan API asinkron dapat menimbulkan biaya tinggi dalam penggunaan daya atau resource sistem lainnya. API yang memungkinkan pemanggil untuk memberikan sinyal saat pekerjaan ini tidak lagi diperlukan mengizinkan penghentian pekerjaan tersebut sebelum dapat menggunakan resource sistem lebih lanjut.

Pertimbangan khusus untuk aplikasi yang di-cache atau dibekukan

Saat mendesain API asinkron yang callback-nya berasal dari proses sistem dan dikirim ke aplikasi, pertimbangkan hal berikut:

  1. Proses dan siklus proses aplikasi: proses aplikasi penerima mungkin dalam status di-cache.
  2. Pembeku aplikasi dalam cache: proses aplikasi penerima mungkin dibekukan.

Saat proses aplikasi memasuki status cache, artinya proses tersebut tidak secara aktif menghosting komponen yang terlihat oleh pengguna seperti aktivitas dan layanan. Aplikasi disimpan dalam memori jika dapat dilihat pengguna lagi, tetapi sementara itu tidak boleh melakukan pekerjaan. Dalam sebagian besar kasus, Anda harus menjeda pengiriman callback aplikasi saat aplikasi tersebut memasuki status cache dan melanjutkan saat aplikasi keluar dari status cache, agar tidak memicu pekerjaan dalam proses aplikasi yang di-cache.

Aplikasi yang di-cache juga dapat dibekukan. Saat dibekukan, aplikasi tidak menerima waktu CPU dan tidak dapat melakukan pekerjaan apa pun. Setiap panggilan ke callback terdaftar aplikasi tersebut akan di-buffer dan dikirim saat aplikasi dicairkan.

Transaksi yang di-buffer ke callback aplikasi mungkin sudah tidak berlaku pada saat aplikasi di-unfreeze dan memprosesnya. Buffer bersifat terbatas, dan jika kelebihan kapasitas akan menyebabkan aplikasi penerima error. Untuk menghindari aplikasi yang kewalahan dengan peristiwa yang sudah tidak berlaku atau melebihi buffer-nya, jangan kirim callback aplikasi saat prosesnya dibekukan.

Sedang ditinjau:

  • Anda harus mempertimbangkan menjeda callback aplikasi pengiriman saat proses aplikasi di-cache.
  • Anda HARUS menjeda pengiriman callback aplikasi saat proses aplikasi dibekukan.

Pelacakan status

Untuk melacak kapan aplikasi memasuki atau keluar dari status yang di-cache:

mActivityManager.addOnUidImportanceListener(
    new UidImportanceListener() { ... },
    IMPORTANCE_CACHED);

Untuk melacak kapan aplikasi dibekukan atau dicairkan:

IBinder binder = <...>;
binder.addFrozenStateChangeCallback(executor, callback);

Strategi untuk melanjutkan callback aplikasi pengiriman

Baik Anda menjeda pengiriman callback aplikasi saat aplikasi memasuki status cache atau status beku, saat aplikasi keluar dari status masing-masing, Anda harus melanjutkan pengiriman callback terdaftar aplikasi setelah aplikasi keluar dari status masing-masing hingga aplikasi membatalkan pendaftaran callback-nya atau proses aplikasi berhenti.

Contoh:

IBinder binder = <...>;
bool shouldSendCallbacks = true;
binder.addFrozenStateChangeCallback(executor, (who, state) -> {
    if (state == IBinder.FrozenStateChangeCallback.STATE_FROZEN) {
        shouldSendCallbacks = false;
    } else if (state == IBinder.FrozenStateChangeCallback.STATE_UNFROZEN) {
        shouldSendCallbacks = true;
    }
});

Atau, Anda dapat menggunakan RemoteCallbackList yang tidak mengirim callback ke proses target saat dibekukan.

Contoh:

RemoteCallbackList<IInterface> rc =
        new RemoteCallbackList.Builder<IInterface>(
                        RemoteCallbackList.FROZEN_CALLEE_POLICY_DROP)
                .setExecutor(executor)
                .build();
rc.register(callback);
rc.broadcast((callback) -> callback.foo(bar));

callback.foo() hanya dipanggil jika prosesnya tidak dibekukan.

Aplikasi sering menyimpan update yang diterima menggunakan callback sebagai snapshot status terbaru. Pertimbangkan API hipotetis untuk aplikasi guna memantau persentase baterai yang tersisa:

interface BatteryListener {
    void onBatteryPercentageChanged(int newPercentage);
}

Pertimbangkan skenario saat beberapa peristiwa perubahan status terjadi saat aplikasi dibekukan. Saat aplikasi dicairkan, Anda hanya boleh mengirimkan status terbaru ke aplikasi dan menghapus perubahan status lama lainnya. Pengiriman ini akan segera terjadi saat aplikasi dicairkan sehingga aplikasi dapat "mengejar". Hal ini dapat dicapai sebagai berikut:

RemoteCallbackList<IInterface> rc =
        new RemoteCallbackList.Builder<IInterface>(
                        RemoteCallbackList.FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT)
                .setExecutor(executor)
                .build();
rc.register(callback);
rc.broadcast((callback) -> callback.onBatteryPercentageChanged(value));

Dalam beberapa kasus, Anda dapat melacak nilai terakhir yang dikirim ke aplikasi sehingga aplikasi tidak perlu diberi tahu tentang nilai yang sama setelah dicairkan.

Status dapat dinyatakan sebagai data yang lebih kompleks. Pertimbangkan API hipotetis agar aplikasi diberi tahu tentang antarmuka jaringan:

interface NetworkListener {
    void onAvailable(Network network);
    void onLost(Network network);
    void onChanged(Network network);
}

Saat menjeda notifikasi ke aplikasi, Anda harus mengingat kumpulan jaringan dan status yang terakhir dilihat aplikasi. Setelah melanjutkan, sebaiknya beri tahu aplikasi tentang jaringan lama yang hilang, jaringan baru yang tersedia, dan jaringan yang ada yang statusnya telah berubah - dalam urutan ini.

Jangan beri tahu aplikasi tentang jaringan yang tersedia, lalu hilang saat callback dijeda. Aplikasi tidak boleh menerima laporan lengkap peristiwa yang terjadi saat aplikasi dibekukan, dan dokumentasi API tidak boleh berjanji untuk mengirim streaming peristiwa tanpa gangguan di luar status siklus proses eksplisit. Dalam contoh ini, jika aplikasi perlu terus memantau ketersediaan jaringan, aplikasi harus tetap dalam status siklus proses yang mencegahnya di-cache atau dibekukan.

Dalam peninjauan, Anda harus menggabungkan peristiwa yang telah terjadi setelah menjeda dan sebelum melanjutkan notifikasi serta mengirimkan status terbaru ke callback aplikasi yang terdaftar secara ringkas.

Pertimbangan untuk dokumentasi developer

Pengiriman peristiwa asinkron dapat tertunda, baik karena pengirim menjeda pengiriman selama jangka waktu tertentu seperti yang ditunjukkan di bagian sebelumnya atau karena aplikasi penerima tidak menerima cukup resource perangkat untuk memproses peristiwa secara tepat waktu.

Melarang developer membuat asumsi tentang waktu antara saat aplikasi mereka diberi tahu tentang suatu peristiwa dan waktu terjadinya peristiwa tersebut.

Ekspektasi developer untuk menangguhkan API

Developer yang sudah memahami konkurensi terstruktur Kotlin mengharapkan perilaku berikut dari API penangguhan:

Fungsi penangguhan harus menyelesaikan semua pekerjaan terkait sebelum menampilkan atau menampilkan

Hasil operasi non-pemblokiran ditampilkan sebagai nilai hasil fungsi normal, dan error dilaporkan dengan menampilkan pengecualian. (Hal ini sering kali berarti bahwa parameter callback tidak diperlukan.)

Fungsi penangguhan hanya boleh memanggil parameter callback di tempat

Fungsi penangguhan harus selalu menyelesaikan semua pekerjaan terkait sebelum ditampilkan, sehingga tidak boleh memanggil callback yang disediakan atau parameter fungsi lainnya atau mempertahankan referensi ke fungsi tersebut setelah fungsi penangguhan ditampilkan.

Fungsi penangguhan yang menerima parameter callback harus mempertahankan konteks, kecuali jika didokumentasikan sebaliknya

Memanggil fungsi dalam fungsi penangguhan akan menyebabkannya berjalan di CoroutineContext pemanggil. Karena fungsi penangguhan harus menyelesaikan semua pekerjaan terkait sebelum menampilkan atau menampilkan, dan hanya boleh memanggil parameter callback di tempat, ekspektasi defaultnya adalah bahwa callback tersebut juga berjalan di CoroutineContext panggilan menggunakan dispatcher terkait. Jika tujuan API adalah menjalankan callback di luar CoroutineContext pemanggil, perilaku ini harus didokumentasikan dengan jelas.

Fungsi penangguhan harus mendukung pembatalan tugas kotlinx.coroutines

Setiap fungsi penangguhan yang ditawarkan harus bekerja sama dengan pembatalan tugas seperti yang ditentukan oleh kotlinx.coroutines. Jika tugas panggilan operasi yang sedang berlangsung dibatalkan, fungsi harus dilanjutkan dengan CancellationException sesegera mungkin agar pemanggil dapat membersihkan dan melanjutkan sesegera mungkin. Hal ini ditangani secara otomatis oleh suspendCancellableCoroutine dan API penangguhan lainnya yang ditawarkan oleh kotlinx.coroutines. Implementasi library umumnya tidak boleh menggunakan suspendCoroutine secara langsung, karena tidak mendukung perilaku pembatalan ini secara default.

Fungsi penangguhan yang melakukan pekerjaan pemblokiran di latar belakang (thread non-utama atau UI) harus menyediakan cara untuk mengonfigurasi dispatcher yang digunakan

Tidak direkomendasikan untuk membuat fungsi pemblokiran ditangguhkan sepenuhnya untuk beralih thread.

Memanggil fungsi penangguhan tidak boleh menghasilkan pembuatan thread tambahan tanpa mengizinkan developer menyediakan thread atau kumpulan thread mereka sendiri untuk melakukan pekerjaan tersebut. Misalnya, konstruktor dapat menerima CoroutineContext yang digunakan untuk melakukan pekerjaan latar belakang untuk metode class.

Fungsi penangguhan yang akan menerima parameter CoroutineContext atau Dispatcher opsional hanya untuk beralih ke dispatcher tersebut guna melakukan pekerjaan pemblokiran harus mengekspos fungsi pemblokiran yang mendasarinya dan merekomendasikan agar developer yang memanggil menggunakan panggilan mereka sendiri ke withContext untuk mengarahkan pekerjaan ke dispatcher yang dipilih.

Class yang meluncurkan coroutine

Class yang meluncurkan coroutine harus memiliki CoroutineScope untuk melakukan operasi peluncuran tersebut. Menghormati prinsip konkurensi terstruktur menyiratkan pola struktural berikut untuk mendapatkan dan mengelola cakupan tersebut.

Sebelum menulis class yang meluncurkan tugas serentak ke cakupan lain, pertimbangkan pola alternatif:

class MyClass {
    private val requests = Channel<MyRequest>(Channel.UNLIMITED)

    suspend fun handleRequests() {
        coroutineScope {
            for (request in requests) {
                // Allow requests to be processed concurrently;
                // alternatively, omit the [launch] and outer [coroutineScope]
                // to process requests serially
                launch {
                    processRequest(request)
                }
            }
        }
    }

    fun submitRequest(request: MyRequest) {
        requests.trySend(request).getOrThrow()
    }
}

Mengekspos suspend fun untuk melakukan pekerjaan serentak memungkinkan pemanggil memanggil operasi dalam konteksnya sendiri, sehingga tidak perlu memiliki MyClass yang mengelola CoroutineScope. Serialisasi pemrosesan permintaan menjadi lebih sederhana dan status sering kali dapat ada sebagai variabel lokal handleRequests, bukan sebagai properti class yang akan memerlukan sinkronisasi tambahan.

Class yang mengelola coroutine harus mengekspos metode tutup dan batalkan

Class yang meluncurkan coroutine sebagai detail implementasi harus menawarkan cara untuk menutup tugas serentak yang sedang berlangsung dengan rapi sehingga tidak membocorkan pekerjaan serentak yang tidak terkontrol ke dalam cakupan induk. Biasanya, ini berupa pembuatan Job turunan dari CoroutineContext yang disediakan:

private val myJob = Job(parent = `CoroutineContext`[Job])
private val myScope = CoroutineScope(`CoroutineContext` + myJob)

fun cancel() {
    myJob.cancel()
}

Metode join() juga dapat disediakan untuk memungkinkan kode pengguna menunggu penyelesaian pekerjaan serentak yang belum selesai yang dilakukan oleh objek. (Tindakan ini dapat mencakup pekerjaan pembersihan yang dilakukan dengan membatalkan operasi.)

suspend fun join() {
    myJob.join()
}

Penamaan operasi terminal

Nama yang digunakan untuk metode yang mematikan tugas serentak yang dimiliki oleh objek yang masih berlangsung harus mencerminkan kontrak perilaku tentang cara penonaktifan terjadi:

Gunakan close() saat operasi yang sedang berlangsung dapat selesai, tetapi tidak ada operasi baru yang dapat dimulai setelah panggilan ke close() ditampilkan.

Gunakan cancel() jika operasi yang sedang berlangsung dapat dibatalkan sebelum selesai. Tidak ada operasi baru yang dapat dimulai setelah panggilan ke cancel() ditampilkan.

Konstruktor class menerima CoroutineContext, bukan CoroutineScope

Jika objek dilarang diluncurkan langsung ke cakupan induk yang disediakan, kesesuaian CoroutineScope sebagai parameter konstruktor akan rusak:

// Don't do this
class MyClass(scope: CoroutineScope) {
    private val myJob = Job(parent = scope.`CoroutineContext`[Job])
    private val myScope = CoroutineScope(scope.`CoroutineContext` + myJob)

    // ... the [scope] constructor parameter is never used again
}

CoroutineScope menjadi wrapper yang tidak perlu dan menyesatkan yang dalam beberapa kasus penggunaan dapat dibuat hanya untuk diteruskan sebagai parameter konstruktor, hanya untuk dihapus:

// Don't do this; just pass the context
val myObject = MyClass(CoroutineScope(parentScope.`CoroutineContext` + Dispatchers.IO))

Parameter CoroutineContext secara default ditetapkan ke EmptyCoroutineContext

Saat parameter CoroutineContext opsional muncul di platform API, nilai defaultnya harus berupa sentinel Empty`CoroutineContext`. Hal ini memungkinkan komposisi perilaku API yang lebih baik, karena nilai Empty`CoroutineContext` dari pemanggil diperlakukan dengan cara yang sama seperti menerima default:

class MyOuterClass(
    `CoroutineContext`: `CoroutineContext` = Empty`CoroutineContext`
) {
    private val innerObject = MyInnerClass(`CoroutineContext`)

    // ...
}

class MyInnerClass(
    `CoroutineContext`: `CoroutineContext` = Empty`CoroutineContext`
) {
    private val job = Job(parent = `CoroutineContext`[Job])
    private val scope = CoroutineScope(`CoroutineContext` + job)

    // ...
}