一、JNI_OnLoad简介
Java JNI有两种方法,一种是通过javah,获取一组带签名函数,然后实现这些函数。这种方法很常用,也是官方推荐的方法。
还有一种就是JNI_OnLoad方法。
当Android的VM(Virtual Machine)执行到C组件(即*so档)里的System.loadLibrary()函数时,
首先会去执行C组件里的JNI_OnLoad()函数。
它的用途有二:
. 告诉VM此C组件使用那一个JNI版本。
如果你的*.so档没有提供JNI_OnLoad()函数,VM会默认该*.so档是使用最老的JNI 1.1版本。
由于新版的JNI做了许多扩充,如果需要使用JNI的新版功能,
例如JNI 1.4的java.nio.ByteBuffer,就必须藉由JNI_OnLoad()函数来告知VM。
. 由于VM执行到System.loadLibrary()函数时,就会立即先呼叫JNI_OnLoad(),
所以C组件的开发者可以藉由JNI_OnLoad()来进行C组件内的初期值之设定(Initialization) 。
其实Android中的so文件就像是Windows下的DLL一样,JNI_OnLoad和JNI_OnUnLoad函数
就像是DLL中的PROCESS ATTATCH和DEATTATCH的过程一样,可以同样做一些初始化和反初始化的动作。
二、Android系统加载JNI Lib的方式
1. Android系统加载JNI Lib的方式
Android系统加载JNI Lib的方式有如下两种:1) 通过JNI_OnLoad
2) 如果JNI Lib没有定义JNI_OnLoad,则dvm调用dvmResolveNativeMethod进行动态解析
2. JNI_OnLoad方法
System.loadLibrary调用流程如下所示:System.loadLibrary->
Runtime.loadLibrary->(Java)
nativeLoad->(C: java_lang_Runtime.cpp)
Dalvik_java_lang_Runtime_nativeLoad->
dvmLoadNativeCode-> (dalvik/vm/Native.cpp)
1) dlopen(pathName, RTLD_LAZY) (把.so mmap到进程空间,并把func等相关信息填充到soinfo中)
2) dlsym(handle, "JNI_OnLoad")
3) JNI_OnLoad->
RegisterNatives->
dvmRegisterJNIMethod(ClassObject* clazz, const char* methodName,
const char* signature, void* fnPtr)->
dvmUseJNIBridge(method, fnPtr)-> (method->nativeFunc = func)
JNI函数在进程空间中的起始地址被保存在ClassObject->directMethods中。
struct ClassObject : Object {
/* static, private, and <init> methods */
int directMethodCount;
Method* directMethods;
/* virtual methods defined in this class; invoked through vtable */
int virtualMethodCount;
Method* virtualMethods;
}
此ClassObject通过gDvm.jniGlobalRefTable或gDvm.jniWeakGlobalRefLock获取。
3. dvmResolveNativeMethod延迟解析机制
如果JNI Lib中没有JNI_OnLoad,即在执行System.loadLibrary时,无法把此JNI Lib实现的函数在进程中的地址增加到ClassObject->directMethods。
则直到需要调用的时候才会解析这些javah风格的函数 。
这样的函数dvmResolveNativeMethod(dalvik/vm/Native.cpp)来进行解析,
其执行流程如下所示:
void dvmResolveNativeMethod(const u4* args, JValue* pResult,
const Method* method, Thread* self) --> (Resolve a native method and invoke it.)
1) void* func = lookupSharedLibMethod(method)(根据signature在所有已经打开的.so中寻找此函数实现)
dvmHashForeach(gDvm.nativeLibs, findMethodInLib,(void*) method)->
findMethodInLib(void* vlib, void* vmethod)->
dlsym(pLib->handle, mangleCM)
2) dvmUseJNIBridge((Method*) method, func);
3) (*method->nativeFunc)(args, pResult, method, self); (调用执行)
三、应用实例
1. 成功调用JNI的实例
调用的jdk为android 2.1源码下的 jdk1.5.0_22的文件夹1.1 静态方式:
1,首先,在android根目录建立test 目录,在test目录下再建立test目录,进入2,进入test目录后vim HelloWorld.java
//HelloWorld.java:
package test;
public class HelloWorld {
public static void main(String[] args){
System.loadLibrary("HelloWorld");
printHello();
}
public static native final void printHello();
}
3,退出test目录,键入命令:
$ ../jdk1.5.0_22/bin/javac test/HelloWorld.java
test目录下将生成HelloWorld.class
4,键入:
$ ../jdk1.5.0_22/bin/javah -o test/Hello.h test.HelloWorld
test目录下将生成Hello.h
5,在test目录下创建HelloWorld.cpp文件
//HelloWorld.cpp:
#include "Hello.h"
#include <cstdio>
Void Java_test_HelloWorld_printHello(JNIEnv *, jclass) {
printf("helloworld");
}
6,退出test目录,键入:
$ g++ test/HelloWorld.cpp -I ../jdk1.5.0_22/include/ -I ../jdk1.5.0_22/include/linux/ -fPIC -shared -o test/libHelloWorld.so
test目录下将会生成libHelloWorld.so
7,运行
$ ../jdk1.5.0_22/bin/java test.HelloWorld
屏幕上会打印出helloworld.
出错情况:
1,如果g++ test/HelloWorld.cpp -I ../jdk1.5.0_22/include/ -fPIC -shared -o test/libHelloWorld.so
虽然jni.h在../jdk1.5.0_22/include/,但是如果不加上这边的目录-I ../jdk1.5.0_22/include/linux/的话,
还是会报一大堆错误的。
2,如果运行的时候../jdk1.5.0_22/bin/java test.HelloWorld有
java.lang.UnsatisfiedLinkError: no HelloWorld in library path
at java.lang.Runtime.loadLibrary(Runtime.java)
at java.lang.System.loadLibrary(System.java)
at HelloWorld.main(HelloWorld.java)
类似的错误,那么就是因为LD_LIBRARY_PATH没有设置正确,用
LD_LIBRARY_PATH=test
export LD_LIBRARY_PATH
在运行一下就可以了。
参考自:
http://java.sun.com/docs/books/jni/html/start.html
1.2 动态方式:
关于动态方式,java上层的调用和静态方式是相同的,关键是native层的调用有所不同,主要关键在于三个地方:1, 定义调用的JNINativeMethod
2, 定义调用挂钩的函数
3, 实现JNI_OnLoad函数
JNI_OnLoad是java jni技术的一个实现,每次java层加载System.loadLibrary之后,
自动会查找改库一个叫JNI_OnLoad的函数,动态注册的时候,cpp可以通过实现JNI_OnLoad而完成jni的动态注册。
1,建立dynamic文件夹,进入,把静态连接的HelloWorld.java拷贝进来,并且修改package test;为package dynamic;
2,建立HelloWorld.cpp
#include "jni.h"
#include <cstdio>
// java转到native层的对应函数
static int android_print(JNIEnv * env, jclass clazz){
printf("helloworld");
}
// 结构体,分别是java层的函数名称,签名,对应的函数指针
static JNINativeMethod gMethods[] ={
{"printHello", "()V", (void*)android_print},
};
// JNI_OnLoad函数实现
jint JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
/*
* 如果要注册,只需要两步,
* 首先FindClass,
* 然后RegisterNatives
*/
char className[20] = {"dynamic/HelloWorld"};
jclass clazz = (env)->FindClass( (const char*)className);
if((env)->RegisterNatives(clazz, gMethods, 1)< 0) {
return -1;
}
//一定要返回版本号,否则会出错。
result = JNI_VERSION_1_4;
return result;
}
然后编译一下:
编译java
$ ../jdk1.5.0_22/bin/javac dynamic/HelloWorld.java
编译cpp生成.so
$ g++ dynamic/HelloWorld.cpp -I ../jdk1.5.0_22/include/ -I ../jdk1.5.0_22/include/linux/ -fPIC -shared -o dynamic /libHelloWorld.so
dynamic目录下将会生成libHelloWorld.so
设置LD_LIBRARY_PATH环境变脸
运行Java
$ ../jdk1.5.0_22/bin/java dynamic.HelloWorld
屏幕上会打印出helloworld
2. 实现动态的函数替换
JNI_OnLoad可以和JNIEnv的registerNatives函数结合起来,实现动态的函数替换。下面用一个简单的例子来说明
2.1 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中的了。
2.2 ni1和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");
}
....
2.3 JNI_OnLoad的使用方法
先看一下jni2.c的完整源代码,并注意注释#include <stdio.h>
#include <stdlib.h>
#include <jni.h> //jni的主要头文件
//函数名字可以随便取,不过参数一定要和javah生成的函数的参数一致,包括返回值
static void JNICALL func2 (JNIEnv *env, jobject jobj)
{
printf("--- func2 called in version 2\n");
}
//定义批量注册的数组,是注册的关键部分
static const JNINativeMethod gMethods[] = {
{"func2", /* func2是在java中声明的native函数名 */
"()V", /* "()V"是函数的签名,可以通过javah获取。*/
(void*)func2
} ,
};
//这是JNI_OnLoad的声明,必须按照这样的方式声明
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void *reserved)
{
JNIEnv* env = NULL; //注册时在JNIEnv中实现的,所以必须首先获取它
jint result = -1;
//从JavaVM获取JNIEnv,一般使用1.4的版本
if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_4) != JNI_OK)
return -1;
jclass clazz;
static const char* const kClassName="mj/jnitest/MyObject";
/*
* 这里可以找到要注册的类,前提是这个类已经加载到java虚拟机中。
* 这里说明,动态库和有native方法的类之间,没有任何对应关系。
*/
clazz = (*env)->FindClass(env, kClassName);
if(clazz == NULL) {
printf("cannot get class:%s\n", kClassName);
return -1;
}
/*
* 这里就是关键了,把本地函数和一个java类方法关联起来。
不管之前是否关联过,一律把之前的替换掉!
*/
if((*env)->RegisterNatives(env, clazz,gMethods, sizeof(gMethods)/sizeof(gMethods[0]))!= JNI_OK)
{
printf("register native method failed!\n");
return -1;
}
//这里很重要,必须返回版本,否则加载会失败。
return JNI_VERSION_1_4;
}
对他进行编译后,得到一个libjni2.so。
2.4 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]))
2.5 Dalvik中动态库的原理简要分析
之所以出现这种结果,和jni的机制有关的,通过对Android中的Dalvik的分析,可以印证。System.loadLibrary,也是一个native方法,它向下调用的过程是:
Dalvik/vm/native/java_lang_Runtime.cpp: Dalvik_java_lang_Runtime_nativeLoad ->
Dalvik/vm/Native.cpp:dvmLoadNativeCode
dvmLoadNativeCode
打开函数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变量,从而和其他进行区分开。
2.6 利用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即可实现用新的方法来替换老的方法。
当然,这是仅对当前进程有效,不影响其他进程。