Halaman ini menjelaskan bagaimana aplikasi Anda dapat menggunakan fungsi OS baru saat menjalankan di versi OS sementara menjaga kompatibilitas dengan perangkat 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, di mana 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. Hal ini melindungi Anda dari
secara tidak sengaja mengirimkan kode yang berfungsi selama pengujian Anda, tetapi akan gagal dimuat
(UnsatisfiedLinkError
akan ditampilkan dari System.loadLibrary()
) di versi yang lebih lama
perangkat. Di sisi lain, lebih sulit untuk menulis kode yang menggunakan API
lebih baru dari 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. A lemah
yang tidak ditemukan ketika pustaka memuat hasil di alamat
simbol tersebut disetel ke nullptr
, bukan gagal dimuat. Mereka masih
tidak dapat dipanggil dengan aman, tetapi selama situs panggilan dilindungi untuk mencegah panggilan
jika tidak tersedia, kode lainnya dapat dijalankan, dan Anda juga 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 kode berikut ke build.gradle.kts
(atau
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
sebagai 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 untuk build untuk melihat apakah ada cara yang disarankan untuk mengaktifkan fitur ini. Jika build Anda tidak mendukung opsi ini secara native, Anda dapat mengaktifkan fitur 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. Giliran kedua peringatan untuk panggilan API yang tidak aman yang menyebabkan 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 adalah menunda {i>error<i} {i>load-time<i} ke kesalahan {i>call-time<i}. Manfaatnya adalah Anda dapat menjaga panggilan tersebut saat runtime dan kembali dengan baik, baik dengan implementasi alternatif atau memberi tahu pengguna bahwa fitur aplikasi tersebut tidak tersedia di perangkat mereka, atau menghindari jalur kode itu sama sekali.
Clang dapat memberikan peringatan (unguarded-availability
) saat Anda membuat tindakan tidak dilindungi
ke API yang tidak tersedia untuk minSdkVersion
aplikasi Anda. Jika Anda
menggunakan ndk-build atau file toolchain CMake,
diaktifkan dan mengalami error saat mengaktifkan fitur ini.
Berikut ini contoh beberapa kode yang membuat penggunaan bersyarat dari API 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 sedang menulis C, tanda tangannya), kode itu akan
berhasil dibangun tetapi selalu
melakukan penggantian saat runtime jika Anda tidak sengaja mengetik nama fungsi yang diteruskan
ke dlsym
, sehingga 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()
, menyimpan hasil dalam cache, dan membandingkannya dengan 31
(yaitu level API yang memperkenalkan AImageDecoder_resultToString()
).
Cara termudah untuk menentukan nilai mana yang digunakan untuk __builtin_available
adalah dengan
upaya untuk membangun tanpa
penjaga (atau penjaga dari
__builtin_available(android 1, *)
) dan lakukan 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 ini akan selalu tersedia untuk
minSdkVersion
dan tidak diperlukan guard, atau build
Anda salah dikonfigurasi dan
Peringatan unguarded-availability
dinonaktifkan.
Atau, referensi NDK API akan mengatakan sesuatu di sepanjang baris "Diperkenalkan di API 30" untuk setiap API. Jika teks tersebut tidak ada, itu berarti bahwa API ini tersedia untuk semua level API yang didukung.
Menghindari pengulangan guard API
Jika Anda menggunakan ini, Anda mungkin akan
memiliki bagian kode di aplikasi Anda yang
hanya dapat digunakan di
perangkat yang cukup baru. Daripada mengulangi
pemeriksaan __builtin_available()
di setiap fungsi, Anda dapat menganotasi
kode sendiri sebagai memerlukan level API tertentu. Misalnya, ImageDecoder API
ditambahkan dalam API 30, jadi untuk fungsi yang banyak menggunakan
API yang dapat Anda lakukan 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 literal
(meskipun mungkin diganti secara makro) if (__builtin_available(...))
berfungsi. Merata
operasi sederhana seperti if (!__builtin_available(...))
tidak akan berfungsi (Clang
akan memberikan peringatan unsupported-availability-guard
, serta
unguarded-availability
). Tindakan ini dapat diperbaiki pada Clang versi mendatang. Lihat
Masalah LLVM 33161 untuk informasi selengkapnya.
Pemeriksaan untuk unguarded-availability
hanya berlaku untuk cakupan fungsi tempat
digunakan. Clang akan memberikan peringatan meskipun fungsi dengan panggilan API
hanya dapat dipanggil dari
dalam ruang lingkup yang dilindungi. Untuk menghindari pengulangan penjagaan di
kode Anda sendiri, lihat Menghindari pengulangan guard API.
Mengapa ini bukan default?
Kecuali digunakan dengan benar, perbedaan antara referensi API yang kuat dan API yang lemah referensi adalah bahwa yang pertama akan gagal dengan cepat dan jelas, sedangkan yang terakhir tidak akan gagal sampai pengguna mengambil tindakan yang menyebabkan API hilang untuk dipanggil. Ketika ini terjadi, pesan {i>error <i}tidak akan jelas waktu kompilasi "AFoo_bar() tidak tersedia" itu akan menjadi {i>segfault<i}. Dengan referensi yang kuat, pesan {i>error <i}jauh lebih jelas, dan kegagalan cepat adalah secara default 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 besar akan selalu mengalami masalah ini, jadi saat ini tidak ada rencana untuk perilaku default menjadi tidak pernah berubah.
Kami menyarankan Anda untuk menggunakan cara ini, tetapi karena akan memperburuk masalah sulit dideteksi dan didebug, Anda harus menerima risiko tersebut dengan sengaja dan daripada perubahan perilaku tanpa sepengetahuan Anda.
Peringatan
Fitur ini berfungsi untuk sebagian besar API, tetapi ada beberapa kasus saat Anda.
Masalah yang paling kecil kemungkinannya adalah libc API yang lebih baru. Tidak seperti materi lainnya
Android API, yang dilindungi dengan #if __ANDROID_API__ >= X
di header
dan bukan hanya __INTRODUCED_IN(X)
, yang bahkan mencegah deklarasi yang lemah dari
terlihat. Karena dukungan NDK modern level API terlama adalah r21,
libc API yang umumnya diperlukan sudah tersedia. libc API baru ditambahkan masing-masing
rilis (lihat status.md), tetapi makin baru
statusnya, makin besar kemungkinannya
menjadi {i>edge case<i} yang
akan dibutuhkan oleh beberapa pengembang. Meskipun demikian, jika Anda
adalah salah satu
developer itu, untuk saat ini Anda harus terus menggunakan dlsym()
untuk memanggil
API jika minSdkVersion
Anda lebih lama dari API. Ini adalah masalah yang dapat dipecahkan,
tetapi melakukan hal itu berisiko merusak kompatibilitas sumber untuk semua aplikasi (semua aplikasi
kode yang berisi polyfill libc API akan gagal dikompilasi karena
atribut availability
tidak cocok pada libc dan deklarasi lokal), sehingga
kami tidak yakin apakah atau
kapan kita akan memperbaikinya.
Kasus yang cenderung ditemui lebih banyak developer adalah ketika library yang
berisi API baru yang lebih baru dari minSdkVersion
Anda. Fitur ini saja
memungkinkan referensi
simbol yang lemah; tidak ada yang namanya perpustakaan yang lemah
alamat IP internal. Misalnya, jika minSdkVersion
Anda adalah 24, Anda dapat menautkan
libvulkan.so
dan melakukan panggilan berjaga ke vkBindBufferMemory2
, karena
libvulkan.so
tersedia di perangkat mulai API 24. Di sisi lain,
jika minSdkVersion
adalah 23, Anda harus kembali ke dlopen
dan dlsym
karena {i>library <i}tidak akan ada di perangkat
pada perangkat yang hanya mendukung
API 23. Kita tidak tahu solusi yang baik untuk
memperbaiki kasus ini, tetapi dalam jangka panjang
istilah itu akan teratasi dengan sendirinya karena kami (jika memungkinkan) tidak lagi mengizinkan
API untuk membuat library baru.
Untuk penulis perpustakaan
Jika Anda mengembangkan library yang akan digunakan dalam aplikasi Android, Anda harus
hindari penggunaan fitur ini di {i>header<i} publik Anda. Enkripsi ini dapat digunakan
dengan aman di
kode out-of-line, tetapi jika Anda mengandalkan __builtin_available
dalam kode apa pun di
seperti fungsi inline atau definisi template, Anda memaksa semua
untuk mengaktifkan fitur ini. Untuk alasan yang sama, kami tidak dapat
fitur secara default di NDK, Anda harus menghindari membuat pilihan itu atas nama
konsumen Anda.
Jika Anda memerlukan perilaku ini di {i>header<i} publik, pastikan untuk mendokumentasikan sehingga pengguna Anda tahu bahwa mereka perlu mengaktifkan fitur tersebut dan menyadari risiko melakukannya.