Menggunakan API yang lebih baru

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

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

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

Alternatif untuk menggunakan referensi kuat adalah menggunakan referensi lemah. Referensi lemah yang tidak ditemukan saat library dimuat akan menyebabkan alamat simbol tersebut ditetapkan ke nullptr, bukan gagal dimuat. API tersebut masih tidak dapat dipanggil dengan aman, tetapi selama situs panggilan dijaga untuk mencegah pemanggilan API saat tidak tersedia, kode Anda yang lain dapat dijalankan, dan Anda dapat memanggil API secara normal tanpa perlu menggunakan dlopen() dan dlsym().

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

Mengaktifkan referensi API lemah dalam 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 yang setara dengan Groovy 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, lihat dokumentasi untuk sistem build Anda guna 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 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 otomatis membuat panggilan ke API baru aman. Satu-satunya hal yang dilakukannya adalah menunda error waktu pemuatan ke error waktu panggilan. Manfaatnya adalah Anda dapat menjaga panggilan tersebut saat runtime dan kembali dengan baik, baik dengan menggunakan implementasi alternatif atau memberi tahu pengguna bahwa fitur aplikasi tersebut tidak tersedia di perangkat mereka, atau menghindari jalur kode tersebut sepenuhnya.

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

Berikut adalah contoh beberapa kode yang menggunakan API secara bersyarat tanpa mengaktifkan fitur ini, 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);
    }
}

Agak berantakan untuk dibaca, ada beberapa duplikasi nama fungsi (dan jika Anda menulis C, tanda tangan juga), build akan berhasil, tetapi selalu mengambil penggantian saat runtime jika Anda tidak sengaja salah ketik 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 API level yang memperkenalkan AImageDecoder_resultToString()).

Cara paling sederhana untuk menentukan nilai yang akan digunakan untuk __builtin_available adalah dengan mencoba mem-build tanpa penjaga (atau penjaga __builtin_available(android 1, *)) dan melakukan apa yang diinformasikan pesan error kepada Anda. Misalnya, panggilan yang tidak dijaga 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 selalu tersedia untuk minSdkVersion Anda dan tidak diperlukan penjaga, atau build Anda salah dikonfigurasi dan peringatan unguarded-availability dinonaktifkan.

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

Menghindari pengulangan penjaga API

Jika menggunakannya, 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 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();
    }
}

Keunikan penjaga API

Clang sangat spesifik tentang cara __builtin_available digunakan. Hanya if (__builtin_available(...)) literal (meskipun mungkin diganti dengan makro) yang berfungsi. Bahkan operasi yang tidak penting seperti if (!__builtin_available(...)) tidak akan berfungsi (Clang akan menampilkan peringatan unsupported-availability-guard, serta unguarded-availability). Hal ini dapat ditingkatkan pada versi Clang mendatang. Lihat Masalah LLVM 33161 untuk informasi selengkapnya.

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

Mengapa ini bukan setelan default?

Kecuali jika digunakan dengan benar, perbedaan antara referensi API yang kuat dan referensi API yang lemah adalah bahwa yang pertama akan gagal dengan cepat dan jelas, sedangkan yang kedua tidak akan gagal hingga pengguna mengambil tindakan yang menyebabkan API yang hilang dipanggil. Jika hal ini terjadi, pesan error tidak akan berupa error "AFoo_bar() is not available" yang jelas pada waktu kompilasi, tetapi akan berupa segfault. Dengan referensi yang kuat, pesan error akan jauh lebih jelas, dan gagal 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 kemungkinan akan selalu memiliki masalah ini, sehingga saat ini tidak ada rencana untuk mengubah perilaku default.

Kami menyarankan agar Anda menggunakannya, tetapi karena hal ini akan membuat masalah lebih sulit dideteksi dan di-debug, Anda harus menerima risiko tersebut dengan sadar, bukan perilaku yang berubah tanpa sepengetahuan Anda.

Peringatan

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

Yang paling tidak mungkin bermasalah adalah API libc yang lebih baru. Tidak seperti API Android lainnya, API tersebut dilindungi dengan #if __ANDROID_API__ >= X di header dan bukan hanya __INTRODUCED_IN(X), yang bahkan mencegah deklarasi lemah terlihat. Karena API level tertua yang didukung NDK modern adalah r21, API libc yang paling diperlukan sudah tersedia. API libc baru ditambahkan setiap rilis (lihat status.md), tetapi semakin baru, semakin besar kemungkinannya menjadi kasus ekstrem yang hanya diperlukan oleh sedikit developer. Namun, 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. Ini adalah masalah yang dapat dipecahkan, tetapi tindakan tersebut berisiko merusak kompatibilitas sumber untuk semua aplikasi (setiap kode yang berisi polyfill dari API libc akan gagal dikompilasi karena atribut availability yang tidak cocok pada deklarasi lokal dan libc), sehingga kami tidak yakin apakah atau kapan kami akan memperbaikinya.

Kasus yang mungkin dialami lebih banyak developer adalah saat library yang berisi API baru lebih baru dari minSdkVersion Anda. Fitur ini hanya memungkinkan referensi simbol yang lemah; tidak ada yang namanya referensi library yang lemah. Misalnya, jika minSdkVersion Anda adalah 24, Anda dapat menautkan libvulkan.so dan melakukan panggilan yang dilindungi ke vkBindBufferMemory2, karena libvulkan.so tersedia di perangkat yang dimulai dengan API 24. Di sisi lain, jika minSdkVersion Anda 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 baik 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 library

Jika Anda mengembangkan library untuk digunakan dalam aplikasi Android, Anda harus menghindari penggunaan fitur ini di header publik. Fungsi ini dapat digunakan dengan aman dalam kode out-of-line, 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. Oleh karena itu, Anda harus menghindari membuat pilihan tersebut atas nama konsumen.

Jika Anda memang memerlukan perilaku ini di header publik, pastikan untuk mendokumentasikannya agar pengguna mengetahui bahwa mereka harus mengaktifkan fitur tersebut dan mengetahui risikonya.