RN新架构 JSI 介绍

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

一、什么是 JSI ?

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

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

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

二、JS引擎注入原理

        由于JSI 的实现是基于 JS 引擎提供的 API 来实现的,为此我们先来了解下 JS 引擎的注入原理。说到向引擎注入方法,相信大家都用过 console.log()、setTimeout()、setInterval() 等方法,像这些方法就是通过 polyfill 的方式注入到 JS 引擎里的,JS引擎内部本身是没有这些方法的 (这些方法的实现,我们会在后面进行讲解)。沿着这个思路,我们可以通过向引擎注入方法和变量的方式,来实现在 JS 中调用注入的 Native(C++) 方法。

        这里我们会以 V8 引擎为例,了解一下 JS 引擎的注入原理。(C++ 嵌入 v8 引擎的教程,感兴趣的同学可以看这里 C++嵌入JS引擎)

1. 向v8引擎中注入方法

向引擎注入额外方法,并实现 JS 调用,基本思想都是一致的,都是要将 JS 所需的内容设法绑定到 JS 执行环境下的 全局对象 global 上。这样在 JS 侧就可以通过 global 来获取注入的内容了,当然我们也可以不直接在 global 中来获取,而是写个 js module,然后重新导出并用 declare 声明,用法类似直接使用 console.log()。

实现步骤如下:

1.1 我们用 C++ 实现一个打印字符串的方法 Print()

void Print(const v8::FunctionCallbackInfo& args) {
    for (int i = 0; i < args.Length(); i++) {
        v8::HandleScope handle_scope(args.GetIsolate());
        v8::String::Utf8Value str(args.GetIsolate(), args[i]);
        const char* cstr = ToCString(str);
        printf("%s", cstr);
    }

    printf("\n");
    fflush(stdout);
}

1.2 将 Print() 方法加入到 Js global 中

// 根据 v8 实例 isolate 获取 JS 中的 global 对象
v8::Local global = v8::ObjectTemplate::New(isolate);

// 将 global 关联到 JS 执行上下文 Context 中
v8::Local context = v8::Context::New(isolate, NULL, global);

// 向 global 中 set 一个属性名为 print 的方法。可以理解为: global.print = Print
context->Global()->Set(context, v8::String::NewFromUtf8(isolate, "print", v8::NewStringType::kNormal).ToLocalChecked(),v8::FunctionTemplate::New(isolate, Print));

ObjectTemplate 是一个 JS 引擎 提供的 JS 对象模版,通过它我们可以创建一个 JS 对象。通过 context.Global(),可以获取到 Js global 全局对象。

1.3 执行C++ 注入到 JS 的方法 print

// 转换成 v8::Local类型 -- (方便垃圾回收)
v8::Local source = v8::String::NewFromUtf8Literal(isolate, "print('Hello World')");

// 将 Js 代码进行编译
v8::Local script = v8::Script::Compile(context, source).ToLocalChecked();

// 运行 Js 代码
v8::Local result = script->Run(context).ToLocalChecked();

// 输出 result 为:Hello World

2. 向v8引擎中注入变量

实现步骤如下:

2.1 注入一个变量到 v8 引擎

// 首先通过 Js 引擎实例 isolate 获取 Js global 对象
v8::Local global = v8::ObjectTemplate::New(isolate);

// 将 global 绑定到 JS 执行上下文 Context 中
v8::Local context = v8::Context::New(isolate, NULL, global);

// 创建一个临时对象 temp
Local temp = v8::ObjectTemplate::New(isolate);

// temp 上加入 x 属性
temp->Set(isolate, "x", v8::Number::New(isolate, 12));

// temp 上加入 y 属性
temp->Set(isolate, "y",v8::Number::New(isolate, 10));

// 创建 temp 的实例 instance
Local instance = temp->NewInstance(context).ToLocalChecked();

// global 中加入属性 options,其值为 instance,可以理解为: global.options
context->Global()->Set(context, String::NewFromUtf8Literal(isolate, "options"),instance).FromJust();

2.2 执行C++ 注入到 JS 的对象

// 类型转换 (方便垃圾回收)
v8::Local source = v8::String::NewFromUtf8Literal(isolate, "options.x");

// 将 Js 代码进行编译
v8::Local script = v8::Script::Compile(context, source).ToLocalChecked();

// 运行 Js 代码
v8::Local result = script->Run(context).ToLocalChecked();

// 输出 result 为:12

实现原理分析:v8 引擎中 方法和变量的注入,实际上是借助于 Js 执行上下文 Context 来实现的。首先需要获取这个 Context 上下文 和 JS global ,然后将 需要注入的 方法 和 变量 设置到 global 当中,最后再将 global 与 Context 进行关联,就完成了向引擎注入的操作。这样就可以在 Js 运行环境中,调用我们注入的方法和变量了。

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

RN新架构 JSI 介绍_第1张图片

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

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

四、举例说明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(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 调用的方法的时候,需要显式的指定它的作用域。

五、JSI 与 JSC(不是JavaScriptCore脚本引擎) 对比

相同点:

首先在底层实现上来说,JSI 与 JSC 都是通过向 JS 引擎中注入方法,来实现的 JS 与 Native 通信,同时 注入的方法也都是挂载到了 JS global 全局对象上面。

不同点:

旧架构中的 JSC 处理的注入对象是JSON 对象与C++ 对象,内部涉及复杂且频繁的类型转换。且在

JSBridge 这种异步传输的设计中存在三个线程之间的通信:UI线程、Layout线程、JS线程,在典型的列表快速滑动时出现空白页的例子中,效率低下得到明显的体现。

而对于 JSI 来讲,弃用了异步的bridge,传输的数据也不再依赖于 JSON 的数据格式,而是将HostObject 接口作为了双边通信的协议,实现了双边同步通信下的高效信息传输。

另外编写 NativeModule 的方式与旧架构中相比发生了改变,除了功能之外的逻辑,需要在一个 C++ 类中来完成。 因此,一个 TurboModule 的实现分为两部分: C++ & Java (OC)。

一句话概括 JSI 提效的本质JSI 实现了通信桥 Bridge 的自定义,并通过 HostObjec 接口协议的方式取代了 旧架构中基于异步 bridge 的JSON数据结构,从而实现了同步通信,并且避免了 JSON 序列化与反序列化的繁琐操作,大大提升了 JS 与 Native 的通信效率。

六、对 RN global 对象的分析

1. RN 中的 global 与 JS 引擎中的 global 的关系

在 JS 引擎中操作的 全局对象 global 是挂在到 JS 执行上下文环境 Context 上的。

该 global 对象 与 RN 里使用的 global 是同一个 global 对象,HouseRN只是扩展了 global 的声明,下面我们通过一个例子来讲解一下:

在RN的开发中,我们调用一个NativeModule是这样调用的:

import { NativeModules } from "react-native";

//调用获取数据

NativeModules.BrokerData.user()

但是通过 NativeModules.js 的源码可以知道,也可以通过 global 来对NativeModules 进行调用,当然RN官网已经帮我们封装好了调用的方式,这里我们只是为了验证一下 global 这个全局对象的作用域,下面我们首先看一下 NativeModules.js 源码实现:

let NativeModules : {[moduleName: string]: Object} = {};
if (global.nativeModuleProxy) {
    NativeModules = global.nativeModuleProxy;
} else {
    const bridgeConfig = global.__fbBatchedBridgeConfig;
    invariant(bridgeConfig, '__fbBatchedBridgeConfig is not set, cannot invoke native modules');
(bridgeConfig.remoteModuleConfig || []).forEach((config: ModuleConfig, moduleID: number) => {
    // Initially this config will only contain the module name when running in JSC. The actual
    // configuration of the module will be lazily loaded.
    const info = genModule(config, moduleID);
    if (!info) {
        return;
    }
    if (info.module) {
        NativeModules[info.name] = info.module;
    }
    // If there's no module config, define a lazy getter
    else {
        defineLazyObjectProperty(NativeModules, info.name, {
            get: () => loadModule(info.name, moduleID)
        });

    }
  });
}

阅读上面的源码可以知道,理论上我们也可以通过 global 来获取一个 NativeModule ,我们先来看一下,在调试模式下 global 中的内容,可以看到所有已注册的 NativeModule 都在里面。

RN新架构 JSI 介绍_第2张图片


RN新架构 JSI 介绍_第3张图片

那么我们可以通过 global 这样来获取一个已注册的 NativeModule:

// 索引 24 代表 BrokerData 的所在位置,索引 1 代表返回的数据体。

// 这种调用方式 与上面的调用方式是等价的

global.__fbBatchedBridgeConfig.remoteModuleConfig[24][1]

回过头来,我们看一下 __fbBatchedBridgeConfig 变量是在哪里被赋值的,来看下 C++ 侧的实现:

void ProxyExecutor::initializeRuntime() {
    // .......
    {
        SystraceSection t("setGlobalVariable");
        setGlobalVariable(
                "__fbBatchedBridgeConfig",
                std::make_unique(folly::toJson(config)));
    }
}
void JSCExecutor::setGlobalVariable(std::string propName, std::unique_ptr jsonValue) {
    // ....
    auto valueToInject = Value::fromJSON(m_context, jsStringFromBigString(m_context, *jsonValue));
    Object::getGlobalObject(m_context).setProperty(propName.c_str(), valueToInject);
}

由代码可知,最终是通过 JS 引擎的

Object : : getGlobalObject(context).setProperty("propertyName", value) 方法,将 "__fbBatchedBridgeConfig" 注入到了 global 全局变量当中。 其中 Object : : getGlobalObject(context) 是 JSC 脚本引擎提供的 API (与 V8 引擎提供的类似),用于获取一个 JSGlobalObject (即 JS的 global 全局对象)。

2. RN 工程中的 global、window、globalThis 的关系

首先我们来引用一个官方一点的说法:

听上去大家有什么感受呢,懂了而又模糊对吧。那么我们通过具体的例子感受一下:

global.window.alert("11111")  // 调用成功
global.alert(222)             // 调用成功
window.alert(333)             // 调用成功
alert(444) // 调用成功
globalThis.window.alert(555); // 调用成功
globalThis.alert(666)         // 调用成功
alert(global.globalThis)      // 调用成功
global.setTimeout(() =>{      // 调用成功
    console.log("aaaaaaa")
}, 1000);
window.setTimeout(() =>{      // 调用成功
    console.log("bbbbbbb")
}, 1000);
window.global.alert(999)      // 失败 (undefined)

global.window.a = 122;
alert(a);                     // 调用成功
alert("hhh " + window.a)      // 调用成功
alert("xx " + global.a)       // 调用成功

        RN中的 global 是一个顶级全局对象,windowglobalThis 以及常用的 API 包括:alert()、setTimeout、setInterval 等都挂在了 global 这个全局对象下面。而像 alert()、setTimeout、setInterval()、console、JSON 等这些顶层API,也可以无需倒入并直接调用,其原因则是在 RN core 包中通过 declare 做了相应类型的声明。

3. 聊聊 setTimeout 的实现

        在 RN 中,像 setTimeout 这样的顶层函数是如何实现的呢?这里我们先给出答案,再来进行分析。其实 setTimeout 这种顶层函数,并不是由 JS 来实现的,而是由 Native 来实现,并借助 JS 引擎中的 polyfill 方式从外部注入到 JS 执行环境当中。接下来我们简单看一下源码实现:

首先我们通过 RN 源码,找到 setupTimer.js 文件,核心代码如下:

 
  
'use strict';
const {polyfillGlobal} = require('../Utilities/PolyfillFunctions');
const {isNativeFunction} = require('../Utilities/FeatureDetection');
//...........
if (global.RN$Bridgeless !== true) {
    const defineLazyTimer = (
      name:
        | $TEMPORARY$string<'cancelAnimationFrame'>
        | $TEMPORARY$string<'cancelIdleCallback'>
        | $TEMPORARY$string<'clearInterval'>
        | $TEMPORARY$string<'clearTimeout'>
        | $TEMPORARY$string<'requestAnimationFrame'>
        | $TEMPORARY$string<'requestIdleCallback'>
        | $TEMPORARY$string<'setInterval'>
        | $TEMPORARY$string<'setTimeout'>,
    ) => {
        polyfillGlobal(name, () => require('./Timers/JSTimers')[name]);
    };

    // 定义 'setTimeout'
    defineLazyTimer('setTimeout');
    defineLazyTimer('clearTimeout');
    defineLazyTimer('setInterval');
    defineLazyTimer('clearInterval');
    defineLazyTimer('requestAnimationFrame');
    defineLazyTimer('cancelAnimationFrame');
    defineLazyTimer('requestIdleCallback');
    defineLazyTimer('cancelIdleCallback');
}

代码中我们可以找到定义 setTimeout 的地方:defineLazyTimer('setTimeout'),继而调用了 polyfillGlobal(), 它就是向 global 上注入成员的一个操作,同时我们看到与之对应的具体实现则交给了 JSTimers.js ,我们跟进去看一下代码

/**
 * JS implementation of timer functions. Must be completely driven by an
 * external clock signal, all that's stored here is timerID, timer type, and
 * callback.
 */
const JSTimers = {

    /**
     * @param {function} func Callback to be invoked after `duration` ms.
     * @param {number} duration Number of milliseconds.
     */
    setTimeout: function(func: Function, duration: number, ...args?: any): number {
        const id = _allocateCallback(
            () => func.apply(undefined, args),
           'setTimeout'
        );
        RCTTiming.createTimer(
            id, duration || 0, Date.now(),
            /* recurring */ false
        );
        return id;
    },

    //...........
    clearTimeout: function(timerID: number) {
        _freeCallback(timerID);
    },
    //...........
};
module.exports = JSTimers;

由源码可知,最终调用的是 RCTTiming 的 createTimer(),而 Timing 则是 Nativemodules 中获取的一个 NativeModule,也就是 Timing 是由 Native 来实现的。因此也就是说 setTimeout 是由Native实现的,并通过 polyfill 的方式注入到 global 全局对象中,至于为什么像 setTimeout 这样挂在 global 下的全局变量为什么可以直接使用,这个其实是 RN 对其进行了 declare 声明:

RN新架构 JSI 介绍_第4张图片 具体是在哪里执行的 global.setTimeout 呢? 看下面的文件:

RN新架构 JSI 介绍_第5张图片

 好了,详细的实现流程我们在这里就不深究了,感兴趣的同学下来可以跟着源码更深入的研究一下。

源码地址: react-native/setUpTimers.js at 8bd3edec88148d0ab1f225d2119435681fbbba33 · facebook/react-native

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