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.