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即可实现用新的方法来替换老的方法。当然,这是仅对当前进程有效,不影响其他进程。