加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

JNI系列之JNI_OnLoad

(2019-02-26 09:18:53)
标签:

教育

分类: 上层开发

一、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的函数来告知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->(
nativeLoad->(C:
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 {

int             directMethodCount;
Method*         directMethods;


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

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
at
at HelloWorld.main(HelloWorld.
类似的错误,那么就是因为LD_LIBRARY_PATH没有设置正确,用
LD_LIBRARY_PATH=test
export LD_LIBRARY_PATH
在运行一下就可以了。
参考自:
http://; font-family:" sans="" line-height:="" text-indent:="" />

1.2 动态方式:

关于动态方式,; font-family:" sans="" line-height:="" text-indent:="" /> 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

// 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;
}


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
#include
#include "mj_jnitest_MyObject.h"


JNIEXPORT void JNICALL Java_mj_jnitest_MyObject_func1
(JNIEnv *env, jobject jobj)
{
printf("--- func1 called in version 1\n");
}


JNIEXPORT void JNICALL Java_mj_jnitest_MyObject_func2
(JNIEnv *env, jobject jobj)
{
printf("--- func2 called in version 1\n");
}

jni2的源代码jni2.c(部分)
include
#include

static void JNICALL func2(JNIEnv *env, jobject jobj)
{
printf("--- func2 called in version 2\n");
}

....

2.3 JNI_OnLoad的使用方法

先看一下jni2.c的完整源代码,并注意注释
#include
#include

#include //jni的主要头文件

//函数名字可以随便取,不过参数一定要和javah生成的函数的参数一致,包括返回值
static void JNICALL func2  (JNIEnv *env, jobject jobj)
{


printf("--- func2 called in version 2\n");
}

//定义批量注册的数组,是注册的关键部分
static const JNINativeMethod gMethods[] = {
{"func2",        
"()V",          
(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";


clazz = (*env)->FindClass(env, kClassName);
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)
{
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 {

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);

result = false;
} else {
if (gDvm.verboseJni) {
LOGI("[Returned from JNI_OnLoad for "%s"]", pathName);
}
}

上面的代码说明,JNI_OnLoad是一种更加灵活,而且处理及时的机制。

用; font-family:" sans="" line-height:="" text-indent:="" /> 这样的函数,是dvmResolveNativeMethod(dalvik/vm/Native.cpp)

dvmResolveNativeMethod
dvmResolveNativeMethod是在一种延迟解析机制,它的代码是
void dvmResolveNativeMethod(const u4* args, JValue* pResult,
const Method* method, Thread* self)
{
ClassObject* clazz = method->clazz;


if (dvmIsStaticMethod(method)) {
if (!dvmIsClassInitialized(clazz) && !dvmInitClass(clazz)) {
assert(dvmCheckException(dvmThreadSelf()));
return;
}
} else {
assert(dvmIsClassInitialized(clazz)
dvmIsClassInitializing(clazz));
}


DalvikNativeFunc infunc = dvmLookupInternalNativeMethod(method);
if (infunc != NULL) {

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;
}



void* func = lookupSharedLibMethod(method); //注意到,这里,是获取地址的地方
if (func != NULL) {

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);



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

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有