Tips JNI

JNI adalah singkatan dari Java Native Interface. JNI mendefinisikan interaksi bytecode, yang dikompilasi Android dari kode terkelola (yang ditulis dalam bahasa pemrograman Java atau Kotlin) dengan kode native (yang ditulis dalam C/C++). JNI tidak bergantung vendor, mendukung pemuatan kode dari library bersama dinamis, dan meskipun rumit kadang-kadang cukup efisien.

Catatan: Karena Android mengompilasi Kotlin ke bytecode yang sesuai untuk ART dengan cara yang mirip dengan bahasa pemrograman Java, Anda dapat menerapkan panduan pada halaman ini untuk bahasa pemrograman Kotlin dan Java dalam hal arsitektur JNI dan biaya terkaitnya. Untuk mempelajari lebih lanjut, lihat Kotlin dan Android.

Jika Anda belum terbiasa dengan JNI, bacalah Spesifikasi Java Native Interface untuk mengetahui cara kerjanya dan fitur-fitur yang tersedia. Beberapa aspek antarmuka ini mungkin tidak bisa langsung dipahami pada pembacaan pertama, jadi beberapa bagian selanjutnya mungkin akan berguna bagi Anda.

Untuk menjelajahi referensi JNI global dan melihat tempat referensi JNI global dibuat dan dihapus, gunakan tampilan JNI heap 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. Biasanya Anda dapat menyederhanakan update UI asinkron dengan mempertahankan update asinkron dalam bahasa yang sama dengan UI. Misalnya, bukannya memanggil fungsi C++ dari UI thread dalam kode Java melalui JNI, sebaiknya lakukan callback antara dua thread dalam bahasa pemrograman Java, dengan salah satu darinya melakukan panggilan C++ pemblokiran dan kemudian memberi tahu UI thread setelah panggilan pemblokiran 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 jika perlu.

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.

JNIEnv digunakan untuk penyimpanan lokal thread. Karena alasan ini, Anda tidak dapat membagikan JNIEnv antar-thread. Jika sepotong kode tidak memiliki cara lain untuk mendapatkan JNIEnv-nya, Anda harus membagikan JavaVM, dan menggunakan GetEnv untuk menemukan JNIEnv thread tersebut. (Dengan mengasumsikan kode tersebut memiliki JNIEnv; lihat AttachCurrentThread bawah ini.)

Deklarasi C untuk JNIEnv dan JavaVM berbeda dengan deklarasi C++. File include "jni.h" menyediakan typedef yang berlainan, bergantung pada apakah file itu disertakan dalam C atau C++. Karena alasan ini, jangan sertakan argumen JNIEnv dalam file header yang disertakan oleh kedua bahasa. (Dengan kata lain: jika file header Anda memerlukan #ifdef __cplusplus, Anda mungkin perlu melakukan tugas tambahan jika ada sesuatu dalam header tersebut yang merujuk ke JNIEnv.)

Thread

Semua thread adalah thread Linux, yang dijadwalkan oleh kernel. Thread biasanya dimulai dari kode terkelola (menggunakan Thread.start), tetapi bisa juga dibuat dari tempat lain dan kemudian ditambahkan ke JavaVM. Misalnya, thread yang dimulai dengan pthread_create dapat ditambahkan dengan fungsi JNI AttachCurrentThread atau AttachCurrentThreadAsDaemon. Sebelum ditambahkan, thread tidak akan memiliki JNIEnv, dan tidak dapat melakukan panggilan JNI.

Penambahan thread yang dibuat secara native menyebabkan objek java.lang.Thread dibuat dan ditambahkan ke ThreadGroup "utama", sehingga terlihat oleh debugger. Pemanggilan AttachCurrentThread pada thread yang sudah ditambahkan tidak akan berhasil.

Android tidak menangguhkan thread yang menjalankan kode native. Jika pembersihan sampah memori sedang berlangsung, atau debugger mengeluarkan permintaan penangguhan, Android akan menjeda thread saat berikutnya membuat panggilan JNI.

Thread yang ditambahkan melalui JNI harus memanggil DetachCurrentThread sebelum keluar. Jika mengkodekan objek ini secara langsung tidak praktis, pada Android 2.0 (Eclair) dan yang lebih tinggi, Anda dapat menggunakan pthread_key_create untuk menentukan fungsi destruktor yang akan dipanggil sebelum thread keluar, dan memanggil DetachCurrentThread dari sana. (Gunakan kunci itu dengan pthread_setspecific untuk menyimpan JNIEnv di penyimpanan lokal thread; dengan cara ini, JNIEnv akan diteruskan ke destruktor 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 tersebut dengan FindClass
  • Mendapatkan ID kolom untuk kolom tersebut dengan GetFieldID
  • Mendapatkan konten kolom dengan sesuatu yang sesuai, seperti GetIntField

Demikian pula, untuk memanggil metode, pertama-tama Anda perlu mendapatkan referensi objek class dan kemudian ID metode. ID ini 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 ke 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 diurai jika semua class yang terkait dengan ClassLoader dapat dibersihkan sampah memorinya, yang jarang terjadi tetapi tidak mustahil di Android. Namun, perlu dicatat bahwa jclass adalah referensi class dan harus dilindungi dengan panggilan ke NewGlobalRef (lihat bagian selanjutnya).

Jika Anda ingin meng-cache ID saat class dimuat, dan otomatis meng-cache ulang saat class diurai dan dimuat kembali, cara yang benar untuk menginisialisasi ID adalah dengan menambahkan sepotong kode yang terlihat seperti ini 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();
        }
    

Buatlah metode nativeClassInit dalam kode C/C++ Anda yang menjalankan pencarian ID. Kode akan dijalankan satu kali, saat class diinisialisasi. Jika class diurai dan kemudian dimuat ulang, maka kode ini akan dijalankan 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 kembali, referensi tidak valid.

Ini berlaku untuk semua sub-class 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 mengambil referensi lokal sebagai argumen dan menampilkan referensi global. Referensi global 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 untuk memiliki nilai yang berbeda. Misalnya, nilai kembalian (return value) dari panggilan berurutan ke NewGlobalRef pada objek yang sama mungkin saja berbeda. Untuk melihat apakah dua referensi merujuk ke objek yang sama, gunakan 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 32 bit yang mewakili suatu objek mungkin berbeda dari satu pemanggilan metode ke pemanggilan metode berikutnya, dan mungkin saja dua objek berbeda memiliki nilai 32 bit yang sama pada panggilan berurutan. Jangan menggunakan nilai jobject sebagai kunci.

Programmer dianjurkan untuk "tidak mengalokasikan secara berlebihan" referensi lokal. Secara operasional, hal ini berarti jika Anda membuat banyak referensi lokal, mungkin saat menjalankannya melalui array objek, Anda perlu membebaskan referensi tersebut secara manual dengan DeleteLocalRef, bukan membiarkan JNI melakukannya untuk Anda. Implementasi ini hanya diperlukan untuk memesan 16 slot referensi lokal, jadi jika memerlukan lebih banyak slot, Anda harus menghapusnya selagi menggunakan atau menggunakan EnsureLocalCapacity/PushLocalFrame untuk memesan lebih banyak slot.

Perhatikan bahwa jfieldID dan jmethodID berjenis opaque, bukan merupakan referensi objek, dan sebaiknya tidak 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 AttachCurrentThread ke thread native, kode yang Anda jalankan tidak akan bisa membebaskan referensi lokal secara otomatis hingga thread tersebut terlepas. 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.

Jika memungkinkan, bekerja dengan string UTF-16 biasanya akan lebih cepat. Saat ini, Android tidak memerlukan salinan di GetStringChars, sedangkan GetStringUTFChars memerlukan alokasi dan konversi ke UTF-8. Perhatikan bahwa string UTF-16 tidak diakhiri nol, dan \u0000 diizinkan, jadi Anda harus berpegang pada panjang string dan juga pointer jchar.

Jangan lupa untuk Release string yang Anda Get. Fungsi string ini menampilkan jchar* atau jbyte*, yang merupakan pointer bergaya C ke data primitif, bukan ke referensi lokal. Pointer ini dijamin valid hingga Release dipanggil, yang berarti pointer tidak dilepas saat metode native kembali.

Data yang diteruskan ke NewStringUTF harus berformat UTF-8 Modifikasi. Salah satu kesalahan umum dalam hal ini adalah membaca data karakter dari file atau aliran jaringan dan menyerahkannya ke NewStringUTF tanpa memfilternya. Kecuali Anda yakin bahwa data itu adalah MUTF-8 yang valid (atau ASCII 7 bit, yang merupakan subset yang kompatibel), Anda perlu 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.

Array primitif

JNI menyediakan fungsi untuk mengakses konten objek array. Sementara array objek harus diakses satu per satu, array primitif dapat dibaca dan ditulis secara langsung seolah-olah dideklarasikan dalam C.

Untuk membuat antarmuka seefisien mungkin tanpa menghambat implementasi VM, kelompok panggilan Get<PrimitiveType>ArrayElements memungkinkan runtime untuk menampilkan pointer ke elemen sebenarnya, atau mengalokasikan sebagian memori dan membuat salinan. Mana pun pilihannya, pointer mentah yang ditampilkan dijamin valid hingga panggilan Release yang sesuai dilakukan (yang menyiratkan bahwa, jika data tidak disalin, objek array akan disematkan dan tidak dapat direlokasi sebagai bagian dari pemadatan heap). Anda harus Release setiap array yang Anda Get. Selain itu, jika panggilan Get gagal, Anda harus memastikan bahwa kode Anda tidak mencoba untuk Release pointer NULL nantinya.

Anda dapat menentukan apakah data disalin atau tidak dengan meneruskan pointer non-NULL untuk argumen isCopy. Tindakan ini jarang berguna.

Panggilan Release menerima argumen mode yang dapat memiliki satu dari tiga nilai. Tindakan yang dijalankan oleh runtime bergantung pada apakah panggilan itu 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 untuk mengetahui apakah Anda perlu memanggil Release dengan JNI_COMMIT setelah membuat perubahan pada suatu array — jika Anda berpindah-pindah antara membuat perubahan dan menjalankan kode yang menggunakan konten array tersebut, Anda mungkin dapat melewati commit tanpa pengoperasian. Kemungkinan alasan lainnya untuk memeriksa flag adalah untuk menangani JNI_ABORT secara efisien. Misalnya, Anda mungkin ingin mendapatkan suatu array, memodifikasinya di tempat, meneruskan potongan 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 memberi Anda yang asli, maka Anda harus membuat salinan sendiri.

Salah satu kesalahan umum (diulang dalam kode contoh) adalah menganggap bahwa Anda dapat melewati panggilan Release jika *isCopy false. Bukan itu masalahnya. Jika tidak ada buffer salinan yang dialokasikan, maka memori asli harus disematkan dan tidak dapat dipindahkan oleh pengumpul sampah memori.

Perhatikan juga bahwa flag JNI_COMMIT tidak melepaskan array, dan Anda harus memanggil Release lagi dengan flag berbeda nantinya.

Panggilan region

Tersedia sebuah alternatif untuk panggilan seperti Get<Type>ArrayElements dan GetStringChars yang dapat sangat berguna jika satu-satunya hal yang ingin Anda lakukan adalah menyalin data masuk atau keluar. Pertimbangkan yang berikut:

    jbyte* data = env->GetByteArrayElements(array, NULL);
        if (data != NULL) {
            memcpy(buffer, data, len);
            env->ReleaseByteArrayElements(array, data, JNI_ABORT);
        }

Kode ini mengambil array, menyalin elemen byte len pertama darinya, lalu melepaskan array tersebut. Bergantung pada implementasinya, panggilan Get akan menyematkan atau menyalin konten array. Kode ini menyalin data (mungkin untuk kedua kalinya), lalu memanggil Release; dalam hal ini JNI_ABORT memastikan tidak ada kemungkinan 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 sesuatu gagal.

Dengan cara yang sama, Anda dapat menggunakan panggilan Set<Type>ArrayRegion untuk menyalin data ke dalam suatu array, dan GetStringRegion atau GetStringUTFRegion untuk menyalin karakter dari sebuah String.

Pengecualian

Anda tidak boleh memanggil sebagian besar fungsi JNI jika masih ada pengecualian yang tertunda. Kode Anda diharapkan dapat melihat pengecualian (melalui nilai kembalian fungsi, ExceptionCheck, atau ExceptionOccurred) dan menampilkan atau menghapus pengecualian itu, lalu 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 yang dapat menampilkan pengecualian, tetapi sering menyediakan cara lebih sederhana untuk memeriksa kegagalan. Misalnya, jika NewString menampilkan nilai non-NULL, Anda tidak perlu memeriksa keberadaan pengecualian. Namun, jika memanggil sebuah metode (menggunakan fungsi seperti CallObjectMethod), Anda harus selalu memeriksa keberadaan pengecualian, karena nilai kembalian tidak akan valid jika ada pengecualian yang ditampilkan.

Perhatikan bahwa pengecualian yang ditampilkan oleh kode yang ditafsirkan tidak menghilangkan frame tumpukan native, dan Android belum mendukung pengecualian C++. Instruksi Throw dan ThrowNew JNI hanya menetapkan pointer pengecualian pada thread saat ini. Setelah kembali 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 itu sendiri, jadi jika Anda ingin (misalkan) mendapatkan string pengecualian, Anda harus menemukan class Throwable, mencari ID metode untuk getMessage "()Ljava/lang/String;", memanggilnya, dan jika hasilnya non-NULL, menggunakan GetStringUTFChars untuk mendapatkan sesuatu yang dapat Anda serahkan 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.
  • ByteBuffer langsung: meneruskan argumen bermasalah ke NewDirectByteBuffer.
  • Pengecualian: melakukan panggilan JNI sementara 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 dari jenis yang salah (misalnya, mencoba menetapkan kolom 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 jmethodID dari jenis yang salah saat melakukan panggilan JNI Call*Method: jenis kembalian salah, ketidakcocokan statis/non-statis, jenis yang salah untuk 'ini' (untuk panggilan tidak-statis) atau class yang salah (untuk panggilan statis).
  • Referensi: menggunakan DeleteGlobalRef/DeleteLocalRef pada jenis referensi yang salah.
  • Mode release: meneruskan mode release bermasalah ke panggilan release (sesuatu selain 0, JNI_ABORT, atau JNI_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 aplikasi dimulai lagi:

D Late-enabling CheckJNI

Anda juga dapat menetapkan atribut android:debuggable dalam manifes aplikasi untuk mengaktifkan CheckJNI khusus untuk aplikasi Anda. Perhatikan bahwa fitur build Android akan otomatis melakukan hal ini 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 "tanpa dekorasi", jadi untuk memuat libfubar.so, Anda harus memasukkan "fubar".

Runtime dapat menemukan metode native Anda melalui dua cara. Anda dapat secara eksplisit mendaftarkannya menggunakan RegisterNatives, atau Anda dapat membiarkan runtime mencarinya secara dinamis dengan dlsym. Kelebihan RegisterNatives adalah Anda dapat memeriksa di awal bahwa simbol-simbol itu ada, plus Anda mendapatkan library bersama yang lebih kecil dan lebih cepat dengan tidak mengekspor apa pun kecuali JNI_OnLoad. Kelebihan membiarkan runtime menemukan fungsi Anda adalah kode yang ditulis akan sedikit berkurang.

Untuk menggunakan RegisterNatives:

  • Tentukan fungsi JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved).
  • Pada JNI_OnLoad Anda, daftarkan semua metode native Anda menggunakan RegisterNatives.
  • Buat project dengan -fvisibility=hidden sehingga hanya JNI_OnLoad yang diekspor dari library Anda. Cara ini menghasilkan kode yang lebih cepat dan lebih kecil, dan menghindari potensi benturan dengan library lain yang dimuat ke dalam aplikasi Anda (tetapi cara ini menghasilkan pelacakan tumpukan 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 -1;
        }

        // Get jclass with env->FindClass.
        // Register methods with env->RegisterNatives.

        return JNI_VERSION_1_6;
    }

Untuk menggunakan "penemuan" metode native saja, Anda harus menamainya dengan cara tertentu (lihat spesifikasi JNI untuk detailnya). Ini berarti jika tanda tangan metode salah, Anda tidak akan mengetahuinya sampai metode tersebut benar-benar dipanggil untuk pertama kali.

Jika hanya ada satu class yang menggunakan metode native, sangat wajar jika panggilan ke System.loadLibrary dilakukan dari class tersebut. Jika tidak, sebaiknya Anda melakukan panggilan dari Application agar Anda tahu bahwa metode itu selalu dimuat, dan selalu dimuat lebih awal.

Setiap panggilan FindClass yang dilakukan dari JNI_OnLoad akan menyelesaikan class dalam konteks loader class yang digunakan untuk memuat library bersama. Biasanya, FindClass menggunakan loader yang terkait dengan metode yang tercantum di bagian teratas tumpukan Java, atau jika tidak ada (karena thread baru saja ditambahkan), maka loader class "sistem" akan digunakan. Hal ini menjadikan JNI_OnLoad sebagai tempat yang praktis untuk mencari dan men-cache referensi objek class.

Pertimbangan 64 bit

Untuk mendukung arsitektur yang menggunakan pointer 64 bit, gunakan kolom long, bukan int, saat menyimpan pointer ke struktur native pada kolom Java.

Fitur yang tidak didukung/kompatibilitas dengan versi sebelumnya

Semua fitur JNI 1.6 didukung, dengan pengecualian berikut:

  • DefineClass tidak diimplementasikan. Android tidak menggunakan file class atau bytecode Java, jadi memasukkan data class biner tidak akan memberikan hasil.

Untuk kompatibilitas 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), penggunaan fungsi destruktor pthread_key_create untuk menghindari pemeriksaan "thread harus dilepaskan sebelum keluar" tidak didukung. (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, dan DeleteWeakGlobalRef. (Spesifikasi ini mendorong programmer untuk membuat referensi paksa ke weak global sebelum melakukan apa pun dengannya, jadi hal ini sama sekali tidak membatasi.)

    Mulai 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 adalah pointer langsung yang sebenarnya. Ice Cream Sandwich menambahkan pengalihan yang diperlukan untuk mendukung pembersihan 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 penjelasan selengkapnya.

    Pada versi Android sebelum Android 8.0, jumlah referensi lokal dibatasi pada batas khusus 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), implementasi GetObjectRefType secara tepat tidak dimungkinkan. Sebagai gantinya, kami menggunakan heuristik yang mencari pointer langsung di tabel weak global, argumen, tabel lokal, tabel global secara urut. Begitu menemukan pointer langsung, heuristik akan melaporkan bahwa referensi Anda merupakan jenis yang sedang diperiksanya. Ini berarti, misalnya, jika Anda memanggil GetObjectRefType pada jclass global yang kebetulan sama dengan jclass yang diteruskan sebagai argumen implisit ke metode native statis Anda, Anda akan mendapatkan JNILocalRefType, bukan JNIGlobalRefType.

FAQ: Mengapa saya menerima 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 library dan izinnya.
  • Library tidak dibuat dengan NDK. Hal ini dapat mengakibatkan dependensi pada fungsi atau library yang tidak ada pada perangkat.

Class kegagalan UnsatisfiedLinkError lainnya terlihat seperti ini:

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. Ini biasanya disebabkan oleh:
    • Untuk pencarian metode lazy, gagal mendeklarasikan fungsi C++ dengan extern "C" dan visibilitas yang sesuai (JNIEXPORT). Perhatikan bahwa sebelum Ice Cream Sandwich, makro JNIEXPORT salah, sehingga penggunaan GCC baru dengan jni.h lama tidak akan memberikan hasil. Anda dapat menggunakan arm-eabi-nm untuk melihat simbol sebagaimana yang terlihat di library; jika simbol terlihat rusak (seperti _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass, bukan Java_Foo_myfunc), atau jika jenis simbol adalah huruf kecil 't' bukan huruf besar 'T', maka Anda perlu menyesuaikan deklarasi tersebut.
    • Untuk pendaftaran eksplisit, error minor saat memasukkan tanda tangan metode. Pastikan bahwa apa yang Anda teruskan ke panggilan pendaftaran sesuai dengan tanda tangan dalam file log. Ingat bahwa 'B' adalah byte dan 'Z' adalah boolean. 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;).

Penggunaan javah untuk membuat header JNI secara otomatis dapat menghindarkan Anda dari beberapa masalah.

FAQ: Mengapa FindClass tidak menemukan class saya?

(Sebagian besar saran ini juga berlaku untuk kegagalan dalam menemukan metode dengan GetMethodID atau GetStaticMethodID, atau menemukan kolom dengan GetFieldID atau GetStaticFieldID.)

Pastikan string nama class menggunakan format yang benar. Nama class JNI dimulai dengan nama paket dan dipisahkan dengan garis miring, seperti java/lang/String. Jika mencari di class array, Anda harus memulai dengan kurung siku yang tepat dan mengapit class dengan 'L' dan ';', sehingga array satu dimensi dari String akan terlihat sebagai [Ljava/lang/String;. Jika Anda melakukan pencarian di class dalam, gunakan '$', bukan '.'. Secara umum, penggunaan javap pada file .class adalah cara yang baik untuk mengetahui nama internal class Anda.

Jika Anda menggunakan ProGuard, pastikan ProGuard tidak menghapus class Anda. Ini bisa terjadi jika class/metode/kolom Anda hanya digunakan dari JNI.

Jika nama class sudah benar, Anda masih mungkin mengalami masalah loader class. FindClass ingin memulai penelusuran class di loader class yang terkait dengan kode Anda. FindClass memeriksa stack panggilan, yang 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 bisa mengalami masalah jika membuat thread sendiri (mungkin dengan memanggil pthread_create lalu menambahkannya dengan AttachCurrentThread). Sekarang, tidak ada frame tumpukan dari aplikasi Anda. Jika Anda memanggil FindClass dari thread ini, JavaVM akan dimulai di loader class "system", bukan loader class yang terkait dengan aplikasi Anda. Jadi, upaya untuk menemukan class khusus aplikasi akan gagal.

Ada beberapa cara untuk mengatasi masalah ini:

  • Lakukan pencarian FindClass sekali, di JNI_OnLoad, lalu simpan referensi class di cache untuk digunakan nanti. Semua panggilan FindClass yang dilakukan sebagai bagian dari eksekusi JNI_OnLoad akan menggunakan loader class yang terkait dengan fungsi yang disebut System.loadLibrary (ini adalah aturan khusus, disediakan untuk mempermudah inisialisasi library). Jika kode aplikasi Anda sedang memuat library, FindClass akan menggunakan loader class yang benar.
  • Teruskan instance class ke dalam fungsi yang memerlukannya, dengan mendeklarasikan metode native yang Anda gunakan untuk menerima argumen Class, lalu meneruskan Foo.class.
  • Simpan referensi ke objek ClassLoader di cache mana pun yang mudah diakses, dan lakukan panggilan loadClass secara langsung. Proses ini memerlukan sedikit usaha.

FAQ: Bagaimana cara berbagi data mentah dengan kode native?

Anda mungkin akan menemukan situasi ketika Anda perlu mengakses buffer data mentah berukuran besar dari kode native dan juga kode terkelola. Contoh umumnya adalah manipulasi bitmap atau sampel suara. Ada dua pendekatan dasar.

Anda dapat menyimpan data dalam byte[]. Cara ini memungkinkan akses sangat cepat dari kode terkelola. Namun, pada sisi native, Anda tidak dijamin dapat mengakses data tanpa menyalinnya. Pada beberapa implementasi, GetByteArrayElements dan GetPrimitiveArrayCritical akan menampilkan pointer sebenarnya ke data mentah dalam heap terkelola, tetapi pada implementasi lainnya ia akan mengalokasikan buffer pada heap native dan menyalin datanya.

Cara lainnya adalah dengan menyimpan data dalam buffer byte langsung. Buffer byte langsung dapat dibuat dengan java.nio.ByteBuffer.allocateDirect, atau fungsi NewDirectByteBuffer JNI. Tidak seperti buffer byte biasa, penyimpanannya tidak dialokasikan pada heap terkelola, dan selalu dapat diakses langsung dari kode native (dapatkan alamatnya dengan GetDirectBufferAddress). Bergantung pada implementasi akses buffer byte langsung, mengakses data dari kode terkelola terkadang berjalan sangat lambat.

Pilihan yang akan digunakan bergantung pada dua faktor:

  1. Apakah sebagian besar akses data akan terjadi dari kode yang ditulis di Java atau di C/C++?
  2. Jika data pada akhirnya diteruskan ke sistem API, apa format yang harus digunakan? (Misalnya, jika data pada akhirnya diteruskan ke fungsi yang menerima byte [], melakukan pemrosesan di ByteBuffer langsung mungkin tidaklah tepat.)

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.