Menggunakan API yang lebih baru

Halaman ini menjelaskan bagaimana aplikasi Anda dapat menggunakan fungsi OS baru saat berjalan di versi OS baru sekaligus mempertahankan kompatibilitas dengan perangkat yang lebih lama.

Secara default, referensi ke NDK API dalam aplikasi Anda merupakan referensi yang kuat. Loader dinamis Android akan segera menyelesaikannya saat library Anda dimuat. Jika simbol tidak ditemukan, aplikasi akan dibatalkan. Hal ini bertentangan dengan perilaku Java, dengan pengecualian yang tidak akan ditampilkan hingga API yang hilang dipanggil.

Karena alasan ini, NDK akan mencegah Anda membuat referensi yang kuat ke API yang lebih baru daripada minSdkVersion aplikasi Anda. Hal ini melindungi Anda dari pengiriman kode secara tidak sengaja yang berfungsi selama pengujian, tetapi akan gagal dimuat (UnsatisfiedLinkError akan ditampilkan dari System.loadLibrary()) di perangkat lama. Di sisi lain, akan lebih sulit untuk menulis kode yang menggunakan API lebih baru daripada minSdkVersion aplikasi Anda, karena Anda harus memanggil API menggunakan dlopen() dan dlsym(), bukan panggilan fungsi normal.

Alternatif selain menggunakan referensi kuat adalah menggunakan referensi lemah. Referensi lemah yang tidak ditemukan saat library dimuat akan menghasilkan alamat simbol tersebut ditetapkan ke nullptr, bukan gagal dimuat. Panggilan tetap tidak dapat dipanggil dengan aman, tetapi selama situs panggilan dilindungi untuk mencegah pemanggilan API saat tidak tersedia, kode lainnya dapat dijalankan, dan Anda dapat memanggil API seperti biasa tanpa perlu menggunakan dlopen() dan dlsym().

Referensi API yang lemah tidak memerlukan dukungan tambahan dari linker dinamis, sehingga dapat digunakan dengan versi Android apa pun.

Mengaktifkan referensi API yang lemah di build Anda

CMake

Teruskan -DANDROID_WEAK_API_DEFS=ON saat menjalankan CMake. Jika Anda menggunakan CMake melalui externalNativeBuild, tambahkan hal berikut ke build.gradle.kts (atau Groovy yang setara jika Anda masih menggunakan build.gradle):

android {
    // Other config...

    defaultConfig {
        // Other config...

        externalNativeBuild {
            cmake {
                arguments.add("-DANDROID_WEAK_API_DEFS=ON")
                // Other config...
            }
        }
    }
}

ndk-build

Tambahkan kode berikut ke file Application.mk Anda:

APP_WEAK_API_DEFS := true

Jika Anda belum memiliki file Application.mk, buat file tersebut di direktori yang sama dengan file Android.mk Anda. Perubahan tambahan pada file build.gradle.kts (atau build.gradle) tidak diperlukan untuk ndk-build.

Sistem build lainnya

Jika Anda tidak menggunakan CMake atau ndk-build, baca dokumentasi bagi sistem build untuk melihat apakah ada cara yang direkomendasikan untuk mengaktifkan fitur ini. Jika sistem build Anda tidak mendukung opsi ini secara native, Anda dapat mengaktifkan fitur ini dengan meneruskan flag berikut saat mengompilasi:

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

Yang pertama mengonfigurasi header NDK untuk mengizinkan referensi yang lemah. Yang kedua mengubah peringatan untuk panggilan API yang tidak aman menjadi error.

Lihat Panduan Pengelola Sistem Build untuk mengetahui informasi selengkapnya.

Panggilan API yang dilindungi

Fitur ini tidak secara ajaib membuat panggilan ke API baru aman. Satu-satunya hal yang dilakukannya adalah menunda error waktu pemuatan ke error waktu panggilan. Manfaatnya adalah Anda dapat melindungi panggilan tersebut saat runtime dan melakukan fallback dengan baik, baik dengan menggunakan implementasi alternatif maupun memberi tahu pengguna bahwa fitur aplikasi tidak tersedia di perangkat mereka, atau menghindari jalur kode tersebut sepenuhnya.

Clang dapat memberikan peringatan (unguarded-availability) saat Anda melakukan panggilan tidak dilindungi ke API yang tidak tersedia untuk minSdkVersion aplikasi. Jika Anda menggunakan ndk-build atau file toolchain CMake, peringatan tersebut akan otomatis diaktifkan dan diubah menjadi error saat mengaktifkan fitur ini.

Berikut adalah contoh beberapa kode yang menggunakan API bersyarat tanpa mengaktifkan fitur ini, dengan menggunakan dlopen() dan dlsym():

void LogImageDecoderResult(int result) {
    void* lib = dlopen("libjnigraphics.so", RTLD_LOCAL);
    CHECK_NE(lib, nullptr) << "Failed to open libjnigraphics.so: " << dlerror();
    auto func = reinterpret_cast<decltype(&AImageDecoder_resultToString)>(
        dlsym(lib, "AImageDecoder_resultToString")
    );
    if (func == nullptr) {
        LOG(INFO) << "cannot stringify result: " << result;
    } else {
        LOG(INFO) << func(result);
    }
}

Membacanya agak berantakan, ada beberapa duplikasi nama fungsi (dan jika Anda menulis C, tanda tangan juga), build akan berhasil, tetapi selalu lakukan penggantian saat runtime jika Anda tidak sengaja salah mengetik nama fungsi yang diteruskan ke dlsym, dan Anda harus menggunakan pola ini untuk setiap API.

Dengan referensi API yang lemah, fungsi di atas dapat ditulis ulang sebagai:

void LogImageDecoderResult(int result) {
    if (__builtin_available(android 31, *)) {
        LOG(INFO) << AImageDecoder_resultToString(result);
    } else {
        LOG(INFO) << "cannot stringify result: " << result;
    }
}

Di balik layar, __builtin_available(android 31, *) memanggil android_get_device_api_level(), meng-cache hasilnya, dan membandingkannya dengan 31 (yang merupakan level API yang memperkenalkan AImageDecoder_resultToString()).

Cara termudah untuk menentukan nilai yang akan digunakan untuk __builtin_available adalah dengan mencoba membangun tanpa guard (atau guard __builtin_available(android 1, *)) dan melakukan apa yang ditunjukkan oleh pesan error tersebut. Misalnya, panggilan tak dilindungi ke AImageDecoder_createFromAAsset() dengan minSdkVersion 24 akan menghasilkan:

error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]

Dalam hal ini, panggilan harus dijaga oleh __builtin_available(android 30, *). Jika tidak ada error build, API tersebut selalu tersedia untuk minSdkVersion dan tidak diperlukan guard, atau build Anda salah dikonfigurasi dan peringatan unguarded-availability dinonaktifkan.

Atau, referensi API NDK akan mencantumkan sesuatu di sepanjang "Diperkenalkan di API 30" untuk setiap API. Jika teks tersebut tidak ada, berarti API tersebut tersedia untuk semua level API yang didukung.

Menghindari pengulangan guard API

Jika menggunakan ini, Anda mungkin akan memiliki bagian kode di aplikasi yang hanya dapat digunakan di perangkat yang cukup baru. Daripada mengulangi pemeriksaan __builtin_available() di setiap fungsi, Anda dapat menganotasi kode Anda sendiri sebagai memerlukan level API tertentu. Misalnya, ImageDecoder API itu sendiri ditambahkan di API 30, sehingga untuk fungsi yang banyak menggunakan API tersebut, Anda dapat melakukan hal seperti:

#define REQUIRES_API(x) __attribute__((__availability__(android,introduced=x)))
#define API_AT_LEAST(x) __builtin_available(android x, *)

void DecodeImageWithImageDecoder() REQUIRES_API(30) {
    // Call any APIs that were introduced in API 30 or newer without guards.
}

void DecodeImageFallback() {
    // Pay the overhead to call the Java APIs via JNI, or use third-party image
    // decoding libraries.
}

void DecodeImage() {
    if (API_AT_LEAST(30)) {
        DecodeImageWithImageDecoder();
    } else {
        DecodeImageFallback();
    }
}

Quirks API guard

Clang sangat spesifik terkait penggunaan __builtin_available. Hanya if (__builtin_available(...)) literal (meskipun mungkin diganti secara makro) yang berfungsi. Bahkan operasi sepele seperti if (!__builtin_available(...)) pun tidak akan berfungsi (Clang akan mengeluarkan peringatan unsupported-availability-guard, serta unguarded-availability). Hal ini dapat ditingkatkan di Clang versi mendatang. Lihat Masalah LLVM 33161 untuk informasi selengkapnya.

Pemeriksaan unguarded-availability hanya berlaku untuk cakupan fungsi tempat fungsi tersebut digunakan. Clang akan memberikan peringatan meskipun fungsi dengan panggilan API hanya dipanggil dari dalam cakupan yang dilindungi. Untuk menghindari pengulangan guard dalam kode Anda sendiri, lihat Menghindari pengulangan guard API.

Mengapa ini bukan default?

Kecuali digunakan dengan benar, perbedaan antara referensi API yang kuat dan referensi API yang lemah adalah bahwa referensi API yang pertama akan gagal dengan cepat dan jelas, sedangkan yang terakhir tidak akan gagal hingga pengguna melakukan tindakan yang menyebabkan API yang tidak ada dipanggil. Jika ini terjadi, pesan error tidak akan berupa error "AFoo_bar() is not available" pada waktu kompilasi yang jelas, yang akan berupa segfault. Dengan referensi yang kuat, pesan error menjadi jauh lebih jelas, dan kegagalan cepat adalah default yang lebih aman.

Karena ini adalah fitur baru, sangat sedikit kode yang ada yang ditulis untuk menangani perilaku ini dengan aman. Kode pihak ketiga yang tidak ditulis dengan mempertimbangkan Android mungkin akan selalu mengalami masalah ini, sehingga saat ini tidak ada rencana untuk perilaku default yang akan berubah.

Kami menyarankan Anda untuk menggunakan cara ini, tetapi karena akan membuat masalah lebih sulit untuk dideteksi dan di-debug, Anda harus menerima risiko tersebut dengan sengaja, bukan perubahan perilaku tanpa sepengetahuan Anda.

Peringatan

Fitur ini berfungsi untuk sebagian besar API, tetapi ada beberapa kasus di mana fitur ini tidak berfungsi.

Masalah yang paling kecil kemungkinannya adalah libc API yang lebih baru. Tidak seperti Android API lainnya, API tersebut dilindungi dengan #if __ANDROID_API__ >= X di header dan bukan hanya __INTRODUCED_IN(X), yang bahkan mencegah deklarasi yang lemah terlihat. Karena dukungan NDK modern level API terlama adalah r21, libc API yang paling umum diperlukan sudah tersedia. libc API baru ditambahkan pada setiap rilis (lihat status.md), tetapi makin baru, makin besar kemungkinannya menjadi kasus ekstrem yang akan diperlukan oleh sedikit developer. Meskipun demikian, jika Anda adalah salah satu developer tersebut, untuk saat ini Anda harus terus menggunakan dlsym() untuk memanggil API tersebut jika minSdkVersion Anda lebih lama dari API tersebut. Ini adalah masalah yang dapat diselesaikan, tetapi hal ini berisiko merusak kompatibilitas sumber untuk semua aplikasi (setiap kode yang berisi polyfill libc API akan gagal dikompilasi karena atribut availability yang tidak cocok pada libc dan deklarasi lokal), sehingga kami tidak yakin apakah atau kapan kami akan memperbaikinya.

Kasus yang cenderung ditemui lebih banyak developer adalah jika library yang berisi API baru lebih baru dari minSdkVersion Anda. Fitur ini hanya memungkinkan referensi simbol lemah. Tidak ada yang namanya referensi library yang lemah. Misalnya, jika minSdkVersion Anda adalah 24, Anda dapat menautkan libvulkan.so dan melakukan panggilan berjaga ke vkBindBufferMemory2, karena libvulkan.so tersedia di perangkat yang dimulai dengan API 24. Di sisi lain, jika minSdkVersion adalah 23, Anda harus kembali ke dlopen dan dlsym karena library tidak akan ada di perangkat pada perangkat yang hanya mendukung API 23. Kami tidak tahu solusi yang tepat untuk memperbaiki kasus ini, tetapi dalam jangka panjang masalah ini akan teratasi dengan sendirinya karena kami (jika memungkinkan) tidak lagi mengizinkan API baru untuk membuat library baru.

Untuk penulis perpustakaan

Jika Anda mengembangkan library yang akan digunakan dalam aplikasi Android, sebaiknya hindari penggunaan fitur ini di header publik. API ini dapat digunakan dengan aman dalam kode luar baris, tetapi jika Anda mengandalkan __builtin_available dalam kode apa pun di header, seperti fungsi inline atau definisi template, Anda akan memaksa semua konsumen untuk mengaktifkan fitur ini. Karena alasan yang sama kami tidak mengaktifkan fitur ini secara default di NDK, sebaiknya jangan membuat pilihan tersebut atas nama konsumen Anda.

Jika Anda memerlukan perilaku ini di header publik, pastikan untuk mendokumentasikannya sehingga pengguna tahu bahwa mereka perlu mengaktifkan fitur tersebut dan menyadari risikonya jika dilakukan.