Facebook 曾在 2018 年 6 月宣布了大规模重构 RN 的计划和路线图,整个的重构目的是为了让 RN 更轻量化、更适应混合开发,接近甚至达到原生的体验。而新架构的技术核心则是JSI,Turbomodule 也正是基于它来实现的。
介于期望大家能够一起更深入的理解 RN 新架构的亮点,接下来的分享将会从 旧架构 和 新架构的 底层实现开始入手,冒泡式的逐一了解RN框架的原理。
众所周知,JNI 是实现 C++ 与 Java 不同语言之间进行通信的一种手段,通过它我们可以方便的实现 Java调用C++ 和 C++调用Java。在Android系统中,JNI方法是以 C++ 语言来实现的,然后编译成一个so文件。JNI方法需要被加载到当前应用程序进程的地址空间,才能够被调用,意思也就是JNI是需要本地系统来直接执行的。
JNI 实现 C++ 与 Java 通信的过程,可以分为两步:首先需要进行注册,也就是将JNI 方法加载到当前应用程序的地址空间中;再者执行native方法的调用。
静态注册 和 动态注册,接下来我们一起来看一下这两种注册方式。
2.1 静态注册
该方式一般适用于NDK的开发,适用于逻辑不是很复杂、通信不是很频繁的需求场景中。
实现步骤分为6步完成,其中包括:
编写.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方法的调用执行。
2.2 动态注册
动态注册,又名 主动注册,即它会提供一个函数映射表,使得 C++ 方法名 和 Java 中 native 方法名不一致的情况下进行一一对应,形成映射关系。以后再修改或新增 native方法时,只需在 C++ 中修改或新增所需关联的方法,并在 getMethods 数组中完成映射关系,最后再通过ndk-build 重新生成so库就可以运行了。
动态注册的方式,适用于RN这样业务场景比较复杂,通信比较频繁的需求,使得开发过程更为灵活。另外,动态注册的方式,需要我们在 C++ 中必须实现一个 JNI_OnLoad 方法,该方法就是执行动态注册的入口。
实现步骤分为 5 步完成,其中包括:
1. 编写.java并声明native方法
2. 编写 C++ 文件并实现 JNI_OnLoad 方法
3. 对应 Java中声明的Native方法,实现一个函数映射表以及在C++侧的具体实现函数,并在 C++ 文件中完成动态注册代码编写
4. 编译生成 .so 文件
5. 编译运行完成加载、注册、方法调用
步骤 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方法的调用执行。
2.3 两种注册方式的区别
我们用一张图来展现一下两种方式在创建流程上的区别:
静态注册:
1. 当so被加载之后,JVM虚拟机需要通过函数名来查找相关函数,初次调用JIN方法时需要建立关联,影响效率。
2. 静态注册多用于NDK开发。
动态注册:
1. 由于映射表的存在,JVM不再需要通过函数名来查找相关函数,而是通过现成的函数映射表的对应关系来执行,因此执行效率更高。
2. 动态注册多用于Framework开发。
不管是静态注册方式,还是动态注册方式,都需要将c文件编译成平台所需要的库。
2.4 JNI 在 JVM 中注册的本质
前面有提到,JNI方法是 .so 文件被加载的时候,被注册到虚拟机的,因此JNI在JVM中的注册是从 .so 文件的加载开始的,也就是从 System.loadLibrary() 开始。
接下来我们要讲的是在 动态注册(主动注册) 的情况下,JVM 注册 JNI 的一个实现机制 。C++ 文件中执行完 JNI_OnLoad 之后,进而会执行到 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被真正调用之前,根据虚拟机的启动参数,来选择的一个初始化流程,不同的初始化流程,调用之前的准备工作也不同。在JVM不开启JNI检查的时候,该Bridge会返回一个 dvmCallJNIMethod() 函数。
insnc:
表示要注册的JNI方法的实际函数地址;
到这里我们可以知道,注册的过程也就是将 Method 的成员 nativeFunc 指向 DalvikBridgeFunc,将成员 insnc 指向实际的native函数。这样一来,当Java层开始调用native函数的时候就会首先进入一个名叫 dvmCallJNIMethod() 函数,而真正的native函数指针则存储在Method->insns中。dvmCallJNIMethod() 函数会先准备好启动参数,然后再调用函数 dvmPlatformInvoke() 执行对应的 native方法 (也就是method->insns所指向的方法),这样就完成了native 函数的调用。
前面在讲JNI的时候,讲到了 Java 调用 C++ 的一个实现过程,那么C++ 调用 Java 又是如何实现的呢?一句话概括 其实就是通过类似Java反射的方式来完成 C++ 对 Java 的调用的。那接下来让我们一起开始了解下。
该方式的实现需要用到的功能如下:
该方式实现的步骤:
步骤一 : 编写Java代码
public class Sample {
public String name;
public static String sayHello(String name) {
return "Hello, " + name + "!";
}
public String sayHello() {
return "Hello, " + name + "!";
}
}
步骤二 : 编译生成class文件
>javac Sample.java
步骤三 : 编写C++ 代码并完成对Java函数的调用
#include
#include
#include
int main(void){
//虚拟机创建所需的相关参数
JavaVMOption options[1]; //相当于在命令行里传入的参数
JNIEnv *env; //JNI环境变量
JavaVM *jvm; //虚拟机实例
JavaVMInitArgs vm_args; //虚拟机创建的初始化参数, 这个参数里面会包含JavaVMOption
long status; //虚拟机启动是否成功的状态
jclass cls; //将要寻找的Class对象
jmethodID mid; //Class对象中的方法Id
jfieldID fid; //Class对象中的属性Id
jobject obj; //创建的新对象
//创建虚拟机
// "-Djava.class.path=."是JVM将要寻找并加载的 .class文件路径
options[0].optionString = "-Djava.class.path=.";
memset(&vm_args, 0, sizeof(vm_args));
vm_args.version = JNI_VERSION_1_4; //vm_args.version是Java的版本
vm_args.nOptions = 1; //vm_args.nOptions是传入的参数的长度
vm_args.options = options; //把JavaVMOption传给JavaVMInitArgs里面去.
//启动虚拟机
status = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
if (status != JNI_ERR){
// 首先获得class对象(JVM在Java中都是自己启动的, 但在C++ 中只能手动启动, 启动完之后的事情就和在Java中一样了, 不过要使用C++ 的语法)。
cls = (*env)->FindClass(env, "Sample2");
if (cls != 0){
// 获取方法ID, 通过方法名和签名, 调用静态方法
mid = (*env)->GetStaticMethodID(env, cls, "sayHello", "(Ljava/lang/String;)Ljava/lang/String;");
if (mid != 0){
const char* name = "World";
jstring arg = (*env)->NewStringUTF(env, name);
jstring result = (jstring)(*env)->CallStaticObjectMethod(env, cls, mid, arg);
const char* str = (*env)->GetStringUTFChars(env, result, 0);
printf("Result of sayHello: %s\n", str);
(*env)->ReleaseStringUTFChars(env, result, 0);
}
/*** 新建一个对象 start ***
// 调用默认构造函数
//obj = (*env)->AllocObjdect(env, cls);
// 调用指定的构造函数, 构造函数的名字叫做
mid = (*env)->GetMethodID(env, cls, "", "()V");
obj = (*env)->NewObject(env, cls, mid);
if (obj == 0){
printf("Create object failed!\n");
}
/*** 新建一个对象 end ***/
// 获取属性ID, 通过属性名和签名
fid = (*env)->GetFieldID(env, cls, "name", "Ljava/lang/String;");
if (fid != 0){
const char* name = "icejoywoo";
jstring arg = (*env)->NewStringUTF(env, name);
(*env)->SetObjectField(env, obj, fid, arg); // 修改属性
}
// 调用成员方法
mid = (*env)->GetMethodID(env, cls, "sayHello", "()Ljava/lang/String;");
if (mid != 0){
jstring result = (jstring)(*env)->CallObjectMethod(env, obj, mid);
const char* str = (*env)->GetStringUTFChars(env, result, 0);
printf("Result of sayHello: %s\n", str);
(*env)->ReleaseStringUTFChars(env, result, 0);
}
//我们可以看到静态方法是只需要class对象, 不需要实例的, 而非静态方法需要使用我们之前实例化的对象.
}
//执行完操作之后,销毁虚拟机
(*jvm)->DestroyJavaVM(jvm);
return 0;
} else{
printf("JVM Created failed!\n");
return -1;
}
}
额外补充:java的String使用了unicode, 是双字节的字符, 而C++中使用的单字节的字符.
从C转换为java的字符, 使用NewStringUTF方法:
jstring arg = (*env)->NewStringUTF(env, name);
从java转换为C的字符, 使用GetStringUTFChars
const char* str = (*env)->GetStringUTFChars(env, result, 0);
步骤四 : 编译运行
编译运行,完成 C++对Java的调用。
首先需要解释一下,为什么我们需要了解一下 JS引擎的注入原理。大家都对 JSBridge 应该都不陌生,做过 webview 混合开发的会有更深刻的认识,它是 JS语言 与 非JS语言 实现双向通信的一种手段、是一个抽象的概念。JSBridge的实现方式包括:JavaScriptInterface、改写浏览器原有对象、URL Scheme(即Url拦截)。不管哪一种方式,其表现都是很不尽如人意的。为了大家能够了解的更清晰,下面会对这三种方式进行展开讲解。
3.1.1 实现Js调用Java的方式
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 ,最终是通过Chromium的IPC机制( 进程间通信IPC机制则是基于
Unix Socket通信协议实现的 ),发出了一条消息。该方式的通信效率是非常低下的。
这主要是修改浏览器中全局对象 window 上的某些方法,进而拦截固定规则的参数,最后分发给Java对应的方法去处理。这里常用的是以下四个方法:
onJsAlert
监听onJsConfirm
监听onConsoleMessage
监听onJsPrompt
监听这种方式局限性很强,也不适用于复杂业务的开发场景。
它可以通过拦截跳转页面时的 URL请求,并解析这个scheme协议,按照一定规则捕获行为并交由Native(Android/Ios)解决。安卓和iOS分别用到拦截URL请求的方法是:
说白了就是类似拦截重定向,这种方式也远远不够灵活。
3.1.2 Java调用JS的实现方式
webView.loadUrl("javascript:callFromJava('call from java')");
上面我们分析了 JSBridge 的实现方式以及得出了运行表现。显然是不能满足 ReactNative 对混合开发高性能的需求的。因此 ReactNative 便越过了 Webview,直接对其执行引擎进行了大刀阔斧的运用。不管是RN旧架构 还是 RN新架构,都采用了注入的方式,来实现 JS 与 Native(C++) 之间的通信。说到向引擎注入方法,相信大家都用过 console.log()、setTimeout()、setInterval() 等方法,像这些方法就是通过 polyfill 的方式注入到 JS 引擎里的,JS引擎内部本身是没有这些方法的 。沿着这个思路,我们可以通过向引擎注入方法和变量的方式,来实现在 JS 中调用注入的 Native(C++) 方法。
这里我们以 JavaScriptCore 为例,来和大家一起了解一下 如何实现像 JS引擎注入方法和变量,它提供了丰富的 API 供上层调用 ( 详细API看这里 )。
1、JavaScriptCore API 数据结构:
数据类型 | 描述 |
JSGlobalContextRef | JavaScript全局上下文。也就是JavaScript的执行环境。 |
JSValueRef: | JavaScript的一个值,可以是变量、object、函数。 |
JSObjectRef: | JavaScript的一个object或函数。 |
JSStringRef: | JavaScript的一个字符串。 |
JSClassRef: | JavaScript的类。 |
JSClassDefinition: | JavaScript的类定义,使用这个结构,C、C++可以定义和注入JavaScript的类。 |
2、JavaScriptCore API 主要函数:
API | 描述 |
JSGlobalContextCreate、JSGlobalContextRelease | 创建和销毁JavaScript全局上下文 |
JSContextGetGlobalObject: | 获取JavaScript的Global对象 |
JSObjectSetProperty、JSObjectGetProperty | JavaScript对象的属性操作 |
JSEvaluateScript | 执行一段JS脚本 |
JSClassCreate | 创建一个JavaScript类 |
JSObjectMake | 创建一个JavaScript对象 |
JSObjectCallAsFunction | 调用一个JavaScript函数 |
JSStringCreateWithUTF8Cstring、JSStringRelease | 创建、销毁一个JavaScript字符串 |
JSValueToBoolean、JSValueToNumber、JSValueToStringCopy、JSValueToObject | JSValueRef转为C++类型 |
JSValueMakeBoolean、JSValueMakeNumber、JSValueMakeString | C++类型转为JSValueRef |
3、C++调用JS
1. 创建JS执行环境
//创建JS全局上下文 (JS执行环境)
JSGlobalContextRef context = JSGlobalContextCreate(NULL);
2. 获取Global全局对象
//获取Global全局对象
JSObjectRef global = JSContextGetGlobalObject(context);
3. 获取JS的全局变量、全局函数、全局复杂对象
/**
* 获取JS的全局变量
*/
// 获取将要被调用的变量名,并转换为 JS 字符串
JSStringRef varName = JSStringCreateWithUTF8CString("JS 变量名");
// 从全局对象 global 中查找并获取 JS 全局变量 varName
JSValueRef var = JSObjectGetProperty(context, global, varName, NULL);
// 手动销毁变量名称字符串,释放内存
JSStringRelease(varName);
// 将JS变量转化为C++类型
int n = JSValueToNumber(context, var, NULL);
/**
* 获取JS的全局函数
*/
//获取将要被调用的函数名,并转换为 JS 字符串
JSStringRef funcName = JSStringCreateWithUTF8CString("JS 函数名");
// 从全局对象 global 中查找并获取 JS 全局函数 funcName
JSValueRef func = JSObjectGetProperty(context, global, funcName, NULL);
// 手动销毁字符串,释放内存
JSStringRelease(funcName);
// 将JS函数转换为一个对象
JSObjectRef funcObject = JSValueToObject(context,func, NULL);
// 准备参数,将两个数值1和2作为两个参数
JSValueRef args[2];
args[0] = JSValueMakeNumber(context, 1);
args[1] = JSValueMakeNumber(context, 2);
// 调用JS函数,并接收返回值
JSValueRef returnValue = JSObjectCallAsFunction(context, funcObject, NULL, 2, args, NULL);
// 将JS的返回值转换为C++类型
int ret = JSValueToNumber(context, returnValue, NULL);
/**
* 获取复杂的对象
*/
//获取将要被调用的JS对象名,并转换为 JS 字符串
JSStringRef objName = JSStringCreateWithUTF8CString("JS 对象名");
// 从全局对象 global 下的查找并生成一个对象
JSValueRef obj = JSObjectGetProperty(context, global, objName, NULL);
// 释放内存
JSStringRelease(objName);
// 将obj转换为对象类型
JSObjectRef object = JSValueToObject(context, obj, NULL);
// 获取JS复杂对象中的方法名
JSStringRef funcObjName = JSStringCreateWithUTF8CString("JS 对象中的方法名");
// 获取复杂对象中的函数
JSValueRef objFunc = JSObjectGetProperty(context, object, funcObjName, NULL);
//释放内存
JSStringRelease(funcObjName);
//调用复杂对象的方法, 这里省略了参数和返回值
JSObjectCallAsFunction(context, objFunc, NULL, 0, 0, NULL);
通过上面实现 C++ 调用 JS 的代码可知,在调用时,首先需要创建一个 JS 上下文环境,进而获取一个全局对象 global,所有会被C++调用的方法、变量,都会挂载到这个 global 对象上。我门通过 JavaScriptCore 提供的 API 做一系列的查询、类型转化,最终完成对JS的调用。
4、JS调用C++ :引擎注入
JS要调用C++,前提是必须先将 C++ 侧的变量、函数、类 通过类型转换之后 注入到JS中 ( 也就是需要将其作为属性设置到 全局对象 global 上 ),这样一来在JS侧就有了一份对照表,因此才能在 JS 侧发起调用流程。
下面我们通过一个实例来了解一下,JS 是如何在 JSC 层完成对C++的调用的。首先我们定义一个C++ 类,并从中定义一组全局函数,然后封装 JavaScriptCore 对 C++ 类的调用,最后提供给JSC进行CallBack回调。
1. 定义一个 C++ 类
class test{
public:
test(){
number = 0;
};
void func(){
number++;
}
int number;
};
2. 定义一个变量 g_test
test g_test;
3. 封装对 test.func() 的调用
JSValueRef testFunc(JSContextRef ctx, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef*){
test* t = static_cast(JSObjectGetPrivate(thisObject));
t->func();
return JSValueMakeUndefined(ctx);
}
4. 封装对 test.number 的 get 操作
JSValueRef getTestNumber(JSContextRef ctx, JSObjectRefthisObject, JSStringRef, JSValueRef*){
test* t = static_cast(JSObjectGetPrivate(thisObject));
return JSValueMakeNumber(ctx, t->number);
}
5. 编写一个方法来创建 JS 类对象
JSClassRef createTestClass(){
//类的成员变量定义,可以有多个,最后一个必须是{ 0, 0, 0 },也可以指定set操作
static JSStaticValue testValues[] = {
{"number", getTestNumber, 0, kJSPropertyAttributeNone },
{ 0, 0, 0, 0}
};
//类的成员方法定义,可以有多个,最后一个必须是{ 0, 0, 0 }
static JSStaticFunction testFunctions[] = {
{"func", testFunc, kJSPropertyAttributeNone },
{ 0, 0, 0 }
};
//定义一个类,设置成员变量 和 成员方法
static JSClassDefinition classDefinition = {
0,kJSClassAttributeNone, "test", 0, testValues, testFunctions,
0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0
};
//创建一个 JS 类对象
static JSClassRef t = JSClassCreate(&classDefinition);
return t;
}
6. 将C++类转换后注入JS到全局对象 global
// 创建JS全局上下文 (JS执行环境)
JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);
// 获取 global 全局对象
JSObjectRef globalObj = JSContextGetGlobalObject(ctx);
// 新建一个 JS 类对象,并使之绑定到 g_test 变量
JSObjectRef classObj = JSObjectMake(ctx, createTestClass(), &g_test);
// 获取将要被调用的 JS 对象名,并转换为 JS 字符串
JSStringRef objName = JSStringCreateWithUTF8CString("g_test");
// 将新建的 JS类对象 注入JS中( 即将 classObj 作为属性,挂载到全局对象 global 中 )
JSObjectSetProperty(ctx, globalObj, objName, classObj, kJSPropertyAttributeNone, NULL);
7. JS中完成调用
g_test.func();
let n = g_test.number;
let t = new test;
JSI是RN新架构实现JS与Native通信的基石,Turbomodules 也是基于 JSI 实现的。 对于了解RN新架构来说,先搞明白 JSI 是至关重要的,那下面就让我们来聊一聊 JSI。
JSI 的全称是 JavaScript Interface,即 JS Interface 接口,它是对 JS引擎 与 Native (C++) 之间相互调用的封装,通过 HostObject 接口实现双边映射,官方也称它为映射框架。
有了这层封装,在 ReactNative 中有了两方面的提升:
Js是运行在 Js Runtime 中的,所谓的方法注入,也就是将所需方法注入到 Js Runtime 中去,JSI 则负责具体的注入工作,通过 Js 引擎提供的 API,完成 C++ 方法的注入。上图就是 JS 与 Native(C++) 在 JSI 新架构中实现通信的简易架构。
那么接下来,就让我们继续来了解一下 JSI 是如何实现 JS 与 Native 互调通信的吧。
接下来我们通过一个实际的例子,来了解下 JSI 是如何实现 JS 与 Native (C++)通信的,首先我们先来看一下 JS 调用 Native(C++)的过程。
1. JS 调用 Native (C++)
步骤如下:
1.1 编写 .java 文件
package com.terrysahaidak.test.jsi;
public class TestJSIInstaller {
// native 方法
public native void installBinding(long javaScriptContextHolder);
// stringField 会被 JS 调用
private String stringField = "Private field value";
static {
//注册 .so 动态库
System.loadLibrary("test-jsi");
}
// runTest 会被 JS 调用
public static String runTest() {
return "Static field value";
}
}
1.2 编写 .h 文件,实现 .java 中的 native 方法,并在此声明一个 SampleModule 对象,该对象就是 TurboModule的实现。SampleModule 需要继承 JSI 中的 I (即 HostObject 接口,它定义了注入操作的细节以及双边数据的交换的逻辑),并实现 install 方法以及 get 方法。
#pragma once
#include
#include "../../../../../../node_modules/react-native/ReactCommon/jsi/jsi/jsi.h"
using namespace facebook;
extern "C" {
JNIEXPORT void JNICALL
Java_com_terrysahaidak_test_jsi_TestJSIInstaller_installBinding(JNIEnv* env, jobject thiz, jlong runtimePtr);
}
// 声明 SampleModule 继承 HostObject,并实现 install 方法
class SampleModule : public jsi::HostObject {
public:
static void install(
jsi::Runtime &runtime,
const std::shared_ptr sampleModule
);
// 每一个 TurboModule -- SampleModule 中的所有方法和属性,都需要通过声明式注册,在 get 中进行声明
jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override;
private:
JNIEnv jniEnv_;
};
1.3 编写 C++ 文件,实现 SampleModule 相关逻辑
#include
#include
#include "TestJSIInstaller.h"
// 虚拟机实例,用来获取 JNIenv 环境
JavaVM *jvm;
// class 类实例
static jobject globalObjectRef;
// class 类对象
static jclass globalClassRef;
// native 方法 installBinding 的具体实现
extern "C" JNIEXPORT void JNICALL
Java_com_terrysahaidak_test_jsi_TestJSIInstaller_installBinding(JNIEnv *env, jobject thiz, jlong runtimePtr){
// runtimePtr 为 long 类型的值,这里强转成 Runtime类型,也就是 代表的 JS 引擎
auto &runtime = *(jsi::Runtime *)runtimePtr;
// 通过智能指针 实例化 SampleModule
auto testBinding = std::make_shared();
// 调用 SampleModule 的 install 方法,
SampleModule::install(runtime, testBinding);
// 获取并存储虚拟机实例 并存储到 &jvm
env->GetJavaVM(&jvm);
// 创建一个全局对象的实例引用
globalObjectRef = env->NewGlobalRef(thiz);
//通过 class 类路径,创建一个 全局类对象引用
auto clazz = env->FindClass("com/terrysahaidak/test/jsi/TestJSIInstaller");
globalClassRef = (jclass)env->NewGlobalRef(clazz);
}
// install 方法的具体实现
void SampleModule::install(jsi::Runtime &runtime, const std::shared_ptr sampleModule){
// 定义 TurboModule 名称,也就是 JS 侧调用时使用的名称。
auto testModuleName = "NativeSampleModule";
// 创建一个 HostObject 实例,即 SampleModule 实例
auto object = jsi::Object::createFromHostObject(runtime, sampleModule);
// 通过 runtime 中的 global() 方法获取到 JS 世界的 global 对象,
// runtime 是 JS 引擎的实例,通过 runtime.global() 获取到 JS 世界的 global 对象,
// 进而调用 setProperty() 将 "NativeSampleModule" 注入到 global 中,
// 从而完成 "NativeSampleModule" 的导出。
// *** 注意 : runtime.global().setProperty 是 JSI 中实现的方法 ***
runtime.global().setProperty(runtime, testModuleName, std::move(object));
}
// TurboModule 的 get 方法,当 JS 侧开始使用 "." 来调用某个方法时,会执行到这里。
jsi::Value SampleModule::get(
jsi::Runtime &runtime,
const jsi::PropNameID &name){
auto methodName = name.utf8(runtime);
// 获取 需要调用的成员名称,并进行判断
if (methodName == "getStaticField"){
// 动态创建 HostFunction 对象
return jsi::Function::createFromHostFunction(
runtime,
name,
0,
[](
jsi::Runtime &runtime,
const jsi::Value &thisValue,
const jsi::Value *arguments,
size_t count) -> jsi::Value {
// 这里通过 反射 完成对 Java 侧 方法的调用
auto runTest = env->GetStaticMethodID(globalClassRef, "runTest", "()Ljava/lang/String;");
auto str = (jstring)env->CallStaticObjectMethod(globalClassRef, runTest);
const char *cStr = env->GetStringUTFChars(str, nullptr);
return jsi::String::createFromAscii(runtime, cStr);
});
}
if (methodName == "getStringPrivateField"){
return jsi::Function::createFromHostFunction(
runtime,
name,
0,
[](
jsi::Runtime &runtime,
const jsi::Value &thisValue,
const jsi::Value *arguments,
size_t count) -> jsi::Value {
auto valId = env->GetFieldID(globalClassRef, "stringField", "Ljava/lang/String;");
auto str = (jstring)env->GetObjectField(globalObjectRef, valId);
const char *cStr = env->GetStringUTFChars(str, nullptr);
return jsi::String::createFromAscii(runtime, cStr);
});
}
return jsi::Value::undefined();
}
1.4 在 JS 中调用注入的方法
{
global.NativeSampleModule.getStaticField()
}
{/* this is from C++ JSI bindings */}
{
global.NativeSampleModule.getStringPrivateField()
}
总结分析:TurboModule 需要注册 (注入) 到 JS 引擎才能够被 JS 调用,在执行静态方法 install 之后,最终通过 runtime.global() 将其注入到了 JS 引擎当中。JSI HostObject 向 JS 导出的方法并不是预先导出的,而是懒加载及时创建的。从 JSI 进入到 get 函数后,先是通过 methodName 判断,动态的创建一个 HostFunction ,作为 get 的返回结果。在 HostFunction 方法中再通过反射的方式,实现对 Java 方法的调用,这样就完成了 JS 通过 JSI 调用 Java 的通信流程。
那么下面我们再来了解一下 Native (C++) 调用 JS 的通信方式。
2. Native (C++)调用 JS
Native调用 JS 主要是通过 JSI 中的 Runtime.global().getPropertyAsFunction(jsiRuntime, "jsMethod").call(jsiRuntime) 方法实现。那么接下来我们就来一起看下整个流程是怎么样的。
实现步骤如下:
2.1 在 JS module 中 增加一个 将被 Native 调用的 JS 方法 jsMethod()
import React from "react";
import type {Node} from "react";
import {Text, View, Button} from "react-native";
const App: () => Node = () => {
// 等待被 Native 调用
global.jsMethod = (message) => {
alert("hello jsMethod");
};
const press = () => {
setResult(global.multiply(2, 2));
};
return (
);
};
export default App;
2.2 Native 调用 JS 全局方法
runtime
.global()
.getPropertyAsFunction(*runtime, "jsMethod")
.call(*runtime, "message内容!");
注意内容: 我们需要通过 JSI 中的 getPropertyAsFunction() 来获取 JS 中的方法,但需要注意,getPropertyAsFunction() 获取的是 global 全局变量下的某个属性或方法,因此,我们在 JS 中声明一个需要被 Native 调用的方法的时候,需要显式的指定它的作用域。
首先需要声明一下,这里的 JSC 指的是 RN 中对 JavaScritpCore引擎 的封装层。
相同点:
首先在底层实现上来说,JSI 与 JSC 都是通过向 JS 引擎中注入方法,来实现的 JS 与 Native 通信,同时 注入的方法也都是挂载到了 JS global 全局对象上面。
不同点:
旧架构中的 JSC 处理的注入对象是JSON 对象与C++ 对象,内部涉及复杂且频繁的类型转换。且在
JSBridge 这种异步传输的设计中存在三个线程之间的通信:UI线程、Layout线程、JS线程,在典型的列表快速滑动时出现空白页的例子中,效率低下得到明显的体现。
而对于 JSI 来讲,弃用了异步的bridge,传输的数据也不再依赖于 JSON 的数据格式,而是将HostObject 接口作为了双边通信的协议,实现了双边同步通信下的高效信息传输。
另外编写 NativeModule 的方式与旧架构中相比发生了改变,除了功能之外的逻辑,需要在一个 C++ 类中来完成。 因此,一个 TurboModule 的实现分为两部分: C++ & Java (OC)。
到这里,我们就把 RN 新老架构中通信的底层原理讲解完了,如果用一句话来概括 JSI 提效的体现,可以这么讲:"JSI 实现了通信桥 Bridge 的自定义,并通过 HostObjec 接口协议的方式取代了 旧架构中基于异步 bridge 的JSON数据结构,从而实现了同步通信,并且避免了 JSON 序列化与反序列化的繁琐操作,大大提升了 JS 与 Native 的通信效率。"
后续我们会继续分享通信流程以及通信模块架构方便相关的知识。
1. JNI、JSBridge、RN -- JsBridge、JSI 、JSC、JavaScriptCore 都是什么,有什么关系和区别?
2. RN 中的 global 与 JS 引擎中的 global 的关系, RN 工程中的 global、window、globalThis 的关系
3. 旧版本中的 bridge 是对外封闭的,我们无法参与到其中,而在新架构中,我们可以通过 JSI 自由的定义自己的 通信桥?