Menggunakan API yang lebih baru

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.