提到RN通信,大家并不会陌生,即JS、C++之间的通信与C++、Native之间的通信。对于JS与C++的互调会在后续的文章中讲解,本篇文章主要带大家一起了解下 C++与Native 的通信实现机制。
我们知道 C++与Java 的通信是借助于JNI来完成的,那么什么是JNI呢?
说到JNI,这里有两个比较重要的概念需要先提一下:静态注册(被动注册) 与 动态注册(主动注册)。因为像RN这样以JNI为基础实现不同语言之间通信的框架,应用到了很多且频繁的JNI方法调用。如果以静态注册的方式来注册JNI,那么将是一件极为繁琐的事情,而且在对native方法的初次调用的时候,效率也较低,相比之下动态注册的方式就很好的解决了此类问题。
谈到JNI的注册方式,那么再往底层走是怎样实现的呢?为此这篇分享里我们还会继续深入一点,了解一下JNI在Dalvik虚拟机中是如何完成注册的。
本篇分享内容如下:
JNI ( 全称是Java Native Interface ),即 Java 与 C++ 通信接口。C++是系统级的编程语言, 可以用来开发任何和系统相关的程序和类库, 但是Java本身编写底层的应用比较难实现,通过JNI接口,Java可以方便的调用现有的本地库,极大地灵活了Java的开发。
在Android系统中,JNI方法是以 C/C++ 语言来实现的,然后编译成一个so文件。JNI方法需要被加载到当前应用程序进程的地址空间,才能够被调用,意思也就是JNI是需要本地系统来直接执行的。
JNI的注册方式分为两种:静态注册 与 动态注册。那么接下来我们分别介绍两种注册方式的实现以及优缺点。
2.1.1 实现步骤如下:
编写.java并声明native方法
使用javac命令编译.java生成.class文件
使用javah命令生成 与 声明的native方法对应的 .h 头文件
使用 C++ 实现 .h 头文件
编译生成 .so 文件
编译运行完成加载、注册、方法调用
步骤 1 : 编写带有native关键字修饰的方法的Java类
public class Sample {
// 声明四种类型的native方法
public native int intMethod(int n);
public native boolean booleanMethod(boolean bool);
public native String stringMethod(String text);
public native int intArrayMethod(int[] intArray);
public static void main(String[] args) {
// 将Sample.so动态类库,加载到当前进程中
System.loadLibrary("Sample");
Sample sample = new Sample();
//调用native方法
int square = sample.intMethod(5);
boolean bool = sample.booleanMethod(true);
String text = sample.stringMethod("Java");
int sum = sample.intArrayMethod(new int[]{1,2,3,4,5,8,13});
//打印得到的值
System.out.println("intMethod: " + square);
System.out.println("booleanMethod: " + bool);
System.out.println("stringMethod: " + text);
System.out.println("intArrayMethod: " + sum);
}
}
步骤 2: 将 java文件编译生成 class文件
>jimmy@58deMacBook-Pro-9 ~> javac Sample.java
步骤 3 : 生成对应的头文件
>jimmy@58deMacBook-Pro-9 ~> javah Sample
.h 头文件代码如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
#ifndef _Included_Sample
#define _Included_Sample
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Sample
* Method: intMethod
* Signature: (I)I
* Java_完整类名_方法名, 完整类名包括了包名。
*/
JNIEXPORT jint JNICALL Java_Sample_intMethod(JNIEnv *, jobject, jint);
/*
* Class: Sample
* Method: booleanMethod
* Signature: (Z)Z
*/
JNIEXPORT jboolean JNICALL Java_Sample_booleanMethod(JNIEnv *, jobject, jboolean);
/*
* Class: Sample
* Method: stringMethod
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_Sample_stringMethod(JNIEnv *, jobject, jstring);
/*
* Class: Sample
* Method: intArrayMethod
* Signature: ([I)I
*/
JNIEXPORT jint JNICALL Java_Sample_intArrayMethod(JNIEnv *, jobject, jintArray);
#ifdef __cplusplus
}
#endif
#endif
以 JNIEXPORT 修饰的方法,就是Java中声明的Native方法在C++中的实现方式。函数名由 Java_完整类名_方法名 组成,每个函数中都会有一个参数JNIEnv * ,它是由Dalvik虚拟机生成的一个JNI环境对象,使用起来类似Java中的反射。
方法签名Signature类型详表如下:
表头java类型 | Signature | 备注 |
boolean | Z | - |
byte | B | |
char | C | |
short | S | |
int | I | |
long | L | |
float | F | |
double | D | |
void | V | |
object | L用/分割的完整类名 | 例如: Ljava/lang/String表示String类型 |
Array | [签名 | 例如: [I表示int数组, [Ljava/lang/String表示String数组 |
Method | (参数签名)返回类型签名 | 例如: ([I)I表示参数类型为int数组, 返回int类型的方法 |
步骤 4 : C++ 实现头文件中的函数
#include "Sample.h"
#include
JNIEXPORT jint JNICALL Java_Sample_intMethod(JNIEnv *env, jobject obj, jint num){
return num * num;
}
JNIEXPORT jboolean JNICALL Java_Sample_booleanMethod(JNIEnv *env, jobject obj, jboolean boolean){
return !boolean;
}
JNIEXPORT jstring JNICALL Java_Sample_stringMethod(JNIEnv *env, jobject obj, jstring string){
const char* str = env->GetStringUTFChars(string, 0);
char cap[128];
strcpy(cap, str);
env->ReleaseStringUTFChars(string, 0);
return env->NewStringUTF(strupr(cap));
}
JNIEXPORT jint JNICALL Java_Sample_intArrayMethod(JNIEnv *env, jobject obj, jintArray array){
int i, sum = 0;
jsize len = env->GetArrayLength(array);
jint *body = env->GetIntArrayElements(array, 0);
for (i = 0; i < len; ++i){
sum += body[i];
}
env->ReleaseIntArrayElements(array, body, 0);
return sum;
}
在上面的 C++代码中,GetStringUTFChars() 是用来将一个Java字符串转换为C字符串的,因为Java本身都使用了双字节的字符,而C语言本身都是单字节的字符, 所以需要进行转换。NewStringUTF() 是将一个 C++ 字符串转换为一个UTF8字符串。ReleaseStringUTFChars() 是用来释放对象的,在Dalvik虚拟机中是有一个垃圾回收机制的, 但是在C++语言中,这些对象必须手动回收,否则可能造成内存泄漏.
步骤 5 : 编译生成 .so 文件
>jimmy@58deMacBook-Pro-9 ~> ndk-build
步骤 6: 编译运行完成加载、注册、方法调用
通过 System.loadLibrary() 完成 .so 的加载,加载的过程即为JNI注册的过程。方法调用是在 Java中发起的并由本地系统完成JNI方法的调用执行。
所谓的动态注册 是指动态的对Java中声明的Native方法完成注册,使得 C++ 中方法名 和 Java 中 Native 方法名不同的情况下,进行一一对应,形成映射关系。以后再修改或新增 Native方法时,只需在C++ 中修改或新增所需关联的方法,并在 getMethods 数组中完成映射关系,最后再通过ndk-build 重新生成so库就可以运行了。
动态注册是在JNI 层实现的,JAVA层不需要关心,因为在 System.loadLibrary() 执行后就会去调用JNI_OnLoad,有就注册,没有就不注册。
2.2.1 实现步骤如下:
编写.java并声明native方法
编写 C++ 文件并实现 JNI_OnLoad 方法
对应 Java中声明的Native方法,实现一个函数映射表以及在C++侧的具体实现函数,并在 C++ 文件中完成动态注册代码编写
编译生成 .so 文件
编译运行完成加载、注册、方法调用
步骤 1: 编写 .java 文件并声明所需的Native方法
public class Hello {
static {
System.loadLibrary("hello");
}
public static final main(String[] args){
System.out.println(stringFromJNI());
}
public native String stringFromJNI();
}
步骤 2: 编写 C++ 代码 并实现 JNI_OnLoad 方法
#include
#include
#include
#include
#include
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
jint result = -1;
if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
assert(env != NULL);
//执行注册并返回注册状态
if (!registerNatives(env)) {
return -1;
}
//成功
result = JNI_VERSION_1_4;
return result;
}
步骤 3: 对应 Java中声明的Native方法,实现一个函数映射表以及在C++侧的具体实现函数,并在 C++ 文件中完成动态注册代码编写
#include
#include
#include
#include
#include
/*
* 使用Native方法返回一个新的 VM String。
*/
jstring native_hello(JNIEnv* env, jobject thiz) {
return (*env)->NewStringUTF(env, "动态注册JNI");
}
/**
* 方法映射表
* struct: Java侧声明的Native方法名称、Native方法签名、需要指向的C++ 函数
*/
static JNINativeMethod gMethods[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void*)native_hello},
};
static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* gMethods, int numMethods) {
jclass clazz;
//根据提供的class类完整名称,获取该class类
clazz = (*env)->FindClass(env, className);
if (clazz == NULL) {
return JNI_FALSE;
}
//检查是否注册成功
if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
static int registerNatives(JNIEnv* env) {
//指定要注册的类
const char* kClassName = "com/example/hello/Hello";
return registerNativeMethods(env, kClassName, gMethods,
sizeof(gMethods) / sizeof(gMethods[0]));
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
jint result = -1;
if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
assert(env != NULL);
//执行注册并返回注册状态
if (!registerNatives(env)) {
return -1;
}
//成功
result = JNI_VERSION_1_4;
return result;
}
步骤 4 : 编译生成 .so 文件
>jimmy@58deMacBook-Pro-9 ~> ndk-build
步骤 5: 编译运行完成加载、注册、方法调用
通过 System.loadLibrary() 完成 .so 的加载,加载的过程即为JNI注册的过程。方法调用是在 Java中发起的并由本地系统完成JNI方法的调用执行。
我们用一张图来展现一下两种方式在创建流程上的区别:
静态注册:
静态注册多用于NDK开发。
动态注册:
由于映射表的存在,JVM不再需要通过函数名来查找相关函数,而是通过现成的函数映射表的对应关系来执行,因此执行效率更高。
动态注册多用于Framework开发。
不管是静态注册方式,还是动态注册方式,都需要将c文件编译成平台所需要的库。
前面提到,JNI方法是 .so 文件被加载的时候,被注册到虚拟机的,因此JNI在JVM中的注册是从 .so 文件的加载开始的,也就是从 System.loadLibrary() 开始。
在 动态注册(主动注册)的情况下,会先执行到 RegisterNatives(env, clazz, gMethods, numMethods) 函数,进而执行一系列的判断与调用,最终执行到 dvmSetNativeFunc(Method* method, DalvikBridgeFunc func, const u2* insns) 函数。
我们看一下它的具体实现:
void dvmSetNativeFunc(Method* method, DalvikBridgeFunc func, const u2* insns){
......
if (insns != NULL) {
//更新 insns 和 nativeFunc
method->insns = insns;
//Android原子变量的设置操作
android_atomic_release_store((int32_t) func,
(void*) &method->nativeFunc);
} else {
//只更新 nativeFunc
method->nativeFunc = func;
}
......
}
该函数就是JNI最终实现注册的地方,它有三个参数:method、func、insns。
method:
表示一个 Method 对象,即要注册JNI方法的Java类成员函数。当检测到它是一个 JNI 方法时,它的成员变量 method.nativeFunc 保存的就是该JNI方法的函数地址,即当发现是个native函数时,就将C++函数地址保存到 method.nativeFunc 中,后续调用native方法的时候,就相当于通过 method.nativeFunc 来调用一个C++函数。
func:
表示当前JNI方法的 Bridge 函数;它就是根据Dalvik虚拟机的启动选项来为即将要注册的JNI选择的一个合适的Bridge函数。所谓Bridge函数,其实就是在JNI被真正调用之前,根据虚拟机的启动参数,来选择的一个初始化流程,不同的Bridge函数有不同的初始化流程,调用之前的准备工作不同。在JVM不开启JNI检查的时候,该Bridge会返回一个 dvmCallJNIMethod() 函数。
insnc:
表示要注册的JNI方法的函数地址;
到这里我们可以知道,注册的过程也就是将 Method 的成员 nativeFunc 指向 DalvikBridgeFunc,将成员 insnc 指向实际的native函数。这样一来,当Java层开始调用native函数的时候就会首先进入一个名叫 dvmCallJNIMethod() 函数,而真正的native函数指针则存储在Method->insns中。dvmCallJNIMethod() 函数会先准备好启动参数,然后再调用函数 dvmPlatformInvoke() 执行对应的 native方法 (也就是method->insns所指向的方法),这样就完成了native 函数的调用。
JSBridge是一个宽泛的概念,它是JS与Native(Android/IOS)进行双向通信的一种手段,使得JS可以借助它实现调用Native(Android/Ios)的功能,以及Native(Android/Ios)可以方便的调用JavaScript中的功能函数。
4.1.1 实现Js调用Java
1. JavaScriptInterface
Android API 4.2 之前使用的是 addJavascriptInterface,但是存在严重的安全隐患,Android 4.2 之后,谷歌提供了@JavascriptInterface
对象注解的方式建立JS对象和Java对象的绑定,提供给JavaScript调用的方法必须带有@JavascriptInterface
。原理就是通过WebView提供的addJavascriptInterface
方法给浏览器 全局对象 window 注入一个命名空间,然后给Web增加一些可以操作Java的反射。
原理实现(源码分析: https://www.cnblogs.com/aimqqroad-13/p/13893588.html )
addJavascriptInterface实现原理:从 webview.addJavaScriptInterface() 开始,通过一系列的调
用,并基于JNI 交由C++来实现,最终是通过Chromium的IPC机制( 进程间通信IPC机制则是基于
Unix Socket通信协议实现的 ),发出了一条消息。
2. 改写浏览器原有对象
这主要是修改浏览器中全局对象 window 上的某些方法,进而拦截固定规则的参数,最后分发给Java对应的方法去处理。这里常用的是以下四个方法:
onJsAlert
监听onJsConfirm
监听onConsoleMessage
监听onJsPrompt
监听3. URL scheme
它可以通过拦截跳转页面时的 URL请求,并解析这个scheme协议,按照一定规则捕获行为并交由Native(Android/Ios)解决。安卓和iOS分别用到拦截URL请求的方法是:
4.1.2 Java调用JS的实现方式
webView.loadUrl("javascript:callFromJava('call from java')");
RN旧版本中所指的JSBridge,其实现原理为,对脚本引擎 JSCore 进行了封装,并以 JS 与 C++进行信息交换的方式,完成 JS 与 C++ 的通信。
新架构中的JSI是对 JavaScript 引擎底层API进行封装,而产生的 JS <=> C++ 的映射框架,以注入的方式实现JS与C++的通信。有了这一层映射,JS与C++可以相互感知、相互持有对方的实例,从而实现JS与C++的同步通讯,也摒弃了JSON数据的序列化和反序列化的过程,大大提升通信效率。
4.4 JNI、JSBridge、RN -- JSBridge、JSI 的区别