Facebook 曾在 2018 年 6 月宣布了大规模重构 RN 的计划和路线图,整个的重构目的是为了让 RN 更轻量化、更适应混合开发,接近甚至达到原生的体验。而新架构的技术核心则是JSI,Turbomodule 也正是基于它来实现的。

介于期望大家能够一起更深入的理解 RN 新架构的亮点,接下来的分享将会从 旧架构 和 新架构的 底层实现开始入手,冒泡式的逐一了解RN框架的原理。

一、Java调用C++ :JNI

众所周知,JNI 是实现 C++ 与 Java 不同语言之间进行通信的一种手段,通过它我们可以方便的实现 Java调用C++ 和 C++调用Java。在Android系统中,JNI方法是以 C++ 语言来实现的,然后编译成一个so文件。JNI方法需要被加载到当前应用程序进程的地址空间,才能够被调用,意思也就是JNI是需要本地系统来直接执行的。

JNI 实现 C++ 与 Java 通信的过程,可以分为两步:首先需要进行注册,也就是将JNI 方法加载到当前应用程序的地址空间中;再者执行native方法的调用。

1. JNI 的注册方式

静态注册动态注册,接下来我们一起来看一下这两种注册方式。

2.1 静态注册

该方式一般适用于NDK的开发,适用于逻辑不是很复杂、通信不是很频繁的需求场景中。

实现步骤分为6步完成,其中包括:

  1. 编写.java并声明native方法

  2. 使用javac命令编译.java生成.class文件

  3. 使用javah命令生成 与 声明的native方法对应的 .h 头文件

  4. 使用 C++ 实现 .h 头文件

  5. 编译生成 .so 文件

  6. 编译运行完成加载、注册、方法调用

步骤 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 <jni.h>
#ifndef _Included_Sample
#define _Included_Sample
#ifdef __cplusplusextern "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 <string.h>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 <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <jni.h>
#include <assert.h>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 <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <jni.h>
#include <assert.h>/** 使用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 和 nativeFuncmethod->insns = insns;//Android原子变量的设置操作android_atomic_release_store((int32_t) func,(void*) &method->nativeFunc);} else {//只更新 nativeFuncmethod->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 函数的调用

二、C++ 主动调用 Java :反射

前面在讲JNI的时候,讲到了 Java 调用 C++ 的一个实现过程,那么C++ 调用 Java 又是如何实现的呢?一句话概括 其实就是通过类似Java反射的方式来完成 C++ 对 Java 的调用的。那接下来让我们一起开始了解下。

该方式的实现需要用到的功能如下:

  1. 创建虚拟机
  2. 寻找class对象, 创建对象
  3. 调用静态方法和成员方法
  4. 获取成员属性, 修改成员属性

该方式实现的步骤:

步骤一 : 编写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 <jni.h>
#include <string.h>
#include <stdio.h>
int main(void){//虚拟机创建所需的相关参数JavaVMOption options[1]; //相当于在命令行里传入的参数JNIEnv *env; //JNI环境变量JavaVM *jvm; //虚拟机实例JavaVMInitArgs vm_args; //虚拟机创建的初始化参数, 这个参数里面会包含JavaVMOptionlong status; //虚拟机启动是否成功的状态jclass cls; //将要寻找的Class对象jmethodID mid; //Class对象中的方法IdjfieldID fid; //Class对象中的属性Idjobject 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);// 调用指定的构造函数, 构造函数的名字叫做<init>mid = (*env)->GetMethodID(env, cls, "<init>", "()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++中使用的单字节的字符.

  1. 从C转换为java的字符, 使用NewStringUTF方法:

jstring arg = (*env)->NewStringUTF(env, name);

  1. 从java转换为C的字符, 使用GetStringUTFChars

const char* str = (*env)->GetStringUTFChars(env, result, 0);

步骤四 : 编译运行

编译运行,完成 C++对Java的调用。

三、JS与C++互调用 :JS引擎

首先需要解释一下,为什么我们需要了解一下 JS引擎的注入原理。大家都对 JSBridge 应该都不陌生,做过 webview 混合开发的会有更深刻的认识,它是 JS语言 与 非JS语言 实现双向通信的一种手段、是一个抽象的概念。JSBridge的实现方式包括:JavaScriptInterface、改写浏览器原有对象、URL Scheme(即Url拦截)。不管哪一种方式,其表现都是很不尽如人意的。为了大家能够了解的更清晰,下面会对这三种方式进行展开讲解。

3.1 为什么不使用Js直接调用 Java :JSBridge

3.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 ,最终是通过Chromium的IPC机制( 进程间通信IPC机制则是基于

Unix Socket通信协议实现的 ),发出了一条消息。该方式的通信效率是非常低下的。

  1. 改写浏览器原有对象

这主要是修改浏览器中全局对象 window 上的某些方法,进而拦截固定规则的参数,最后分发给Java对应的方法去处理。这里常用的是以下四个方法:

  • alert -- 可以被webview的 onJsAlert 监听
  • confirm -- 可以被webview的 onJsConfirm 监听
  • console.log -- 可以被webview的 onConsoleMessage 监听
  • prompt -- 可以被webview的 onJsPrompt 监听

这种方式局限性很强,也不适用于复杂业务的开发场景。

  1. URL scheme

它可以通过拦截跳转页面时的 URL请求,并解析这个scheme协议,按照一定规则捕获行为并交由Native(Android/Ios)解决。安卓和iOS分别用到拦截URL请求的方法是:

  • shouldOverrideUrlLoading
  • UIWebView的delegate

说白了就是类似拦截重定向,这种方式也远远不够灵活。

3.1.2 Java调用JS的实现方式

webView.loadUrl("javascript:callFromJava('call from java')");

3.2 基于JS 引擎实现 JS 与 C++通信

上面我们分析了 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<test*>(JSObjectGetPrivate(thisObject));t->func();return JSValueMakeUndefined(ctx);
}

4. 封装对 test.number 的 get 操作

JSValueRef getTestNumber(JSContextRef ctx, JSObjectRefthisObject, JSStringRef, JSValueRef*){test* t = static_cast<test*>(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

JSI是RN新架构实现JS与Native通信的基石,Turbomodules 也是基于 JSI 实现的。 对于了解RN新架构来说,先搞明白 JSI 是至关重要的,那下面就让我们来聊一聊 JSI。

1、什么是 JSI ?

JSI 的全称是 JavaScript Interface,即 JS Interface 接口,它是对 JS引擎 与 Native (C++) 之间相互调用的封装,通过 HostObject 接口实现双边映射,官方也称它为映射框架

有了这层封装,在 ReactNative 中有了两方面的提升:

  • 可以自由切换引擎,比如: JavaScriptCore、V8、Hermes等。
  • 在 JS 中调用 C++ 注入到 JS 引擎中的方法,数据格式是通过 HostObject 接口规范化后的,摒弃了旧架构中以 JSON 作为数据的异步机制,从而使得 JS 与 Native 之间的调用可以实现同步感知。

2、JSI、JS、JS Runtime、Native(C++)的关系

Js是运行在 Js Runtime 中的,所谓的方法注入,也就是将所需方法注入到 Js Runtime 中去,JSI 则负责具体的注入工作,通过 Js 引擎提供的 API,完成 C++ 方法的注入。上图就是 JS 与 Native(C++) 在 JSI 新架构中实现通信的简易架构。

那么接下来,就让我们继续来了解一下 JSI 是如何实现 JS 与 Native 互调通信的吧。

3、JSI 实际应用

接下来我们通过一个实际的例子,来了解下 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 <jni.h>
#include "../../../../../../node_modules/react-native/ReactCommon/jsi/jsi/jsi.h"using namespace facebook;extern "C" {JNIEXPORT void JNICALLJava_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> sampleModule);// 每一个 TurboModule -- SampleModule 中的所有方法和属性,都需要通过声明式注册,在 get 中进行声明jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override;
private:JNIEnv jniEnv_;
};

1.3 编写 C++ 文件,实现 SampleModule 相关逻辑

#include <jsi/jsi.h>
#include <jni.h>
#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;// 通过智能指针 实例化 SampleModuleauto testBinding = std::make_shared<SampleModule>();// 调用 SampleModule 的 install 方法,SampleModule::install(runtime, testBinding);// 获取并存储虚拟机实例 并存储到 &jvmenv->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> 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 中调用注入的方法

<Text style={styles.sectionTitle}>{global.NativeSampleModule.getStaticField()}
</Text>
<Text style={styles.sectionTitle}>{/* this is from C++ JSI bindings */}{global.NativeSampleModule.getStringPrivateField()}
</Text>

总结分析: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 (<View style={{}}></View>);
};export default App;

2.2 Native 调用 JS 全局方法

runtime.global().getPropertyAsFunction(*runtime, "jsMethod").call(*runtime, "message内容!");

注意内容: 我们需要通过 JSI 中的 getPropertyAsFunction() 来获取 JS 中的方法,但需要注意,getPropertyAsFunction() 获取的是 global 全局变量下的某个属性或方法,因此,我们在 JS 中声明一个需要被 Native 调用的方法的时候,需要显式的指定它的作用域。

4、JSI 与 JSC对比

首先需要声明一下,这里的 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 自由的定义自己的 通信桥?

RN通信底层原理 -- 总结篇相关推荐

  1. IO多路复用底层原理及源码解析

    基本概念 1. 关于linux文件描述符 在Linux中,一切都是文件,除了文本文件.源文件.二进制文件等,一个硬件设备也可以被映射为一个虚拟的文件,称为设备文件.例如,stdin 称为标准输入文件, ...

  2. 面试官再问你 HashMap 底层原理,就把这篇文章甩给他看

    来自:烟雨星空 前言 HashMap 源码和底层原理在现在面试中是必问的.因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合.文章较长,介绍了很多原理性的问题,希 ...

  3. 查询已有链表的hashmap_面试官再问你 HashMap 底层原理,就把这篇文章甩给他看...

    前言 HashMap 源码和底层原理在现在面试中是必问的.因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合.文章较长,介绍了很多原理性的问题,希望对你有所帮助~ ...

  4. 网络协议从入门到底层原理(11)网络爬虫、无线网络、HTTP缓存、即时通信、流媒体

    补充知识 网络爬虫 网络爬虫的简易实例 robots.txt 无线网络 HTTP 缓存(Cache) 缓存 - 响应头 缓存 - 请求头 缓存的使用流程 即时通信(IM) XMPP MQTT 流媒体 ...

  5. 网络协议从入门到底层原理(8)HTTPS(成本、通信过程、TLS1.2的连接,配置服务器HTTPS)

    HTTPS SSL / TLS OpenSSL HTTPS的成本 HTTPS的通信过程 TLS 1.2 的连接(ECDHE密钥交换算法) ① Client Hello ② Server Hello ③ ...

  6. iOS 底层探索篇 —— KVC 底层原理

    iOS 底层探索篇 -- KVC 底层原理 1. Method Swizzling的坑与应用 1.1 method-swizzling 是什么? 1.2 坑点 坑点1:method-swizzling ...

  7. 深入浅出TCP/UDP 原理-UDP篇(2)及完整MATLAB实现UDP通信

    目录 调试代码在文末 写在前面 3. UDP疑难杂症 3.1 UDP的传输方式:面向报文 3.2 UDP数据包的发送和接收问题 3.3 UDP丢包问题 3.4 UDP冗余传输 4 UDP真的比TCP要 ...

  8. 【SemiDrive源码分析】【MailBox核间通信】44 - 基于Mailbox IPCC RPC 实现核间通信(RTOS侧 IPCC_RPC Server 消息接收及回复 原理分析篇)

    [SemiDrive源码分析][MailBox核间通信]44 - 基于Mailbox IPCC RPC 实现核间通信(RTOS侧 IPCC_RPC Server 消息接收及回复 原理分析篇) 一.RT ...

  9. Spring 事务原理篇:@EnableTransactionManagement注解底层原理分析技巧,就算你看不懂源码,也要学会这个技巧!

    前言 学习了关于Spring AOP原理以及事务的基础知识后,今天咱们来聊聊Spring在底层是如何操作事务的.如果阅读到此文章,并且对Spring AOP原理不太了解的话,建议先阅读下本人的这篇文章 ...

  10. Spring5底层原理 学习笔记(二)AOP篇

    文章目录 AOP实现之ajc编译器 AOP实现之agent类加载 AOP实现之动态代理 jdk动态代理 演示 模拟实现动态代理 动态生成代理类需要使用到asm的api,这里就不展开了 Jdk对于反射调 ...

最新文章

  1. 现在的编译器还需要手动展开循环吗_性能 - 如果有的话,循环展开仍然有用吗?...
  2. Math类(工具类)
  3. Linux之Sed详解
  4. S5PV210开发 -- I2C 你知道多少?(二)
  5. 大数据WEB阶段(一)XML文件的操作
  6. 郑州百知面试题 SSM试题三
  7. Linux 安装Zookeeper单机版(使用Mac远程访问)
  8. AutoCAD.NET API 最新(2012)教程下载及在线视频教程DevTV
  9. observable_Java Observable addObserver()方法与示例
  10. 为什么玩我的世界老提示Java se错误_我的世界error错误信息 error could解决方法
  11. 计算机办公应用软件初级,电脑办公软件有哪些?桌面便签办公软件基础教程
  12. pytorch中CrossEntropyLoss和NLLLoss的区别与联系
  13. vue element dialog 对话框
  14. 搭建php环境,更换织梦服务器
  15. Apple M1 开启HiDPI的新方法,无需关闭SIP,无需SwitchResX
  16. 海康威视相机 RTSP 传输延迟解决方案
  17. CSR8670项目实战:BlueSiri轻智能蓝牙音箱
  18. 订单管理_01新增订单信息流程
  19. 计算机主板供电故障,电脑主板内部电池供电出现问题,会出现哪些故障问题
  20. 亮剑java web_为什么《亮剑Java Web 项目开发案例导航》第二个项目运行不了?

热门文章

  1. build Variants创建不同配置的app(图文详解)
  2. infer的用法_infer使用的浅谈简析
  3. 2021 年第一次做核酸检测是什么体验
  4. 信息学奥赛一本通 1296:开餐馆(evd)
  5. Realtek 1296 (RTD1296) OpenWRT Android 双系统全功能开发板
  6. python numpy 计算标准差
  7. MATLAB学习(一)——————format 命令
  8. 如何修改Tomcat的默认主页
  9. EXCEL 合并同样格式的多个excel文件
  10. Hexo+GithubPage搭建Fan主题的博客(7)配置自定义域名