JNI adalah singkatan dari Java Native Interface. Antarmuka ini menentukan cara bagi bytecode yang dikompilasi Android dari kode terkelola (ditulis dalam bahasa pemrograman Java atau Kotlin) untuk berinteraksi dengan kode native (yang ditulis dalam C/C++). JNI tidak dibatasi untuk vendor tertentu, memiliki dukungan untuk memuat kode dari library bersama dinamis, dan cukup efisien walaupun terkadang rumit.
Catatan: Karena Android mengompilasi Kotlin ke bytecode yang sesuai untuk ART dengan cara yang mirip dengan bahasa pemrograman Java, Anda dapat menerapkan panduan di halaman ini pada bahasa pemrograman Kotlin dan Java dalam hal arsitektur JNI dan konsekuensi yang terkait. Untuk mempelajari lebih lanjut, lihat Kotlin dan Android.
Jika Anda belum terbiasa dengan JNI, baca Spesifikasi Java Native Interface untuk mengetahui cara kerjanya dan fitur-fitur yang tersedia. Beberapa aspek antarmuka ini mungkin tidak dapat langsung dipahami saat pertama kali dibaca. Jadi, beberapa bagian selanjutnya mungkin akan berguna bagi Anda.
Untuk mempelajari referensi JNI global serta melihat tempatnya dibuat dan dihapus, gunakan tampilan heap JNI pada Memory Profiler di Android Studio 3.2 dan yang lebih tinggi.
Tips Umum
Cobalah untuk meminimalkan jejak lapisan JNI Anda. Ada beberapa dimensi yang perlu dipertimbangkan di sini. Solusi JNI Anda harus mencoba mengikuti panduan berikut (yang dicantumkan di bawah sesuai urutan tingkat kepentingannya, mulai dari yang paling penting):
- Minimalkan marshalling resource di seluruh lapisan JNI. Marshalling di seluruh lapisan JNI memerlukan biaya besar. Cobalah untuk mendesain antarmuka yang meminimalkan jumlah data yang perlu Anda marshall dan frekuensi marshalling data yang harus dilakukan.
- Hindari komunikasi asinkron antara kode yang ditulis dalam bahasa pemrograman terkelola dan kode yang ditulis dalam C++ jika memungkinkan. Ini akan memudahkan pemeliharaan antarmuka JNI. Anda biasanya dapat menyederhanakan update UI asinkron dengan menyamakan bahasa update asinkron dan bahasa UI. Misalnya, alih-alih memanggil fungsi C++ dari UI thread dalam kode Java melalui JNI, sebaiknya lakukan callback antara dua thread dalam bahasa pemrograman Java, dengan salah satu thread yang melakukan panggilan C++ pemblokiran lalu memberi tahu UI thread saat panggilan pemblokiran tersebut selesai.
- Minimalkan jumlah thread yang perlu menyentuh atau disentuh oleh JNI. Jika Anda memang perlu menggunakan kumpulan thread dalam bahasa Java dan C++, cobalah untuk mempertahankan komunikasi JNI antara pemilik kumpulan, bukan antara masing-masing thread pekerja.
- Simpan kode antarmuka Anda di sejumlah kecil lokasi sumber C++ dan Java yang mudah diidentifikasi untuk memudahkan pemfaktoran ulang di masa mendatang. Pertimbangkan untuk menggunakan library pembuatan otomatis JNI sesuai kebutuhan.
JavaVM dan JNIEnv
JNI menentukan dua struktur data utama, "JavaVM" dan "JNIEnv". Kedua struktur ini pada dasarnya merupakan pointer ke pointer tabel fungsi. (Dalam versi C++, keduanya adalah class dengan pointer ke tabel fungsi dan fungsi anggota untuk setiap fungsi JNI yang mengarah melalui tabel tersebut.) JavaVM menyediakan fungsi "antarmuka pemanggilan", yang memungkinkan Anda membuat dan merusak JavaVM. Secara teori, Anda dapat memiliki beberapa JavaVM per proses, tetapi Android hanya mengizinkan satu JavaVM.
JNIEnv menyediakan sebagian besar fungsi JNI. Semua fungsi native Anda menerima JNIEnv sebagai
argumen pertama, kecuali untuk metode @CriticalNative
,
melihat panggilan native yang lebih cepat.
JNIEnv digunakan untuk penyimpanan lokal thread. Karena alasan ini, Anda tidak dapat membagikan JNIEnv antar-thread.
Jika tidak ada cara lain bagi suatu kode untuk mendapatkan JNIEnv miliknya, Anda harus berbagi JavaVM, lalu menggunakan GetEnv
untuk menemukan JNIEnv thread. (Dengan asumsi kode tersebut memilikinya; lihat AttachCurrentThread
di bawah.)
Deklarasi C untuk JNIEnv dan JavaVM berbeda dengan deklarasi C++. File penyertaan "jni.h"
menyediakan berbagai typedef, bergantung apakah file tersebut disertakan dalam C atau C++. Oleh karena itu, sebaiknya jangan sertakan argumen JNIEnv dalam file header yang disertakan oleh kedua bahasa tersebut. (Dengan kata lain: jika file header Anda memerlukan #ifdef __cplusplus
, Anda mungkin harus melakukan beberapa pekerjaan tambahan apabila ada sesuatu dalam header tersebut yang mengacu pada JNIEnv.)
Thread
Semua thread adalah thread Linux, yang dijadwalkan oleh kernel. Thread tersebut biasanya dimulai dari kode terkelola (menggunakan Thread.start()
), tetapi juga dapat dibuat di tempat lain lalu ditambahkan ke JavaVM
. Misalnya, thread yang dimulai dengan pthread_create()
atau std::thread
dapat ditambahkan menggunakan fungsi AttachCurrentThread()
atau AttachCurrentThreadAsDaemon()
. Sebelum ditambahkan, thread tidak akan memiliki JNIEnv, dan tidak dapat melakukan panggilan JNI.
Sebaiknya gunakan Thread.start()
untuk membuat thread apa pun yang perlu dipanggil ke kode Java. Cara ini akan memastikan Anda memiliki ruang stack yang cukup, menggunakan ThreadGroup
yang benar, dan menggunakan ClassLoader
yang sama dengan kode Java Anda. Selain itu, akan lebih mudah untuk menetapkan nama thread guna melakukan proses debug di Java dibandingkan dengan menetapkan dari kode native (lihat pthread_setname_np()
jika Anda memiliki pthread_t
atau thread_t
, dan std::thread::native_handle()
jika Anda memiliki std::thread
dan menginginkan pthread_t
).
Menambahkan thread yang dibuat secara native menyebabkan objek java.lang.Thread
dibuat dan ditambahkan ke ThreadGroup
"utama", sehingga membuatnya terlihat oleh debugger. Memanggil AttachCurrentThread()
pada thread yang sudah ditambahkan merupakan tindakan tanpa pengoperasian.
Android tidak menangguhkan thread yang mengeksekusi kode native. Jika pembersihan sampah memori sedang berlangsung, atau debugger mengeluarkan permintaan penangguhan, Android akan menjeda thread saat berikutnya melakukan panggilan JNI.
Thread yang ditambahkan melalui JNI harus memanggil DetachCurrentThread()
sebelum keluar.
Jika coding objek ini secara langsung dianggap tidak memungkinkan, Anda dapat menggunakan pthread_key_create()
di Android 2.0 (Eclair) dan yang lebih tinggi untuk menentukan fungsi destruktor yang akan dipanggil sebelum thread keluar, lalu memanggil DetachCurrentThread()
dari sana. (Gunakan kunci tersebut dengan pthread_setspecific()
untuk menyimpan JNIEnv dalam penyimpanan lokal thread; dengan demikian, kunci akan diteruskan ke destruktor Anda sebagai argumen.)
jclass, jmethodID, dan jfieldID
Jika ingin mengakses kolom objek dari kode native, Anda perlu melakukan tindakan berikut:
- Mendapatkan referensi objek class untuk class dengan
FindClass
- Mendapatkan ID kolom untuk kolom dengan
GetFieldID
- Mendapatkan konten kolom dengan sesuatu yang sesuai, seperti
GetIntField
Demikian pula, untuk memanggil metode, Anda perlu mendapatkan referensi objek class terlebih dahulu, lalu ID metode. ID tersebut terkadang hanyalah pointer ke struktur data runtime internal. Pencarian ID mungkin memerlukan beberapa pembandingan string, tetapi begitu ditemukan, panggilan sebenarnya untuk mendapatkan kolom atau memanggil metode dapat dilakukan dengan sangat cepat.
Jika performa menjadi pertimbangan penting, sebaiknya cari nilai tersebut sekali lagi lalu cache hasilnya dalam kode native Anda. Karena ada batas satu JavaVM per proses, maka wajar untuk menyimpan data ini dalam struktur lokal statis.
Referensi class, ID kolom, dan ID metode dijamin valid hingga class diurai. Class hanya akan diurai jika semua class yang terkait dengan ClassLoader dapat dibersihkan sampah memorinya, yang jarang terjadi tetapi tidak mustahil di Android. Namun, perlu diketahui bahwa jclass
adalah referensi class dan harus dilindungi dengan panggilan ke NewGlobalRef
(lihat bagian berikutnya).
Jika Anda ingin meng-cache ID saat sebuah class dimuat, lalu meng-cache ulang secara otomatis jika class diurai dan dimuat ulang, cara yang benar untuk menginisialisasi ID adalah menambahkan kode seperti berikut ke class yang sesuai:
Kotlin
companion object { /* * We use a static class initializer to allow the native code to cache some * field offsets. This native function looks up and caches interesting * class/field/method IDs. Throws on failure. */ private external fun nativeInit() init { nativeInit() } }
Java
/* * We use a class initializer to allow the native code to cache some * field offsets. This native function looks up and caches interesting * class/field/method IDs. Throws on failure. */ private static native void nativeInit(); static { nativeInit(); }
Buat metode nativeClassInit
dalam kode C/C++ yang melakukan pencarian ID. Kode akan dieksekusi satu kali, saat class diinisialisasi. Jika class diurai lalu dimuat ulang, maka kode tersebut akan dieksekusi lagi.
Referensi lokal dan global
Setiap argumen yang diteruskan ke metode native, dan hampir setiap objek yang ditampilkan oleh fungsi JNI, merupakan "referensi lokal". Artinya, argumen tersebut hanya valid selama durasi metode native saat ini di thread saat ini. Meskipun objek tersebut tetap aktif setelah metode native ditampilkan, referensinya tidak valid.
Hal ini berlaku untuk semua subclass jobject
, termasuk jclass
, jstring
, dan jarray
.
(Runtime akan memperingatkan Anda tentang sebagian besar kesalahan penggunaan referensi jika pemeriksaan JNI yang diperluas diaktifkan.)
Satu-satunya cara untuk mendapatkan referensi non-lokal adalah melalui fungsi NewGlobalRef
dan NewWeakGlobalRef
.
Jika ingin mempertahankan referensi untuk waktu yang lama, Anda harus menggunakan referensi "global". Fungsi NewGlobalRef
menggunakan referensi lokal sebagai argumen dan menampilkan referensi global.
Referensi global akan dijamin valid hingga Anda memanggil DeleteGlobalRef
.
Pola ini biasanya digunakan saat meng-cache jclass, yang ditampilkan dari FindClass
, misalnya:
jclass localClass = env->FindClass("MyClass"); jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
Semua metode JNI menerima referensi lokal dan global sebagai argumen.
Referensi ke objek yang sama mungkin saja memiliki nilai yang berbeda.
Misalnya, panggilan yang berurutan ke NewGlobalRef
pada objek yang sama mungkin akan menampilkan nilai hasil yang berbeda.
Untuk melihat apakah dua referensi mengacu pada objek yang sama, Anda harus menggunakan fungsi IsSameObject
. Jangan sekali-kali membandingkan referensi dengan ==
dalam kode native.
Salah satu konsekuensi dari hal ini adalah Anda tidak boleh menganggap referensi objek bersifat konstan atau unik dalam kode native. Nilai yang mewakili sebuah objek mungkin berbeda
dari satu pemanggilan metode ke pemanggilan berikutnya, dan ada kemungkinan dua
objek yang berbeda dapat memiliki nilai yang sama pada panggilan berturut-turut. Jangan gunakan
Nilai jobject
sebagai kunci.
Programmer dianjurkan untuk "tidak membuat alokasi berlebihan" atas referensi lokal. Dalam praktiknya, hal ini berarti bahwa jika Anda membuat banyak referensi lokal, mungkin saat menjalankan array objek, Anda harus menghapusnya secara manual dengan DeleteLocalRef
, bukan membiarkan JNI melakukannya. Implementasi tersebut hanya diperlukan untuk menyiapkan slot bagi 16 referensi lokal, sehingga jika memerlukan lebih banyak, Anda harus menghapusnya dalam proses atau menggunakan EnsureLocalCapacity
/PushLocalFrame
untuk menyiapkan lebih banyak slot.
Perlu diperhatikan bahwa jfieldID
dan jmethodID
adalah jenis buram, bukan referensi objek, dan tidak boleh diteruskan ke NewGlobalRef
. Pointer data mentah yang ditampilkan oleh fungsi seperti GetStringUTFChars
dan GetByteArrayElements
juga bukan merupakan objek. (Pointer tersebut dapat diteruskan antar-thread, dan valid hingga Release yang cocok melakukan panggilan.)
Ada satu kasus tidak wajar yang perlu disebutkan khusus di sini. Jika Anda menambahkan thread native dengan AttachCurrentThread
, kode yang Anda jalankan tidak akan menghapus referensi lokal secara otomatis hingga thread dilepas. Setiap referensi lokal yang Anda buat harus dihapus secara manual. Secara umum, setiap kode native yang menghasilkan referensi lokal dalam sebuah loop kemungkinan perlu melakukan penghapusan manual.
Harap berhati-hati saat menggunakan referensi global. Referensi global tidak dapat dihindari, tetapi sulit di-debug dan dapat menyebabkan (salah) perilaku memori yang sulit didiagnosis. Dengan hal-hal lainnya tetap sama, solusi dengan lebih sedikit referensi global mungkin akan lebih baik.
String UTF-8 dan UTF-16
Bahasa pemrograman Java menggunakan UTF-16. Agar praktis, JNI menyediakan metode yang juga berfungsi dengan UTF-8 Modifikasi. Encoding yang dimodifikasi ini berguna untuk kode C karena mengenkode \u0000 sebagai 0xc0 0x80, bukan 0x00. Sisi positifnya, Anda akan mendapatkan string berakhiran nol bergaya C, yang cocok untuk digunakan dengan fungsi string libc standar. Sisi negatifnya, Anda tidak dapat meneruskan data UTF-8 arbitrer ke JNI dan berharap data tersebut akan berfungsi dengan benar.
Untuk mendapatkan representasi UTF-16 dari String
, gunakan GetStringChars
.
Perlu diperhatikan bahwa string UTF-16 tidak diakhiri dengan nol, dan \u0000 diizinkan,
jadi Anda perlu berpegang pada panjang {i>string<i} serta {i>jchar pointer<i}.
Jangan lupa untuk Release
(melepas) string yang Anda Get
(dapatkan). Fungsi string menampilkan jchar*
atau jbyte*
, yang merupakan pointer bergaya C ke data primitif, bukan referensi lokal. String tersebut akan dijamin valid hingga Release
dipanggil yang berarti string tersebut tidak akan dilepas saat metode native ditampilkan.
Data yang diteruskan ke NewStringUTF harus berformat UTF-8 Modifikasi. Kesalahan yang umum terjadi adalah membaca data karakter dari file atau aliran jaringan dan menyerahkannya ke NewStringUTF
tanpa memfilternya.
Kecuali Anda yakin bahwa data tersebut adalah MUTF-8 yang valid (atau ASCII 7 bit, yang merupakan subset yang kompatibel), Anda harus menghapus karakter yang tidak valid atau mengonversinya menjadi format UTF-8 Modifikasi yang tepat.
Jika tidak, konversi UTF-16 kemungkinan akan memberikan hasil yang tidak terduga.
CheckJNI—yang aktif secara default untuk emulator—memindai string dan membatalkan VM jika input yang diterima tidak valid.
Sebelum Android 8, biasanya lebih cepat untuk beroperasi dengan string UTF-16 karena Android
tidak memerlukan salinan di GetStringChars
, sedangkan
GetStringUTFChars
memerlukan alokasi dan konversi ke UTF-8.
Android 8 mengubah representasi String
sehingga menggunakan 8 bit per karakter
untuk string ASCII (untuk menghemat memori) dan mulai menggunakan
bergerak
pembersih sampah memori. Fitur-fitur ini sangat mengurangi jumlah kasus di mana ART
dapat memberikan pointer ke data String
tanpa membuat salinan, bahkan
untuk GetStringCritical
. Namun, jika sebagian besar string yang diproses oleh kode
yang singkat, alokasi dan dealokasi dapat dihindari dalam banyak kasus dengan
menggunakan buffer dengan alokasi stack dan GetStringRegion
atau
GetStringUTFRegion
. Contoh:
constexpr size_t kStackBufferSize = 64u; jchar stack_buffer[kStackBufferSize]; std::unique_ptr<jchar[]> heap_buffer; jchar* buffer = stack_buffer; jsize length = env->GetStringLength(str); if (length > kStackBufferSize) { heap_buffer.reset(new jchar[length]); buffer = heap_buffer.get(); } env->GetStringRegion(str, 0, length, buffer); process_data(buffer, length);
Array dasar
JNI menyediakan fungsi untuk mengakses konten objek array. Sementara array objek harus diakses satu per satu, array dasar dapat dibaca dan ditulis secara langsung seolah-olah dideklarasikan di C.
Agar antarmuka tetap efisien tanpa membatasi implementasi VM, kumpulan panggilan Get<PrimitiveType>ArrayElements
memungkinkan runtime menampilkan pointer ke elemen sebenarnya, atau mengalokasikan beberapa memori dan membuat salinan. Apa pun itu, pointer mentah yang ditampilkan akan dijamin valid hingga panggilan Release
yang sesuai dikeluarkan (yang menyiratkan bahwa, jika data tidak disalin, objek array akan disematkan dan tidak dapat dialokasikan ulang sebagai bagian dari proses pemadatan heap).
Anda harus Release
(melepas) setiap array yang Anda Get
(dapatkan). Selain itu, jika panggilan Get
gagal, Anda harus memastikan kode Anda nantinya tidak mencoba Release
(melepas) pointer NULL.
Anda dapat menentukan apakah data disalin atau tidak dengan meneruskan pointer non-NULL untuk argumen isCopy
. Tindakan ini jarang berguna.
Panggilan Release
menggunakan argumen mode
yang dapat memiliki satu dari tiga nilai. Tindakan yang dijalankan oleh runtime bergantung pada apakah panggilan tersebut menampilkan pointer ke data sebenarnya atau salinannya:
0
- Sebenarnya: objek array tidak disematkan.
- Salinan: data disalin kembali. Buffer dengan salinan dibebaskan.
JNI_COMMIT
- Sebenarnya: tidak berfungsi apa pun.
- Salinan: data disalin kembali. Buffer dengan salinan tidak dibebaskan.
JNI_ABORT
- Sebenarnya: objek array tidak disematkan. Penulisan sebelumnya tidak dibatalkan.
- Salinan: buffer dengan salinan dibebaskan; setiap perubahan padanya akan hilang.
Salah satu alasan untuk memeriksa flag isCopy
adalah guna mengetahui apakah Anda perlu memanggil Release
dengan JNI_COMMIT
setelah melakukan perubahan pada array; jika Anda berpindah-pindah antara melakukan perubahan dan mengeksekusi kode yang menggunakan konten array, Anda mungkin dapat melewatkan commit tanpa pengoperasian. Alasan memungkinkan lainnya untuk memeriksa flag tersebut adalah untuk menangani JNI_ABORT
secara efisien. Misalnya, Anda mungkin ingin mendapatkan suatu array, memodifikasinya di tempat, meneruskan beberapa bagian ke fungsi lain, lalu menghapus perubahannya. Jika mengetahui bahwa JNI membuat salinan baru untuk Anda, Anda tidak perlu membuat salinan yang "dapat diedit" lagi. Jika JNI memberikan yang asli, Anda harus membuat salinan sendiri.
Salah satu kesalahan umum (yang diulang dalam contoh kode) adalah berasumsi bahwa Anda dapat melewati panggilan Release
jika *isCopy
bernilai salah. Bukan itu masalahnya. Jika tidak ada buffer salinan yang dialokasikan, memori asli harus dipasangi pin dan tidak boleh dipindahkan oleh pembersih sampah memori.
Perlu diperhatikan juga bahwa flag JNI_COMMIT
tidak melepas array, dan pada akhirnya Anda harus memanggil Release
lagi dengan flag lain.
Panggilan region
Terdapat alternatif untuk panggilan seperti Get<Type>ArrayElements
dan GetStringChars
yang mungkin akan sangat membantu saat Anda hanya ingin menyalin data ke dalam atau ke luar. Pertimbangkan hal berikut:
jbyte* data = env->GetByteArrayElements(array, NULL); if (data != NULL) { memcpy(buffer, data, len); env->ReleaseByteArrayElements(array, data, JNI_ABORT); }
Tindakan ini mengambil array, menyalin elemen byte len
pertama dari array, lalu melepas array. Bergantung pada implementasinya, panggilan Get
akan mengunci atau menyalin konten array.
Kode tersebut menyalin data (mungkin untuk kedua kalinya), lalu memanggil Release
; dalam hal ini, JNI_ABORT
memastikan tidak akan ada salinan ketiga.
Anda dapat memperoleh hasil yang sama dengan lebih sederhana:
env->GetByteArrayRegion(array, 0, len, buffer);
Kode ini memiliki beberapa keunggulan:
- Memerlukan satu panggilan JNI, bukan 2, sehingga mengurangi overhead.
- Tidak memerlukan penyematan atau salinan data tambahan.
- Mengurangi risiko error oleh programmer; tidak ada risiko lupa memanggil
Release
setelah terjadi kegagalan.
Anda juga dapat menggunakan panggilan Set<Type>ArrayRegion
untuk menyalin data ke dalam array, dan GetStringRegion
atau
GetStringUTFRegion
untuk menyalin karakter dari
String
.
Pengecualian
Anda tidak boleh memanggil sebagian besar fungsi JNI jika masih ada pengecualian yang tertunda.
Kode Anda diharapkan mengetahui pengecualian tersebut (melalui nilai hasil fungsi, ExceptionCheck
, atau ExceptionOccurred
) dan menampilkannya, atau menghapus dan menanganinya.
Fungsi JNI yang boleh Anda panggil selagi masih ada pengecualian yang tertunda dibatasi pada:
DeleteGlobalRef
DeleteLocalRef
DeleteWeakGlobalRef
ExceptionCheck
ExceptionClear
ExceptionDescribe
ExceptionOccurred
MonitorExit
PopLocalFrame
PushLocalFrame
Release<PrimitiveType>ArrayElements
ReleasePrimitiveArrayCritical
ReleaseStringChars
ReleaseStringCritical
ReleaseStringUTFChars
Banyak panggilan JNI dapat menampilkan pengecualian, tetapi sering kali menyediakan cara yang lebih mudah untuk memeriksa kegagalan. Misalnya, jika NewString
menampilkan nilai non-NULL, Anda tidak perlu memeriksa keberadaan pengecualian. Namun, jika Anda memanggil suatu metode (menggunakan fungsi seperti CallObjectMethod
), Anda harus selalu memeriksa keberadaan pengecualian, karena nilai hasil tidak akan valid jika ada pengecualian yang ditampilkan.
Perlu diketahui bahwa pengecualian yang ditampilkan oleh kode terkelola tidak melepaskan stack native
{i>frame<i}. (Dan pengecualian C++, umumnya tidak disarankan di Android, tidak boleh
ditampilkan melintasi batas transisi JNI dari kode C++ ke kode terkelola.)
Petunjuk JNI Throw
dan ThrowNew
hanya menetapkan pointer pengecualian di thread saat ini. Setelah ditampilkan dari kode native ke kode terkelola, pengecualian akan dicatat dan ditangani dengan tepat.
Kode native dapat "menangkap" pengecualian dengan memanggil ExceptionCheck
atau ExceptionOccurred
, dan menghapusnya dengan ExceptionClear
. Seperti biasa, menghapus pengecualian tanpa menanganinya dapat menyebabkan masalah.
Tidak ada fungsi bawaan untuk memanipulasi objek Throwable
sendiri, sehingga jika ingin (katakanlah) mendapatkan string pengecualian, Anda harus menemukan class Throwable
, mencari ID metode untuk getMessage "()Ljava/lang/String;"
, memanggilnya, dan jika hasilnya adalah non-NULL, gunakan GetStringUTFChars
untuk mendapatkan sesuatu yang dapat Anda berikan ke printf(3)
atau yang setara.
Pemeriksaan diperluas
JNI tidak banyak melakukan pengecekan error. Error biasanya menghasilkan crash. Android juga menawarkan sebuah mode yang disebut CheckJNI, di mana pointer tabel fungsi JavaVM dan JNIEnv dialihkan ke tabel fungsi yang menjalankan serangkaian pemeriksaan tambahan sebelum memanggil implementasi standar.
Pemeriksaan tambahan meliputi:
- Array: mencoba mengalokasikan array berukuran negatif.
- Pointer bermasalah: meneruskan jarray/jclass/jobject/jstring bermasalah ke panggilan JNI, atau meneruskan pointer NULL ke panggilan JNI dengan argumen yang non-nullable.
- Nama class: meneruskan apa saja selain nama class bergaya "java/lang/String" ke panggilan JNI.
- Panggilan penting: melakukan panggilan JNI antara get "penting" dan release yang terkait.
- ByteBuffers Langsung: meneruskan argumen buruk ke
NewDirectByteBuffer
. - Pengecualian: melakukan panggilan JNI saat masih ada pengecualian yang tertunda.
- JNIEnv*: menggunakan JNIEnv* dari thread yang salah.
- jfieldID: menggunakan jfieldID NULL, atau menggunakan jfieldID untuk menetapkan kolom ke sebuah nilai yang jenisnya salah (misalnya, mencoba menetapkan StringBuilder ke kolom String), atau menggunakan jfieldID kolom statis untuk menetapkan kolom instance atau sebaliknya, atau menggunakan jfieldID dari satu class dengan instance dari class lain.
- jmethodID: menggunakan jenis jmethodID yang salah saat melakukan panggilan JNI
Call*Method
: jenis nilai yang ditampilkan tidak tepat, ketidaksesuaian statis/non-statis, jenis yang salah untuk 'ini' (untuk panggilan nonstatis), atau class yang salah (untuk panggilan statis). - Referensi: menggunakan
DeleteGlobalRef
/DeleteLocalRef
pada jenis referensi yang salah. - Mode release: meneruskan mode release yang buruk ke panggilan release (selain
0
,JNI_ABORT
, atauJNI_COMMIT
). - Keamanan jenis: menampilkan jenis yang tidak kompatibel dari metode native (misalnya, menampilkan StringBuilder dari metode yang dinyatakan untuk menampilkan String).
- UTF-8: meneruskan urutan byte UTF-8 Modifikasi yang tidak valid ke panggilan JNI.
(Aksesibilitas metode dan kolom masih belum diperiksa: pembatasan akses tidak berlaku untuk kode native.)
Ada beberapa cara untuk mengaktifkan CheckJNI.
Jika Anda menggunakan emulator, CheckJNI aktif secara default.
Jika menggunakan perangkat yang telah di-root, Anda dapat menggunakan urutan perintah berikut untuk memulai ulang runtime dengan CheckJNI aktif:
adb shell stop adb shell setprop dalvik.vm.checkjni true adb shell start
Pada kasus mana pun, Anda akan melihat kode seperti ini dalam output logcat saat runtime dimulai:
D AndroidRuntime: CheckJNI is ON
Jika menggunakan perangkat reguler, Anda dapat menggunakan perintah berikut:
adb shell setprop debug.checkjni 1
Hal ini tidak akan memengaruhi aplikasi yang telah berjalan, tetapi aplikasi apa pun yang diluncurkan sejak saat itu akan memiliki CheckJNI aktif. (Ubah properti ini ke nilai lain apa pun, atau reboot untuk menonaktifkan lagi CheckJNI.) Dalam hal ini, Anda akan melihat kode seperti ini dalam output logcat saat berikutnya aplikasi dimulai:
D Late-enabling CheckJNI
Anda juga dapat menyetel atribut android:debuggable
dalam manifes aplikasi Anda ke
mengaktifkan CheckJNI hanya untuk aplikasi Anda. Perhatikan bahwa alat {i>build<i} Android akan
melakukan ini secara otomatis untuk
jenis build tertentu.
Library native
Anda dapat memuat kode native dari library bersama dengan System.loadLibrary
standar.
Dalam praktiknya, Android versi lama memiliki bug di PackageManager yang menyebabkan penginstalan dan update library native tidak dapat diandalkan. Project ReLinker menawarkan solusi untuk hal ini dan masalah pemuatan library native lainnya.
Panggil System.loadLibrary
(atau ReLinker.loadLibrary
) dari penginisialisasi class statis. Argumennya adalah nama library yang "tanpa dekorasi", sehingga untuk memuat libfubar.so
, Anda perlu meneruskan "fubar"
.
Jika hanya memiliki satu class dengan metode native, cukup masuk akal bahwa panggilan ke System.loadLibrary
akan berada pada penginisialisasi statis untuk class tersebut. Jika tidak, Anda mungkin ingin melakukan panggilan dari Application
untuk memastikan library selalu dimuat, dan selalu dimuat lebih awal.
Runtime dapat menemukan metode native Anda melalui dua cara. Anda dapat mendaftarkannya secara eksplisit dengan RegisterNatives
, atau membiarkan runtime mencarinya secara dinamis dengan dlsym
. Keuntungan menggunakan RegisterNatives
adalah Anda dapat memeriksa di awal bahwa simbol tersebut ada, serta dapat memiliki library bersama yang berukuran lebih kecil dan lebih cepat tanpa mengekspor apa pun selain JNI_OnLoad
. Kelebihan membiarkan runtime menemukan fungsi Anda adalah kode yang ditulis akan sedikit berkurang.
Untuk menggunakan RegisterNatives
:
- Berikan fungsi
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
. - Di
JNI_OnLoad
, daftarkan semua metode native menggunakanRegisterNatives
. - Buat dengan
-fvisibility=hidden
sehingga hanyaJNI_OnLoad
yang diekspor dari library Anda. Ini menghasilkan kode yang lebih cepat dan lebih kecil, serta menghindari potensi bentrok dengan library lain yang dimuat ke dalam aplikasi Anda (tetapi menimbulkan stack trace yang kurang berguna jika aplikasi Anda mengalami error pada kode native).
Penginisialisasi statis terlihat seperti ini:
Kotlin
companion object { init { System.loadLibrary("fubar") } }
Java
static { System.loadLibrary("fubar"); }
Fungsi JNI_OnLoad
akan terlihat seperti ini jika ditulis dalam C++:
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env; if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } // Find your class. JNI_OnLoad is called from the correct class loader context for this to work. jclass c = env->FindClass("com/example/app/package/MyClass"); if (c == nullptr) return JNI_ERR; // Register your class' native methods. static const JNINativeMethod methods[] = { {"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)}, {"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)}, }; int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod)); if (rc != JNI_OK) return rc; return JNI_VERSION_1_6; }
Untuk menggunakan "penemuan" metode native saja, Anda harus menamainya dengan cara tertentu (lihat spesifikasi JNI untuk mengetahui detailnya). Artinya, jika tanda tangan metode salah, Anda tidak akan mengetahuinya sampai metode tersebut benar-benar dipanggil untuk pertama kali.
Semua panggilan FindClass
yang dilakukan dari JNI_OnLoad
akan menyelesaikan class sesuai konteks loader class yang digunakan untuk memuat library bersama. Saat dipanggil dari konteks lain, FindClass
akan menggunakan loader class yang terkait dengan metode di bagian atas stack Java, atau jika tidak ada (karena panggilan berasal dari thread native yang baru ditambahkan), loader class "sistem" akan digunakan. Loader class sistem tidak mengetahui class aplikasi sehingga Anda tidak dapat mencari class Anda sendiri dengan FindClass
dalam konteks tersebut. Hal ini menjadikan JNI_OnLoad
sebagai tempat yang praktis untuk mencari dan meng-cache class: sekali
Anda memiliki referensi global jclass
yang valid
Anda dapat menggunakannya
dari utas mana pun yang terpasang.
Panggilan native yang lebih cepat dengan @FastNative
dan @CriticalNative
Metode native dapat dianotasi dengan
@FastNative
atau
@CriticalNative
(tetapi tidak keduanya) untuk mempercepat transisi antara kode native dan kode terkelola. Namun, anotasi ini
disertai dengan perubahan perilaku tertentu yang perlu
dipertimbangkan dengan cermat sebelum digunakan. Meskipun kita
menyebutkan perubahan ini secara singkat di bawah, lihat dokumentasi untuk mengetahui detailnya.
Anotasi @CriticalNative
hanya dapat diterapkan ke metode native yang tidak
menggunakan objek terkelola (dalam parameter atau nilai yang ditampilkan, atau sebagai this
implisit), dan tindakan ini
mengubah ABI transisi JNI. Implementasi native harus mengecualikan
Parameter JNIEnv
dan jclass
dari tanda tangan fungsinya.
Saat menjalankan metode @FastNative
atau @CriticalNative
, sampah memori
koleksi tidak dapat menangguhkan thread untuk pekerjaan penting dan dapat diblokir. Jangan gunakan ini
untuk metode yang berjalan lama, termasuk metode yang biasanya cepat, tetapi umumnya tidak terbatas.
Secara khusus, kode tidak boleh melakukan operasi I/O yang signifikan atau memperoleh kunci native yang
dapat disimpan untuk waktu yang lama.
Anotasi ini diimplementasikan untuk
penggunaan sistem karena
Android 8
dan menjadi publik dengan uji CTS
API di Android 14. Pengoptimalan ini kemungkinan juga akan berfungsi pada perangkat Android 8-13 (meskipun
tanpa jaminan CTS yang kuat), namun pencarian dinamis metode native hanya didukung di
Android 12+, pendaftaran eksplisit dengan RegisterNatives
JNI diwajibkan
untuk berjalan di Android versi 8-11. Anotasi ini diabaikan di Android 7-, ketidakcocokan ABI
untuk @CriticalNative
akan menyebabkan marshalling argumen yang salah dan kemungkinan error.
Untuk metode penting bagi performa yang memerlukan anotasi ini, sebaiknya
mendaftarkan metode secara eksplisit dengan RegisterNatives
JNI, bukan mengandalkan
"penemuan" berbasis nama metode native. Untuk mendapatkan performa startup aplikasi yang optimal, sebaiknya
untuk menyertakan pemanggil metode @FastNative
atau @CriticalNative
dalam
profil dasar pengukuran. Sejak Android 12,
panggilan ke metode native @CriticalNative
dari metode terkelola yang dikompilasi hampir sama
lebih murah daripada panggilan non-inline dalam C/C++ selama semua argumen sesuai dengan register (misalnya hingga
8 integral dan hingga 8 argumen floating point di arm64).
Terkadang lebih baik untuk membagi metode native menjadi dua, metode yang sangat cepat yang dapat gagal dan satu lagi yang menangani kasus lambat. Contoh:
Kotlin
fun writeInt(nativeHandle: Long, value: Int) { // A fast buffered write with a `@CriticalNative` method should succeed most of the time. if (!nativeTryBufferedWriteInt(nativeHandle, value)) { // If the buffered write failed, we need to use the slow path that can perform // significant I/O and can even throw an `IOException`. nativeWriteInt(nativeHandle, value) } } @CriticalNative external fun nativeTryBufferedWriteInt(nativeHandle: Long, value: Int): Boolean external fun nativeWriteInt(nativeHandle: Long, value: Int)
Java
void writeInt(long nativeHandle, int value) { // A fast buffered write with a `@CriticalNative` method should succeed most of the time. if (!nativeTryBufferedWriteInt(nativeHandle, value)) { // If the buffered write failed, we need to use the slow path that can perform // significant I/O and can even throw an `IOException`. nativeWriteInt(nativeHandle, value); } } @CriticalNative static native boolean nativeTryBufferedWriteInt(long nativeHandle, int value); static native void nativeWriteInt(long nativeHandle, int value);
Pertimbangan 64 bit
Untuk mendukung arsitektur yang menggunakan pointer 64 bit, gunakan kolom long
, bukan int
, saat menyimpan pointer ke struktur native di kolom Java.
Fitur yang tidak didukung/kompatibilitas mundur
Semua fitur JNI 1.6 didukung, dengan pengecualian berikut:
DefineClass
tidak diimplementasikan. Android tidak menggunakan file class atau bytecode Java, sehingga meneruskan data class biner tidak akan memberikan hasil.
Untuk kompatibilitas mundur dengan rilis Android yang lebih lama, berikut ini beberapa hal yang mungkin perlu Anda ketahui:
- Pencarian fungsi native secara dinamis
Hingga Android 2.0 (Eclair), karakter '$' tidak dikonversi dengan benar menjadi "_00024" selama penelusuran nama metode. Untuk mengatasi hal ini, diperlukan pendaftaran eksplisit atau pemindahan metode native keluar dari class dalam.
- Melepaskan thread
Hingga Android 2.0 (Eclair), Anda tidak dapat menggunakan fungsi destruktor
pthread_key_create
untuk menghindari pemeriksaan "thread harus dilepas sebelum keluar". (Runtime juga menggunakan fungsi destruktor kunci pthread, jadi akan terjadi persaingan mana yang dipanggil pertama.) - Referensi weak global
Hingga Android 2.2 (Froyo), referensi weak global tidak diimplementasikan. Versi yang lebih lama akan menolak dengan keras upaya penggunaan referensi ini. Anda dapat menggunakan konstanta versi platform Android untuk menguji ketersediaan dukungan.
Hingga Android 4.0 (Ice Cream Sandwich), referensi weak global hanya dapat diteruskan ke
NewLocalRef
,NewGlobalRef
, danDeleteWeakGlobalRef
. (Spesifikasi ini mendorong programmer untuk membuat referensi paksa ke referensi weak global sebelum melakukan apa pun dengannya, jadi hal ini sama sekali tidak membatasi.)Mulai dari Android 4.0 (Ice Cream Sandwich) dan seterusnya, referensi weak global dapat digunakan seperti referensi JNI lainnya.
- Referensi lokal
Hingga Android 4.0 (Ice Cream Sandwich), referensi lokal yang sebenarnya adalah pointer langsung. Ice Cream Sandwich menambahkan pengalihan yang diperlukan untuk mendukung pembersih sampah memori dengan lebih baik, tetapi ini berarti banyak bug JNI yang tidak terdeteksi pada rilis lama. Lihat Perubahan Referensi Lokal JNI di ICS untuk mengetahui detail selengkapnya.
Pada versi Android sebelum Android 8.0, jumlah referensi lokal dibatasi sesuai versi. Mulai Android 8.0, Android mendukung referensi lokal tanpa batas.
- Menentukan jenis referensi dengan
GetObjectRefType
Hingga Android 4.0 (Ice Cream Sandwich), sebagai konsekuensi dari penggunaan pointer langsung (lihat di atas),
GetObjectRefType
tidak akan dapat diimplementasikan dengan benar. Sebagai gantinya, kami menggunakan heuristik yang mencari pointer tabel referensi weak global, argumen referensi weak global, tabel referensi lokal, tabel referensi global sesuai urutan tersebut. Begitu menemukan pointer langsung, heuristik akan melaporkan bahwa referensi Anda merupakan jenis yang sedang diperiksanya. Ini berarti, misalnya, jika Anda memanggilGetObjectRefType
pada jclass global yang kebetulan sama dengan jclass yang diteruskan sebagai argumen implisit ke metode native statis, Anda akan mendapatkanJNILocalRefType
, bukanJNIGlobalRefType
. @FastNative
dan@CriticalNative
Hingga Android 7, anotasi pengoptimalan ini diabaikan. ABI ketidakcocokan untuk
@CriticalNative
akan menyebabkan argumen yang salah marshalling dan kemungkinan terjadinya kecelakaan.Pencarian dinamis fungsi native untuk
@FastNative
dan Metode@CriticalNative
tidak diterapkan di Android 8-10 dan berisi bug yang diketahui di Android 11. Menggunakan pengoptimalan ini tanpa pendaftaran eksplisit dengan JNIRegisterNatives
cenderung yang menyebabkan error di Android 8-11.FindClass
menampilkanClassNotFoundException
Untuk kompatibilitas mundur, Android akan menampilkan
ClassNotFoundException
bukanNoClassDefFoundError
jika class tidak ditemukan olehFindClass
. Perilaku ini konsisten dengan Java Refleksi APIClass.forName(name)
.
FAQ: Mengapa saya mendapatkan UnsatisfiedLinkError
?
Saat menangani kode native, kegagalan seperti ini adalah hal yang biasa:
java.lang.UnsatisfiedLinkError: Library foo not found
Dalam beberapa kasus, pesan itu berarti seperti yang ditampilkan — library tidak ditemukan. Dalam kasus lain, library ada, tetapi tidak dapat dibuka oleh dlopen(3)
, dan detail kegagalan dapat ditemukan dalam pesan detail pengecualian.
Beberapa alasan umum mengapa Anda menerima pengecualian "library tidak ditemukan":
- Library tidak ada atau tidak dapat diakses oleh aplikasi. Gunakan
adb shell ls -l <path>
untuk memeriksa keberadaan dan izinnya. - Library tidak dibuat dengan NDK. Hal ini dapat menimbulkan dependensi pada fungsi atau library yang tidak ada pada perangkat.
Class kegagalan UnsatisfiedLinkError
lainnya terlihat seperti berikut:
java.lang.UnsatisfiedLinkError: myfunc at Foo.myfunc(Native Method) at Foo.main(Foo.java:10)
Dalam logcat, Anda akan melihat:
W/dalvikvm( 880): No implementation found for native LFoo;.myfunc ()V
Ini berarti runtime mencoba menemukan metode yang cocok tetapi tidak berhasil. Beberapa alasan umumnya adalah:
- Library belum dimuat. Periksa output logcat untuk menemukan pesan tentang pemuatan library.
- Metode tidak ditemukan karena ketidakcocokan nama atau tanda tangan. Hal ini biasanya disebabkan oleh:
- Untuk pencarian metode lambat, kegagalan untuk mendeklarasikan fungsi C++ dengan
extern "C"
dan visibilitas yang sesuai (JNIEXPORT
). Perlu diperhatikan bahwa sebelum Ice Cream Sandwich, makro JNIEXPORT salah, sehingga penggunaan GCC baru denganjni.h
lama tidak akan berhasil. Anda dapat menggunakanarm-eabi-nm
untuk melihat simbol tersebut saat muncul di library; jika terlihat tidak jelas (seperti_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass
, bukanJava_Foo_myfunc
), atau jika jenis simbol adalah huruf 't' kecil, bukan T besar, Anda harus menyesuaikan deklarasinya. - Untuk pendaftaran eksplisit, error minor saat memasukkan tanda tangan metode. Pastikan apa yang Anda teruskan ke panggilan pendaftaran sesuai dengan tanda tangan dalam file log.
Harap diingat bahwa 'B' adalah
byte
dan 'Z' adalahboolean
. Komponen nama class dalam tanda tangan dimulai dengan 'L', diakhiri dengan ';', menggunakan '/' untuk memisahkan nama paket/class, dan menggunakan '$' untuk memisahkan nama class dalam (misalnya,Ljava/util/Map$Entry;
).
- Untuk pencarian metode lambat, kegagalan untuk mendeklarasikan fungsi C++ dengan
Penggunaan javah
untuk membuat header JNI secara otomatis dapat membantu Anda menghindari beberapa masalah.
FAQ: Mengapa FindClass
tidak dapat menemukan class saya?
(Sebagian besar saran ini juga berlaku untuk kegagalan dalam menemukan metode dengan GetMethodID
atau GetStaticMethodID
, atau kolom dengan GetFieldID
atau GetStaticFieldID
.)
Pastikan string nama class menggunakan format yang benar. Nama class JNI diawali dengan nama paket dan dipisahkan dengan garis miring, seperti java/lang/String
. Jika mencari class array, Anda harus memulai dengan jumlah kurung siku yang sesuai serta harus menggabungkan class dengan 'L' dan ';', sehingga array satu dimensi String
akan menjadi [Ljava/lang/String;
.
Jika mencari class dalam, gunakan '$', bukan '.'. Secara umum, menggunakan javap
pada file .class adalah cara yang baik untuk mengetahui nama internal class Anda.
Jika mengaktifkan penyingkatan kode, pastikan Anda mengonfigurasi kode mana yang harus dipertahankan. Penting untuk mengonfigurasi aturan yang tepat guna mempertahankan kode, karena penyingkat kode mungkin akan menghapus class, metode, atau kolom yang hanya digunakan dari JNI.
Jika nama class sudah benar, Anda mungkin mengalami masalah loader class. FindClass
ingin memulai penelusuran class di loader class yang terkait dengan kode Anda. Semua stack panggilan akan diperiksa, yang akan terlihat seperti:
Foo.myfunc(Native Method) Foo.main(Foo.java:10)
Metode paling atas adalah Foo.myfunc
. FindClass
menemukan objek ClassLoader
yang terkait dengan class Foo
dan menggunakannya.
Metode ini biasanya berfungsi sesuai harapan. Anda dapat mengalami masalah jika membuat thread sendiri (mungkin dengan memanggil pthread_create
lalu menambahkannya dengan AttachCurrentThread
). Sekarang, tidak ada frame stack dari aplikasi Anda.
Jika Anda memanggil FindClass
dari thread ini, JavaVM akan dimulai di loader class "sistem", bukan yang terkait dengan aplikasi Anda, sehingga upaya untuk menemukan class khusus aplikasi akan gagal.
Ada beberapa cara untuk mengatasi masalah ini:
- Lakukan pencarian
FindClass
sekali, diJNI_OnLoad
, lalu cache referensi class untuk digunakan nanti. Setiap panggilanFindClass
yang dilakukan sebagai bagian dari eksekusiJNI_OnLoad
akan menggunakan loader class yang terkait dengan fungsi yang memanggilSystem.loadLibrary
(ini merupakan aturan khusus untuk mempermudah inisialisasi library). Jika kode aplikasi Anda memuat library,FindClass
akan menggunakan loader class yang benar. - Teruskan instance class ke dalam fungsi yang memerlukannya, dengan mendeklarasikan metode native Anda agar menerima argumen Class, lalu meneruskan
Foo.class
. - Cache referensi ke objek
ClassLoader
di tempat yang mudah diakses, dan keluarkan panggilanloadClass
secara langsung. Proses ini memerlukan sedikit usaha.
FAQ: Bagaimana cara berbagi data mentah dengan kode native?
Anda mungkin akan menemukan situasi saat Anda perlu mengakses buffer data mentah berukuran besar dari kode native dan kode terkelola. Contoh umumnya mencakup manipulasi bitmap atau sampel suara. Ada dua pendekatan dasar.
Anda dapat menyimpan data di byte[]
. Cara ini memungkinkan akses yang sangat cepat dari kode terkelola. Namun, pada sisi native, tidak ada jaminan bahwa Anda dapat mengakses data tanpa menyalinnya. Dalam beberapa implementasi, GetByteArrayElements
dan GetPrimitiveArrayCritical
akan menampilkan pointer sebenarnya ke data mentah dalam heap terkelola, tetapi dalam implementasi lainnya, keduanya akan mengalokasikan buffer pada heap native dan menyalin data tersebut.
Cara lainnya adalah dengan menyimpan data dalam buffer byte langsung. Anda dapat membuatnya dengan java.nio.ByteBuffer.allocateDirect
, atau fungsi NewDirectByteBuffer
JNI. Tidak seperti buffer byte reguler, penyimpanan tidak dialokasikan pada heap terkelola, dan selalu dapat diakses secara langsung dari kode native (dapatkan alamatnya dengan GetDirectBufferAddress
). Bergantung pada cara implementasi akses buffer byte langsung, proses akses data dari kode terkelola dapat berjalan sangat lambat.
Pilihan yang akan digunakan bergantung pada dua faktor:
- Apakah sebagian besar akses data akan terjadi dari kode yang ditulis di Java atau di C/C++?
- Jika data pada akhirnya diteruskan ke API sistem, format apa yang harus digunakan? (Misalnya, jika pada akhirnya data diteruskan ke fungsi yang menggunakan byte[], mungkin akan kurang tepat untuk melakukan pemrosesan dalam
ByteBuffer
langsung.)
Jika tidak ada jawaban pasti, gunakan buffer byte langsung. Dukungan untuk buffer byte langsung sudah terintegrasi dalam JNI, dan performanya akan meningkat dalam rilis-rilis mendatang.