RN通信底层原理 -- 总结篇

        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 
#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 两种注册方式的区别

我们用一张图来展现一下两种方式在创建流程上的区别:

RN通信底层原理 -- 总结篇_第1张图片

静态注册:

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 函数的调用

二、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 
#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++中使用的单字节的字符.

  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(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

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++)的关系

RN通信底层原理 -- 总结篇_第2张图片

        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 
#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 调用的方法的时候,需要显式的指定它的作用域。

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 自由的定义自己的 通信桥?

你可能感兴趣的:(ReactNative,android,react,native)