JNI专题1

Android系统源码分析-JNI


因为在接下来的源码分析中将涉及大量的Java和Native的互相调用。当然对于我们的代码分析没有什么影响,但是,这样一个黑盒子摆在面前,对于其实现原理还是充满了好奇心。本篇将从JNI最基本的概念到简单的代码实例和其实现原理逐步展开。序言

1.JNI

JNI(Java Native Interface,Java本地接口)是一种编程框架使得Java虚拟机中的Java程序可以调用本地应用/或库,也可以被其他程序调用。 本地程序一般是用其它语言C,C++或汇编语言编写的, 并且被编译为基于本机硬件和操作系统的程序。在Android平台,为了更方便开发者的使用和增强其功能性,Android提供了NDK来更方便开发者的开发。

2.为什么要有JNI?

JNI允许程序员用其他编程语言来解决用纯粹的Java代码不好处理的情况, 例如, Java标准库不支持的平台相关功能或者程序库。也用于改造已存在的用其它语言写的程序, 供Java程序调用。许多基于JNI的标准库提供了很多功能给程序员使用, 例如文件I/O、音频相关的功能。当然,也有各种高性能的程序,以及平台相关的API实现, 允许所有Java应用程序安全并且平台独立地使用这些功能。Java层可以用来负责UI功能实现,而C++负责进行计算操作。

JNI框架允许Native方法调用Java对象,就像Java程序访问Native对象一样方便。Native方法可以创建Java对象,读取这些对象, 并调用Java对象执行某些方法。当然Native方法也可以读取由Java程序自身创建的对象,并调用这些对象的方法。

3.Hello World

这里,我们先通过一个简单的Hello World实例来对JNI的调用流程有一个直观的印象,然后针对其中的实现原理和细节做分析。

在Java文件中定义native函数

在此方法声明中,使用 native 关键字的作用是告诉虚拟机,函数位于共享库中(即在原生端实现)。

private native String helloWorld();

利用Javah生成头文件

对于native方法的命名规则,函数名根据以下规则构建:

在名称前面加上 Java_。

描述与顶级源目录相关的文件路径。

使用下划线代替正斜杠。

删掉 .java 文件扩展名。

在最后一个下划线后,附加函数名。

按照这些规则,此示例使用的函数名为Java_com_example_hellojni_HelloJni_stringFromJNI。 此名称描述hellojni/src/com/example/hellojni/HelloJni.java中一个名为 stringFromJNI()的 Java 函数。我们想通过更简单的方式,让写native函数如同和写java函数没有这一步的转化,那么可以通过javah来实现。

javah -d ../jni -jni com.chenjensen.myapplication.MainActivity

d :头文件输出目录

jni:生成jni文件

根据Javah生成的头文件,实现相应的native函数

JNIEXPORT jstring JNICALL 

Java_com_chenjensen_myapplication_MainActivity_helloWorld

(JNIEnv *, jobject)

;

头文件中生成了我们的java文件中定义的native方法,也做好了类型转化,我们只需要新建一个cpp文件来实现相应的方法即可。

cpp文件

JNIEXPORT jstring JNICALL

Java_com_chenjensen_myapplication_MainActivity_helloWorld

(JNIEnv *env, jobject)

{

char *str = "Hello world";

return (*env).NewStringUTF(str);

}

build文件中编译支持指定的平台(arm,x86等)

ndk {

 moduleName "hello"  

abiFilters "armeabi", "armeabi-v7a", "x86" }

这里指定了生成so文件的name之后,编译系统就会从JNI目录下去寻找相应的c/cpp文件,来生成相应的so文件。

执行

在Java代码中,native方法的执行之前,要提前加载相应的动态库,然后才可以执行,一般会在该类中通过静态代码块的方式来加载。应用启动时,调用此函数以加载 .so 文件。

static {

System.loadLibrary("hello");

}

这个时候,我们在Java代码中调用相应的native代码就会生效了。

那么在C/C++文件中如何调用Java呢,这里的调用方式和Java中通过反射查找一个类的调用相似。核心函数为以下几个。

FindClass(), 

NewObject(), 

GetStaticMethodID(), 

GetMethodID(), 

CallStaticObjectMethod(),

CallVoidMethod()

找到相应的类,相应的方法,调用相应的类和方法。这里不在给出具体的代码示例。可参考文章末尾给出的相应链接。

4.如何调用

通过上述6个步骤,我们便实现了Java调用native函数,借助了相应的工具,我们可以很快的实现其互相调用,但是,工具也屏蔽掉了大量的实现细节,让这个过程变成黑盒,不了解其实现。这个过程中,

当JVM调用这些函数,传递了一个JNIEnv指针,一个jobject的指针,任何在Java方法中声明的Java参数。

一个JNI函数看起来类似这样:

JNIEXPORT void JNICALL Java_ClassName_MethodName

(JNIEnv *env, jobject obj)

{

}

Java和C++之间的调用,Java的执行需要在JVM上,因此在调用的时候,JVM必须知道要调用那一个本地函数,本地函数调用Java的时候,也必须要知道应用对象和具体的函数。

JNI中C++和Java的执行是在同一个线程,但是其线程值是不相同的。

JNIEnv是JNI的使用环境,JNIEnv对象是和线程绑定在一起的,在进行调用的时候,会传递一个JavaVM的指针作为参数,然后通过JavaVM的getEnv函数得到JNIEnv对象的指针。在Java中每次创建一个线程,都会生成新的JNIEnv对象。

在分析系统源码的时候,我们可以看到很多的java对于native的调用,通过对于源码的分析,我们发现在系统开机之后,就会有许多的Service进程被启动,这个时候,而其很多实现都是通过native来实现的,这个时候如何调用,让我们回归到系统的启动过程中。在Zygote进程中首先会调用启动VM。

if (startVm(&mJavaVM, &env, zygote) != 0) {

 return;

}

onVmCreated(env);if (startReg(env) < 0) {

return;

}

int AndroidRuntime::startReg(JNIEnv* env)

{

if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {

env->PopLocalFrame(NULL);

return -1;

}

....

return 0;

}

static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env){

for (size_t i = 0; i < count; i++) {

if (array[i].mProc(env) < 0) {

return -1;

}

}

return 0;

}

static const RegJNIRec gRegJNI[] = {

REG_JNI(register_com_android_internal_os_RuntimeInit),

REG_JNI(register_android_os_SystemClock),

REG_JNI(register_android_util_EventLog),

REG_JNI(register_android_util_Log),

.....

}

array[i]是指gRegJNI数组, 该数组有100多个成员。其中每一项成员都是通过REG_JNI宏定义。

#define REG_JNI(name){ 

name 

}

struct RegJNIRec {

int (*mProc)(JNIEnv*);

};

调用mProc,就等价于调用其参数名所指向的函数。 例如REG_JNI(register_com_android_internal_os_RuntimeInit).mProc也就是指进入register_com_android_internal_os_RuntimeInit方法,进入这些方法之后,就会是对于该类中的一些native方法和java方法的映射。

int register_com_android_internal_os_RuntimeInit(JNIEnv* env) {

return jniRegisterNativeMethods(env, "com/android/internal/os/RuntimeInit",

gMethods, NELEM(gMethods));

}

static JNINativeMethod gMethods[] = {

{ "nativeFinishInit", "()V",

(void*) com_android_internal_os_RuntimeInit_nativeFinishInit },

{ "nativeZygoteInit", "()V",

(void*) com_android_internal_os_RuntimeInit_nativeZygoteInit },

{ "nativeSetExitWithoutCleanup", "(Z)V",

(void*) com_android_internal_os_RuntimeInit_nativeSetExitWithoutCleanup },

};

至此就完成了对于native方法和Java方法的映射关联。

另一种加载方式

对于JNI方法的注册无非是通过两种方式一个是上述启动过程中的注册,一个是在程序中通过System.loadLibrary的方式进行注册,这里,我们以System.loadLibrary来分析其注册过程。

public static void loadLibrary(String libname) {

Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);

}

public static Runtime getRuntime() {

 return currentRuntime;

}

synchronized void load0(Class fromClass, String filename) {

if (!(new File(filename).isAbsolute())) {

throw new UnsatisfiedLinkError(

"Expecting an absolute path of the library: " + filename);

}

if (filename == null) {

throw new NullPointerException("filename == null");

}

String error = doLoad(filename, fromClass.getClassLoader());

if (error != null) {

throw new UnsatisfiedLinkError(error);

}

}

String librarySearchPath = null;if (loader != null && loader instanceof BaseDexClassLoader) {

BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;

librarySearchPath = dexClassLoader.getLdLibraryPath();

}

synchronized (this) {

return nativeLoad(name, loader, librarySearchPath);

}

经过层层调用之后来到了nativeLoad方法,这里对于这段代码的分析,目的是为了了解,整个JNI的注册过程和调用的时候,JVM是如何找到相应的native方法的。

对于nativeLoad执行的内容,会转交到classLoader,最终会转化为系统的调用,调用dlopen和dlsym函数。

调用dlopen函数,打开一个so文件并创建一个handle;

调用dlsym()函数,查看相应so文件的JNI_OnLoad()函数指针,并执行相应函数。

简单的说,dlopen、dlsym提供一种动态转载库到内存的机制,在需要的时候,可以调用库中的方法。

在Java字节码中,普通的方法是直接把字节码放到code属性表中,而native方法,与普通的方法通过一个标志“ACC_NATIVE”区分开来。java在执行普通的方法调用的时候,可以通过找方法表,再找到相应的code属性表,最终解释执行代码。

在将动态库load进来的时候,首先要做的第一步就是执行该动态库的JNI_OnLoad方法,我们需要在该方法中声明好native和java的关联,系统中的相关类因为没有提供该方法,因此需要手动调用了各自相应的注册方法。而在我们写的demo中,编译器则为我们做了这个操作,也不需要我们来做。写好映射关系之后,调用registerNativeMethods方法来将这些方法进行注册。具体的函数映射和注册方式如上Runtime所示。

在编译成的java代码中,普通的Java方法会直接指向方法表中具体的方法,而对于native方法则是做了特殊的标记,在执行到native方法时,就会根据我们之前加载进来的native的方法对应表中去查找相应的方法,然后执行。

作者 | Jensen_czx

地址 | https://www.jianshu.com/p/e6b9611f3045

声明 | 本文是 Jensen_czx 原创,已获授权发布,未经原作者允许请勿转载

阅读原文

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容