react native 老架构
即和 weex 架构类似,js 端和 na 端通过跨端桥通信,两者相互独立。在 js 端不能直接获取设备信息,需要通过桥调用 na 的方法。
在 na 端,一般使用 objc 和 java 相应模块方法供 js 端调用。js 引擎在 java 中,需要通过 JNI 调用 c++和 c 的代码;同时在 ios 中,objc 可直接调用 c++ 和 c 的代码,若是 swift,需要通过 objc 或 c。
跨端桥调用中,两端交流需要将信息序列化为 json:
在 React Native 应用程序中,示例相机打开流程如图所示:
Bridge 三个特点:
- 异步。这些消息队列是异步的,无法保证处理事件。
- 序列化。通过 JSON 格式来传递消息,每次都要经历序列化和反序列化,开销很大。
- 批处理。对 Native 调用进行排队,批量处理。
异步设计的好处是不阻塞,这种设计在大部分情况下性能满足需求,但是在某些情况下就会出问题,比如瀑布流滚动。
当瀑布流向下滑动的时候,需要发请求给服务端拿数据进行下一步渲染。
滚动事件发生在 UI thread,然后通过 Bridge 发给 JS thread。JS thread 监听到消息后发请求,服务端返回数据,再通过 Bridge 返回给 Native 进行渲染。由于都是异步,就会出现空白模块,导致性能问题。
从上面可以看出,性能瓶颈主要是存在 JS 线程和 Native 有交互的情况,如果不存在交互,RN 的性能良好。
与本机通信相比,此操作非常慢。出于这个原因,需要用新的架构替换 Bridge。
因此,对于 RN 的优化,主要集中在 Bridge 上,有下面 3 个原则:
- JS 和 Native 端不通信。最彻底的方式,消息不走 Bridge。
- JS 和 Native 减少通信。在两端无法避免的情况下,尽量通信减少次数。比如多个请求合并成一个。
- 较少 JSON 的大小。比如图片转为 Base64 会导致传输数据变大,用网络图片代替。
JSI 简介
JSI 是整个新架构的核心和基石,所有的一切都是建立在它上面。
JSI 是 Javascript Interface 的缩写,一个用 C++ 写成的轻量级框架,其作用就是通过 JSI,JS 对象可以直接获得 C++ 对象 (Host Objects) 引用,并调用对应方法。
另外 JSI 与 React 无关,可以用在任何 JS 引擎(V8, Hermes, JSC)。
有了 JSI,JS 和 Native 就可以直接通信了, 调用过程如下:
JS->JSI->C++->ObjectC/Java
自此三个线程通信再也不需要通过 Bridge,可以直接知道对方的存在,让同步通信成为现实。具体的用法可以看 官方例子。
另外一个好处就是有了 JSI,JS 引擎不再局限于 JSC,可以自由的替换为 V8, Hermes,进一步提高 JS 解析执行的速度。
跨端桥和 JSI 的不同
JSI 创建了比 Bridge 更高性能的结构,因为它提供了对 JS 运行时的更快、更直接的访问。
另一方面,在 Bridge 中,JS 和 Native 端通信异步发生,消息以批处理形式进行,需要简单的操作(例如添加 2 个数字)才能使用 await 关键字。
由于在 JSI 中,默认情况下一切都是同步的,因此它们也可以在顶级作用域中使用。当然,可以为长时间运行的操作创建异步方法,并且可以轻松使用 promise。
缺点是,由于 JSI 访问 JS 运行时,因此无法使用 Google Chrome 等远程调试器。取而代之的是,可以使用 Flipper 桌面应用程序来调试应用程序。
因为 JSI 成为原生实现的抽象层,不需要直接使用 JSI,也不需要知道 C++的内部结构。我们只是像以前一样从 JS 端调用原生函数。此外,Turbo Modules API 与 Native Modules API 几乎相同。因此,RN 生态系统中的每个现有原生模块都可以轻松迁移到 Turbo 模块,而无需从头开始重写。
JSI 使用示例
使用 create-react-native-library 创建模块:
npx create-react-native-library react-native-awesome-module
选择相应选项,注意选择 C++ for iOS and Android
通过这种方式创建的模块,其中主要的逻辑一般集中在 cpp/
中,而 android/
和 ios/
中仅负责引用 cpp/
的方法:
cpp-adapter.cpp
这是 Java 本机接口 (JNI) 适配器,它允许 java 和本机 c++ 代码之间的双向通信。我们可以从 java 调用 c++ 代码,从 c++ 调用 java 代码。
在 src
中:
ios 中,在 objc 可直接使用 c++模块:
前端调用如下:
JavaScript 和 JSI 中的方法定义
另外,一般可以在 JavaScript 端定义函数,如下所示:
// JS
const add = (first, second) => {
return first + second
}
要在 C++ 端创建一个函数并在 JavaScript 端使用它,createFromHostFunction
方法的使用方式如下:
// JSI (C++)
auto add = jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "add"), // add function name
2, // first, second variables (2 variables)
[](
jsi::Runtime& runtime,
const jsi::Value& thisValue,
const jsi::Value* arguments, // function arguments
size_t count
) -> jsi::Value {
double result = arguments[0].asNumber() + arguments[1].asNumber();
return jsi::Value(result);
}
);
JSI 中的数字始终是双精度型。如上所述创建的 add 方法可以直接在 JavaScript 端使用
// JavaScript
const result = add(5, 8)
此方法也可以在C++端使用:
// JSI (C++)
auto result = add.call(runtime, 5, 8);
当然,为了如上所述使用全局命名空间中的 add 函数,我们需要定义如下:
// JSI
global.add = add;// JSI (C++)
runtime.global().setProperty(runtime, "add", std::move(add));
如果我们将我们创建的方法与桥接中的其他本机模块进行比较,我们可以注意到这个函数没有被定义为异步,因此是同步运行的。正如你在这里看到的,一个操作的结果是在 host 函数中创建的,并直接由 JS 端使用。如果 add 函数是 bridge 函数,我们需要与 await
关键字一起使用,如下所示:
const resul = await global.add(5, 2)
正如你所注意到的,JSI 函数是直接的、同步的,并且是 JavaScript 运行时中最快的调用方法。
如果我创建一个方法获取 ip 地址。我们首先需要创建一个在 C++ 中返回 IP 地址的方法,然后我们使用 global
属性在 JS 端加载这个函数,最后简单地调用该函数。现在,我们不需要使用 await
关键字,因为我们可以直接调用函数,并且我们可以像调用任何其他 JS 方法一样调用它。此外,由于没有序列化过程,我们从额外的处理负载中解脱出来。
// JSI (C++)
auto getIpAddress = jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "getIpAddress"),
0, // Takes no parameter so it have 0 parameters
[](
jsi::Runtime& runtime,
// thisValue, arguments ve count variables not necessary
const jsi::Value&,
const jsi::Value*,
size_t
) -> jsi::Value {
// iOS or android side method will be called
auto ip = SomeIosApi.getIpAddress();
return jsi::String::createFromUtf8(runtime, ip.toString());
}
);
runtime.global().setProperty(runtime, "getIpAddress", std::move(getIpAddress));
之后,我们可以从 JS 端调用它:
// JavaScript
const ip = global.getIpAddress();
将原生模块转换为 JSI 模块
以 react-native-mmkv 为例:
// cpp-adapter.cpp
...
void install(jsi::Runtime& jsiRuntime) {
// MMKV.createNewInstance() 添加 JSI 绑定
auto mmkvCreateNewInstance = jsi::Function::createFromHostFunction(
jsiRuntime, jsi::PropNameID::forAscii(jsiRuntime, "mmkvCreateNewInstance"),
1,
[](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments,
size_t count) -> jsi::Value {
if (count != 1) {
throw jsi::JSError(runtime, "MMKV.createNewInstance(..) expects one argument (object)!");
}
jsi::Object config = arguments[0].asObject(runtime);
std::string instanceId = getPropertyAsStringOrEmptyFromObject(config, "id", runtime);
std::string path = getPropertyAsStringOrEmptyFromObject(config, "path", runtime);
std::string encryptionKey =
getPropertyAsStringOrEmptyFromObject(config, "encryptionKey", runtime);
auto instance = std::make_shared<MmkvHostObject>(instanceId, path, encryptionKey);
return jsi::Object::createFromHostObject(runtime, instance);
});
jsiRuntime.global().setProperty(jsiRuntime, "mmkvCreateNewInstance",
std::move(mmkvCreateNewInstance));
// Adds the PropNameIDCache object to the Runtime. If the Runtime gets destroyed, the Object gets
// destroyed and the cache gets invalidated.
auto propNameIdCache = std::make_shared<InvalidateCacheOnDestroy>(jsiRuntime);
jsiRuntime.global().setProperty(jsiRuntime, "mmkvArrayBufferPropNameIdCache",
jsi::Object::createFromHostObject(jsiRuntime, propNameIdCache));
}
...
创建自定义 host object
通过创建自定义 host object
,可以返回不同类型的数据,如下所示:
#pragma once
#include <jsi/jsi.h>
#include <jni.h>
#include <fbjni/fbjni.h>
using namespace facebook;
class ExampleHostObject : public jsi::HostObject {
public:
explicit ExampleHostObject() {}
public:
std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime& rt) override {
std::vector<jsi::PropNameID> result;
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("someValue")));
return result;
}
jsi::Value get(jsi::Runtime&, const jsi::PropNameID& propName) override {
auto name = propName.utf8(runtime);
if (name == "someValue") {
// number
return jsi::Value(13);
}
if (name == "someBool") {
// bool
return jsi::Value(true);
}
if (name == "someString") {
// string
return jsi::String::creaeFromUtf8(runtime, "Hello!");
}
if (name == "someObject") {
// object
auto object = jsi::Object(runtime);
object.setProperty(runtime, "someValue", jsi::Value(13));
object.setProperty(runtime, "someBool", jsi::Value(true));
return object;
}
if (name == "someArray") {
// array
auto array = jsi::Array(runtime, 2);
array.setValueAtIndex(runtime, 0, jsi::Value(13));
array.setValueAtIndex(runtime, 1, jsi::Value(true));
return array;
}
if (name == "someHostObjec") {
// object (C++)
auto newHostObject = std::make_shared<ExampleHostObject>();
return jsi::Object::createFromHostObject(runtime, newHostObject);
}
if (name == "someHostFunction") {
// function
auto func = [](
jsi::Runtime& runtime,
const jsi::Value& thisValue,
const jsi::Value* arguments,
size_t count
) -> jsi::Value
{
double result = arguments[0].asNumber() + arguments[1].asNumber();
return jsi::Value(result);
};
return jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "func"),
2, // first, second
func);
}
return jsi::Value::undefined();
}
};
总结
使用 JSI,使得这些模块和相应应用程序的性能提高。但是以 create-react-native-library
为例,其创建 turbo module
才有使用 cpp 的机会,仅创建 UI 组件和视图或使用老架构一般不会涉及 cpp。
创建 JSI 为基的组件,比如 react-native-mmkv, 至少需要涉及 3 种语言。将逻辑集中于 cpp/
,一方面降低了重复逻辑代码的编写,另一方面需要开发者更高的素养,不过总的来说,提高了 js 和 na 端的通信效率。参考 StorageBenchmark 通过从存储中读取值 1000 次来将常用的存储库相互比较:
总的来说,react-native 的相关组件零散且繁多,生态更是庞大,这里所触不过冰山一角,JSI 是新架构的核心,杰出的想法和框架。