JSI是RN新架构实现JS与Native通信的基石,Turbomodules 也是基于 JSI 实现的。 对于了解RN新架构来说,先搞明白 JSI 是至关重要的,那下面就让我们来聊一聊 JSI。
JSI 的全称是 JavaScript Interface,即 JS Interface 接口,它是对 JS引擎 与 Native (C++) 之间相互调用的封装,通过 HostObject 接口实现双边映射,官方也称它为映射框架。
有了这层封装,在 ReactNative 中有了两方面的提升:
由于JSI 的实现是基于 JS 引擎提供的 API 来实现的,为此我们先来了解下 JS 引擎的注入原理。说到向引擎注入方法,相信大家都用过 console.log()、setTimeout()、setInterval() 等方法,像这些方法就是通过 polyfill 的方式注入到 JS 引擎里的,JS引擎内部本身是没有这些方法的 (这些方法的实现,我们会在后面进行讲解)。沿着这个思路,我们可以通过向引擎注入方法和变量的方式,来实现在 JS 中调用注入的 Native(C++) 方法。
这里我们会以 V8 引擎为例,了解一下 JS 引擎的注入原理。(C++ 嵌入 v8 引擎的教程,感兴趣的同学可以看这里 C++嵌入JS引擎)
向引擎注入额外方法,并实现 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.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 运行环境中,调用我们注入的方法和变量了。
Js是运行在 Js Runtime 中的,所谓的方法注入,也就是将所需方法注入到 Js Runtime 中去,JSI则负责具体的注入工作,通过 Js 引擎提供的 API,完成 C++ 方法的注入。上图就是 JS 与 Native(C++) 在 JSI 新架构中实现通信的简易架构。
那么接下来,就让我们继续来了解一下 JSI 是如何实现 JS 与 Native 互调通信的吧。
接下来我们通过一个实际的例子,来了解下 JSI 是如何实现 JS 与 Native (C++)通信的,首先我们先来看一下 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 的通信方式。
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 都是通过向 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 的通信效率。
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 都在里面。
那么我们可以通过 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 是一个顶级全局对象,window、globalThis 以及常用的 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 声明:
具体是在哪里执行的 global.setTimeout 呢? 看下面的文件:
好了,详细的实现流程我们在这里就不深究了,感兴趣的同学下来可以跟着源码更深入的研究一下。
源码地址: react-native/setUpTimers.js at 8bd3edec88148d0ab1f225d2119435681fbbba33 · facebook/react-native