Java JNI有兩種方法,一種是通過javah,獲取一組帶簽名函數,然後實現這些函數。這種方法很常用,也是官方推薦的方法,本文不再詳述,重點說明一下JNI_OnLoad方法。
當在系統中調用System.loadLibrary函數時,該函數會找到對應的動態庫,然後首先試圖找到"JNI_OnLoad"函數,如果該函數存在,則調用它。
JNI_OnLoad可以和JNIEnv的registerNatives函數結合起來,實現動態的函數替換。
下面用一個簡單的例子來說明
java類聲明
創建目錄mj/jnitest,並新建兩個文件MyObject.java和JniTest.java
MyObject.java
-
package mj.jnitest;
-
-
class MyObject {
-
-
static {
-
-
System.loadLibrary("jni"); //這是加載使用javah規定風格實現的庫
-
-
}
-
-
//下面定義兩個native函數
-
-
public native void func1();
-
-
public native void func2();
-
-
}
複製代碼
JniTest.java
-
package mj.jnitest;
-
-
class JniTest {
-
-
// static { //這是一種靜態的加載方式,可以完全工作;但是下面我們要用更靈活的方式進行
-
-
// System.loadLibrary("jni2");
-
-
// }
-
-
public static void main(String[] args) {
-
-
MyObject obj = new MyObject();
-
-
//在fun2函數替換之前,先進行一次調用,會調研jni1中的函數
-
-
obj.func1();
-
-
obj.func2();
-
-
//用JNI_OnLoad進行主動註冊
-
-
System.loadLibrary("jni2");
-
-
obj.func1();
-
-
obj.func2(); //func2已經被jni2中的函數替換
-
-
}
-
-
};
複製代碼
在JniTest.java中,有兩個動態庫jni1和jni2會被同時加載。jni1在MyObject類被鏈接時被加載;jni2則在MyObject的實例obj運行時被加載。首先看看他的輸出結果:$ java mj.jnitest.JniTest--- func1 called in version 1--- func2 called in version 1--- func1 called in version 1--- func2 called in version 2從結果看出,前兩行調用obj.func1和obj.func2,都是jni1中的函數,所以打印的是version 1;而加載了jni2後,obj.func1函數仍舊是jni1中的,而func2就變成了jni2中的了。下面看下jni1和jni2的源代碼jni1的源代碼mj_jnitest_MyObject.c
-
#include <stdio.h>
-
#include <stdlib.h>
-
-
#include "mj_jnitest_MyObject.h"
-
-
/*
-
* Class: mj_jnitest_MyObject
-
* Method: func1
-
* Signature: ()V
-
*/
-
JNIEXPORT void JNICALL Java_mj_jnitest_MyObject_func1
-
(JNIEnv *env, jobject jobj)
-
{
-
printf("--- func1 called in version 1\n");
-
}
-
-
/*
-
* Class: mj_jnitest_MyObject
-
* Method: func2
-
* Signature: ()V
-
*/
-
JNIEXPORT void JNICALL Java_mj_jnitest_MyObject_func2
-
(JNIEnv *env, jobject jobj)
-
{
-
printf("--- func2 called in version 1\n");
-
}
-
複製代碼
斜體部分正是打印的內容jni2的源代碼jni2.c(部分)include <stdlib.h>
-
#include <jni.h>
-
-
static void JNICALL func2
-
(JNIEnv *env, jobject jobj)
-
{
-
printf("--- func2 called in version 2\n");
-
}
複製代碼
....JNI_OnLoad的使用方法先看一下jni2.c的完整源代碼,並注意註釋
-
#include <stdio.h>
-
#include <stdlib.h>
-
-
#include <jni.h> //jni的主要頭文件
-
-
static void JNICALL func2 //函數名字可以隨便取,不過參數一定要和javah生成的函數的參數一致,包括返回值
-
(JNIEnv *env, jobject jobj)
-
{
-
printf("--- func2 called in version 2\n");
-
}
-
-
static const JNINativeMethod gMethods[] = { //定義批量註冊的數組,是註冊的關鍵部分
-
{"func2", "()V", (void*)func2} // func2是在java中聲明的native函數名,"()V"是函數的簽名,可以通過javah獲取。
-
};
-
-
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void *reserved) //這是JNI_OnLoad的聲明,必須按照這樣的方式聲明
-
{
-
JNIEnv* env = NULL; //註冊時在JNIEnv中實現的,所以必須首先獲取它
-
jint result = -1;
-
-
if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_4) != JNI_OK) //從JavaVM獲取JNIEnv,一般使用1.4的版本
-
return -1;
-
-
jclass clazz;
-
static const char* const kClassName="mj/jnitest/MyObject";
-
-
clazz = (*env)->FindClass(env, kClassName); //這裏可以找到要註冊的類,前提是這個類已經加載到java虛擬機中。 這裏說明,動態庫和有native方法的類之間,沒有任何對應關係。
-
-
if(clazz == NULL)
-
{
-
printf("cannot get class:%s\n", kClassName);
-
return -1;
-
}
-
-
if((*env)->RegisterNatives(env, clazz,gMethods, sizeof(gMethods)/sizeof(gMethods[0]))!= JNI_OK) //這裏就是關鍵了,把本地函數和一個java類方法關聯起來。不管之前是否關聯過,一律把之前的替換掉!
-
{
-
printf("register native method failed!\n");
-
return -1;
-
}
-
-
return JNI_VERSION_1_4; //這裏很重要,必須返回版本,否則加載會失敗。
-
}
複製代碼
對他進行編譯後,得到一個libjni2.so。C++用法說明上面的用法是c語言中的用法,在C++中更簡單。JavaVM和JNIEnv都是經過簡單封裝的類,可以直接按照如下方式調用(vm->GetEnv((void**)&env, JNI_VERSION_1_4)env->FindClass(kClassName);env->RegisterNatives(clazz,gMethods, sizeof(gMethods)/sizeof(gMethods[0]))Dalvik中動態庫的原理簡要分析之所以出現這種結果,和jni的機制有關的,通過對Android中的Dalvik的分析,可以印證。System.loadLibrary,也是一個native方法,它向下調用的過程是:Dalvik/vm/native/java_lang_Runtime.cpp: Dalvik_java_lang_Runtime_nativeLoad -> Dalvik/vm/Native.cpp:dvmLoadNativeCodedvmLoadNativeCode打開函數dvmLoadNativeCode,可以找到以下代碼
-
bool result = true;
-
void* vonLoad;
-
int version;
-
-
vonLoad = dlsym(handle, "JNI_OnLoad"); //獲取JNI_OnLoad的地址
-
if (vonLoad == NULL) { //這是用javah風格的代碼了,推遲解析
-
LOGD("No JNI_OnLoad found in %s %p, skipping init",
-
pathName, classLoader);
-
} else {
-
/*
-
* Call JNI_OnLoad. We have to override the current class
-
* loader, which will always be "null" since the stuff at the
-
* top of the stack is around Runtime.loadLibrary(). (See
-
* the comments in the JNI FindClass function.)
-
*/
-
OnLoadFunc func = (OnLoadFunc)vonLoad;
-
Object* prevOverride = self->classLoaderOverride;
-
-
self->classLoaderOverride = classLoader;
-
oldStatus = dvmChangeStatus(self, THREAD_NATIVE);
-
if (gDvm.verboseJni) {
-
LOGI("[Calling JNI_OnLoad for \"%s\"]", pathName);
-
}
-
version = (*func)(gDvmJni.jniVm, NULL); //調用JNI_OnLoad,並獲取返回的版本信息
-
dvmChangeStatus(self, oldStatus);
-
self->classLoaderOverride = prevOverride;
-
-
if (version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 &&
-
version != JNI_VERSION_1_6) //對版本進行判斷,這是爲什麼要返回正確版本的原因
-
{
-
LOGW("JNI_OnLoad returned bad version (%d) in %s %p",
-
version, pathName, classLoader);
-
/*
-
* It's unwise to call dlclose() here, but we can mark it
-
* as bad and ensure that future load attempts will fail.
-
*
-
* We don't know how far JNI_OnLoad got, so there could
-
* be some partially-initialized stuff accessible through
-
* newly-registered native method calls. We could try to
-
* unregister them, but that doesn't seem worthwhile.
-
*/
-
result = false;
-
} else {
-
if (gDvm.verboseJni) {
-
LOGI("[Returned from JNI_OnLoad for \"%s\"]", pathName);
-
}
-
}
-
-
上面的代碼說明,JNI_OnLoad是一種更加靈活,而且處理及時的機制。
-
-
用javah風格的代碼,則推遲解析,直到需要調用的時候纔會解析。這樣的函數,是dvmResolveNativeMethod(dalvik/vm/Native.cpp)
-
-
dvmResolveNativeMethod
-
dvmResolveNativeMethod是在一種延遲解析機制,它的代碼是
-
-
void dvmResolveNativeMethod(const u4* args, JValue* pResult,
-
const Method* method, Thread* self)
-
{
-
ClassObject* clazz = method->clazz;
-
-
/*
-
* If this is a static method, it could be called before the class
-
* has been initialized.
-
*/
-
if (dvmIsStaticMethod(method)) {
-
if (!dvmIsClassInitialized(clazz) && !dvmInitClass(clazz)) {
-
assert(dvmCheckException(dvmThreadSelf()));
-
return;
-
}
-
} else {
-
assert(dvmIsClassInitialized(clazz) ||
-
dvmIsClassInitializing(clazz));
-
}
-
-
/* start with our internal-native methods */
-
DalvikNativeFunc infunc = dvmLookupInternalNativeMethod(method);
-
if (infunc != NULL) {
-
/* resolution always gets the same answer, so no race here */
-
IF_LOGVV() {
-
char* desc = dexProtoCopyMethodDescriptor(&method->prototype);
-
LOGVV("+++ resolved native %s.%s %s, invoking",
-
clazz->descriptor, method->name, desc);
-
free(desc);
-
}
-
if (dvmIsSynchronizedMethod(method)) {
-
LOGE("ERROR: internal-native can't be declared 'synchronized'");
-
LOGE("Failing on %s.%s", method->clazz->descriptor, method->name);
-
dvmAbort(); // harsh, but this is VM-internal problem
-
}
-
DalvikBridgeFunc dfunc = (DalvikBridgeFunc) infunc;
-
dvmSetNativeFunc((Method*) method, dfunc, NULL);
-
dfunc(args, pResult, method, self);
-
return;
-
}
-
-
/* now scan any DLLs we have loaded for JNI signatures */
-
void* func = lookupSharedLibMethod(method); //注意到,這裏,是獲取地址的地方
-
if (func != NULL) {
-
/* found it, point it at the JNI bridge and then call it */
-
dvmUseJNIBridge((Method*) method, func);
-
(*method->nativeFunc)(args, pResult, method, self);
-
return;
-
}
-
-
IF_LOGW() {
-
char* desc = dexProtoCopyMethodDescriptor(&method->prototype);
-
LOGW("No implementation found for native %s.%s %s",
-
clazz->descriptor, method->name, desc);
-
free(desc);
-
}
-
-
dvmThrowUnsatisfiedLinkError(method->name);
-
}
-
複製代碼
lookupSharedLibMethod函數會調用到函數findMethodInLib,當然,不是直接調用,有興趣的可以參考具體源碼。findMethodInLib是實現解析的:static int findMethodInLib(void* vlib, void* vmethod){ const SharedLib* pLib = (const SharedLib*) vlib; const Method* meth = (const Method*) vmethod; char* preMangleCM = NULL; char* mangleCM = NULL; char* mangleSig = NULL; char* mangleCMSig = NULL; void* func = NULL; int len; if (meth->clazz->classLoader != pLib->classLoader) { LOGV("+++ not scanning '%s' for '%s' (wrong CL)", pLib->pathName, meth->name); return 0; } else LOGV("+++ scanning '%s' for '%s'", pLib->pathName, meth->name); /* * First, we try it without the signature. */ preMangleCM = createJniNameString(meth->clazz->descriptor, meth->name, &len); if (preMangleCM == NULL) goto bail; mangleCM = mangleString(preMangleCM, len); //這裏,把java的native方法的名字進行轉換,生成和javah一致的名字 if (mangleCM == NULL) goto bail; LOGV("+++ calling dlsym(%s)", mangleCM); func = dlsym(pLib->handle, mangleCM); if (func == NULL) { mangleSig = createMangledSignature(&meth->prototype); if (mangleSig == NULL) goto bail; mangleCMSig = (char*) malloc(strlen(mangleCM) + strlen(mangleSig) +3); if (mangleCMSig == NULL) goto bail; sprintf(mangleCMSig, "%s__%s", mangleCM, mangleSig); LOGV("+++ calling dlsym(%s)", mangleCMSig); func = dlsym(pLib->handle, mangleCMSig); //dlsym清晰的表明,這裏纔是獲取符號的地方。 if (func != NULL) { LOGV("Found '%s' with dlsym", mangleCMSig); } } else { LOGV("Found '%s' with dlsym", mangleCM); }bail: free(preMangleCM); free(mangleCM); free(mangleSig); free(mangleCMSig); return (int) func;}實際上,無論是那種方式,從vm的代碼中,都可以看出,這些符號可以放在任意的動態庫中,只要確保他們調用了System.loadLibrary即可。JNI_OnLoad函數,可以通過registerNatives,在任意時刻替換。VM把native函數指針通過JNI Bridge,放到一個Method結構中,這個Method結構,最終會放在struct DvmGlobals gDvm;這個全局變量中。由於是普通的全局變量,在java獨立進程中保存,一旦該全局變量被修改,linux的copy-on-write機制啓動,就會形成一個該進行獨有的一個gDvm變量,從而和其他進行區分開。利用JNI_OnLoad替換WebCore模塊在Android的WebViewCore類裏,靜態加載了
-
static {
-
// Load libwebcore and libchromium_net during static initialization.
-
// This happens in the zygote process so they will be shared read-only
-
// across all app processes.
-
try {
-
System.loadLibrary("webcore");
-
System.loadLibrary("chromium_net");
-
} catch (UnsatisfiedLinkError e) {
-
Log.e(LOGTAG, "Unable to load native support libraries.");
-
}
-
}
複製代碼
注意到紅字部分的說明,Android通過zygote進程,來孵化每個新啓動的進程。Android爲了加快啓動速度,把一些重要的類都放在了preloaded-classes中,這個列表,可以在Android源碼的frameworks/base/preloaded-classes中找到,也可以在frameworks.jar包中找到,就在最上層。而webkit相關的類,也在這個proloaded-classes的列表中。它意味着,在android系統啓動時,這些類就都會被加載到系統中。但是,通過JNI_OnLoad機制,在瀏覽器的主Activiy中,只要加入
-
static {
-
-
System.loadLibrary("mxwebcore");
-
-
}
複製代碼
VM即可實現用新的方法來替換老的方法。當然,這是僅對當前進程有效,不影響其他進程。