本文基于Android 9.0源碼分析
Android JNI簡介
JNI是Java Native Interface, 它提供了一種從字節碼(Java/Kotlin)到Native代碼(c/c++/assembly)的交互方式
JavaVM與JNIEnv
JNI定義了兩個關鍵的數據結構:JavaVM和JNIEnv
- JavaVM
- JavaVM提供了"invocation interface"函數表,允許你創建和銷毀JavaVM,理論上你可以在進程內創建多個JavaVM實例,但Android只允許創建一個。
- JNIEnv
- JNIEnv提供了大部分JNI方法,JNIEnv存儲在TLS中,基于上述原因,不能在線程間共享JNIEnv。上面的圖中JNINativeInterface實際是全局結構體,圖有點問題。
- 線程
- Android中的線程都是Linux線程,由Linux內核調度。通常通過
Thread.start()
啟動線程,但是也可以使用其他方法啟動線程,然后attach到JavaVM。比如,使用pthread_create()
創建線程,調用AttachCurrentThread()
或者AttachCurrentThreadAsDaemon()
attach到JavaVM。注意,在attach之前,沒有JNIEnv,無法使用JNI。
- Android中的線程都是Linux線程,由Linux內核調度。通常通過
Android Native方法注冊
使用RegisterNatives
顯示注冊
示例
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
......
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
// 1) using RegisterNatives register native method
static jstring StringFromJNI(JNIEnv *env, jobject obj) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
static JNINativeMethod jni_methods[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void *)StringFromJNI}
};
// 這里JNI_OnLoad不需要添加extern "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;
}
if (env->RegisterNatives(env->FindClass("lbtrace/jniregister/MainActivity"),
jni_methods, sizeof(jni_methods) / sizeof(JNINativeMethod)) != JNI_OK)
return -1;
return JNI_VERSION_1_6;
}
原理
-
首先獲取調用者的類加載(這里是應用類加載器),然后在類加載器的DexPathList中查找Native庫
- 根據Native庫的名字產生平臺相關的庫名
- 在DexPathList的Native庫路徑列表中查找Native庫,找到后返回庫的絕對路徑
JavaVM負責加載Native庫,如果沒有加載過,裝載Native庫到進程地址空間,然后查找Native庫中是否有函數
JNI_OnLoad()
,如果存在,執行;否則運行時動態查找Native方法。-
在
JNI_OnLoad()
中,首先獲取當前線程的JNIEnv,然后調用其RegisterNatives()
注冊Native方法。- 首先檢查需要注冊Native方法的信息是否合法
- 根據方法名、方法簽名查找對應的Native方法的ArtMethod
- 將Native函數地址寫入ArtMethod對象特定的成員中
運行時動態查找
示例
// 2) Android Runtime dynamic find native method
// 必須添加extern "C"
extern "C" JNIEXPORT jstring JNICALL
Java_lbtrace_jniregister_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
原理
- ART運行時執行到調用Native方法的字節碼時,首先獲取方法對應的ArtMethod,根據ArtMethod對象的地址以及成員entry_point_from_quick_compiled_code_的偏移得到JNI Stub的地址。
- 在JNI Stub函數中,首先進行Native方法調用前的準備工作(參數處理、進程狀態切換等),然后根據特定的Native方法名(java_xxx_xxx_xxx)查找Native方法,調用Native方法,Native方法地址保存到ArtMethod,最后是Native方法調用結束后的清理工作。
思考:Native方法中jobject類型的參數是什么?
在先前的Android版本中是Local Reference
- StackReference
Android JNI本地引用和全局引用
對于基本類型,進行JNI調用時,可以直接拷貝到Native方法,但是對于Java對象采用引用傳遞。虛擬機必須能夠追蹤到所有傳遞到Native方法的Java對象,避免被GC回收。當Native方法不再需要Java對象時,必須有一種方法通知虛擬機。
對于本地引用及全局引用的實現細節,將在后續文章中討論。
Local Reference
本地引用在Native方法調用期間可用,在Native方法返回后自動釋放。
- 對于大的Java對象的本地引用,不用時應及時釋放
- 不要創建太多本地應用
Global Reference
全局引用不會自動釋放,直到顯式的刪除
Weak Global Reference
弱全局引用是一種特殊的全局引用,與普通的全局引用不同的是,GC可以回收弱全局引用關聯的Java對象。當GC運行時,會回收一個僅僅被弱全局引用關聯Java對象。所以在使用之前,應該首先檢查弱全局引用的Java對象是否被回收。
思考:Android中Java引用到底是什么?
- 根據Java引用原理,可以在Native層直接修改Java對象內容
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
......
TestObject testObject = new TestObject();
Log.i("JNI", "Before : " + testObject.month + "月" +
testObject.day + "日");
stringFromJNI(testObject);
Log.i("JNI", "After : " + testObject.month + "月" +
testObject.day + "日");
......
}
public native String stringFromJNI(Object obj);
static class TestObject {
int month = 11;
int day = 29;
}
}
extern "C" JNIEXPORT jstring JNICALL
Java_lbtrace_jniregister_MainActivity_stringFromJNI(
JNIEnv* env,
jobject obj, jobject test_obj) {
std::string hello = "Hello from C++";
// Java TestObject Mirror address
int32_t *test_obj_ptr = reinterpret_cast<int32_t *>(
*(reinterpret_cast<int32_t *>(test_obj)));
// According TestObject memory layout
// Swap month field and day field in TestObject
int32_t tmp = *(test_obj_ptr + 2);
*(test_obj_ptr + 2) = *(test_obj_ptr + 3);
*(test_obj_ptr + 3) = tmp;
return env->NewStringUTF(hello.c_str());
}