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.