本文由 简悦SimpRead 转码,原文地址 engineering.teknasyon.com
在去年11月,我谈到了React Native JSI架构。而在这篇文章中,我将ta......
图片来源。cornerstoneseducation.co.uk/news/what-t…
在去年11月,我谈到了React Native JSI架构。而在这篇文章中,我将谈论这个新的架构以及如何在我们的项目中使用它。但首先,我们需要提到旧的React Native架构。
旧的React Native架构如何工作
正如你们中的一些人所知,在React Native中,JS端和Native端在Bridge基础上进行通信。因为JS端已经有了一个很好的隔离环境,它没有任何机制来与Native端对话。例如,你无法访问设备名称,或者在不创建Native模块的情况下,你无法获得当前设备的本地IP地址。
你需要创建Native模块来获得当前设备的本地IP地址。
在本地模块中,你用Java和ObjC语言创建API,从你的JS代码中访问本地端。而且这种情况不仅仅是React Native特有的。在Java中,你需要使用JNI来调用C++和本地C代码。同样地,你需要使用C或ObjC语言创建一个桥接层来调用Swift中的C++ APIs。在React Native中,Bridge结构以同样的方式工作。
React Native中的桥接结构能够通过JSON消息进行通信。
桥梁提供了一个隧道,通过这个隧道,它在JS和Native之间进行消息传输。例如,为了获得一个设备的IP地址,我们需要调用Native端的 "getIPAddress"方法。当原生端获得这个消息时,它就得到了设备的真实IP地址,之后,它把这个地址包在另一个消息中,并通过Bridge把它发送给JS端。JS端从消息中获得这个IP地址,并可以在屏幕上显示。
在一个React Native应用程序中,相机开启流程示例如图所示
正如你们中的一些人所知,桥接流程并不是调用Native端的理想解决方案。它对这些消息进行了批处理。因为批处理系统对这些消息进行批处理,所以在用户设备上会发生一些滞后。由于这个原因,消息不能被即时发送到另一方。此外,还需要进行一些序列化操作,将消息封装成JSON格式。例如,即使你想发送一个简单的数字变量到Native端,这个变量也必须被转换为JSON字符串。而这一操作与原生通信相比是非常缓慢的。出于这个原因,我们需要用新的架构来取代Bridge。
JSI的出现
JSC(JavaScript Core)、Hermes和V8引擎都支持JSI。
诸如JSC、Hermes和V8等JavaScript运行时是用C和C++编写的,因为它们需要在高性能下工作。想利用这种情况的开发者们创建了可以与原生端对话的C++ APIs。而他们称之为JSI(JavaScript Interface)。JSI为JavaScript Runtime提供一个抽象层。你可以把这个层看作是面向对象语言中接口的概念。在这些语言中,你指定要在接口中定义的功能,实现它的类就有义务覆盖它。同样地,通过在JSI侧执行这个操作,你可以直接从C++侧发送一个数字值,并由JavaScript侧获得它,而无需进行类型转换。
JavaScript和JSI中的变量定义
数字类型定义
在JavaScript侧,我们知道如何定义一个数字变量,如下。
// Javascript
const number = 42
那么,我们如何在C++端定义一个变量并将其传递给JS端?首先,让我们仔细看看数字在C++中是如何定义的。
// JSI (C++)
jsi::Value number = jsi::Value(42);
正如你在这里看到的,使用jsi命名空间中的**Value()**构造函数创建了一个Value类的实例。之后,这个实例可以直接在JavaScript端作为一个数字使用。
字符串类型定义
同样的,字符串变量在JS和C++方面的定义如下。
// JavaScript
const name = "Marc"
// JSI (C++)
jsi::Value name = jsi::String::createFromUtf8(runtime, "Marc")
这里,字符串变量是使用jsi命名空间中String的createFromUtf8方法创建的。
函数定义
如你所知,函数可以在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函数,我们需要对它进行如下定义。
// Javascript
global.add = add;
// JSI (C++)
runtime.global().setProperty(runtime, "add", std::move(add));
如果我们将我们创建的方法与桥梁中的其他本地模块进行比较,我们可以注意到这个函数没有被定义为同步,因此是同步运行的。正如你在这里看到的,操作的结果是在宿主函数中创建的,直接被JS端使用。如果add函数是一个桥接函数,我们需要与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();
Bridge和JSI的区别
综上所述,我们可以说JSI技术将取代Bridge。虽然JSI和Bridge会在项目中存在一段时间,但Bridge很快就会被完全删除,所有的本地模块都会使用JSI。JSI创造了一个比Bridge更高性能的结构,因为它既能提供更快的速度,又能直接访问JS运行时间。
另一方面,在Bridge中,JS和Native端的通信是异步进行的,消息是以批处理的,像2个数字相加这样的简单操作是需要使用await关键字的。
由于在JSI中一切都默认为同步工作,所以它们也可以在顶层范围内使用。当然,对于长期运行的操作可以创建异步方法,并且可以很容易地使用承诺。
作为一个缺点,由于JSI访问JS运行时,不可能使用像谷歌浏览器那样的远程调试器。与此相反,我们可以使用Flipper桌面应用程序来调试我们的应用程序。
因为JSI成为了本地实现的抽象层,我们不需要直接使用JSI,也不需要了解C++内部。我们只需像以前那样从JS端调用本地函数。另外,Turbo Modules的API与Native Modules的API几乎一样。因此,RN生态系统中现有的每个Native Module都可以很容易地迁移到Turbo Modules中,而不需要从头开始重写。
现在,仔细看看MMKV库,了解JSI的实现方式。
MMKV库作为JSI的一个例子
react-native-mmkv是一个库,它在JSI的帮助下执行一个简单的键值存储操作。通过同步调用,它执行读/写操作的速度比AsyncStorage快30倍。
从iPhone 8设备上的存储中读取操作时间(ms)1000倍。而MMKV约为10ms,AsyncStorage约为230ms。大约,MMKV比AsyncStorage快23倍。
由于这些特点,mmkv库也是JSI的一个很好的例子。让我们看一下JSI实现的Android项目结构。我们可以从MainApplicaton.java开始,看看它是如何实现的。
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost =
new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return new PackageList(this).getPackages();
}
@Override
protected String getJSMainModuleName() {
return "index";
}
@Override
protected JSIModulePackage getJSIModulePackage() {
return new MmkvModulePackage();
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
}
与Bridge不同,JSI模块没有自动链接机制。由于这个原因,我们需要手动进行链接操作。在MainApplication.java中,我们需要在ReactNativeHost实例中覆盖getJSIModulePackage方法。这个方法返回MmkvModulePackage(),实现了JSIModulePackage。
当我们看MmkvModulePackage.java文件时,我们看到这个类重写了getJSIModules函数。
public class MmkvModulePackage implements JSIModulePackage {
@Override
public List<JSIModuleSpec> getJSIModules(
ReactApplicationContext ctx,
JavaScriptContextHolder jsContext) {
MmkvModule.install(jsContext,ctx.getFilesDir().getAbsolutePath() + "/mmkv");
return Collections.emptyList();
}
}
如你所见,getJSIModules函数以列表形式返回JSIModuleSpec实例。而在返回行中,该函数只是返回一个空列表。这是因为这个函数只在JS线程中被调用。由于这样的调用是在创建bundle之前进行的,所以mmkv模块可以很容易地被加载到全局命名空间。如果我们在不同的线程,如Native Module线程中做这个,我们的应用程序就会因为在运行时出错而崩溃。
现在,让我们看看install方法是如何工作的。
public static void install(
JavaScriptContextHolder jsContext,
String storageDirectory) {
nativeInstall(jsContext.get(), storageDirectory);
}
private static native void nativeInstall(long jsiPtr, String path);
正如你在这里看到的,安装方法需要一个名为JavaScriptContextHolder的实例参数。这个类是一个混合的Java类,也包括作为C++实例的JavaScript运行时间。因此,将数值从Java转移到C++使其成为可能。这里的nativeInstall函数只是一个JNI函数,允许C++中的本地函数被Java调用。另外,语言之间的数据传输也在这里发生。
nativeInstall 函数在 C++ file 中定义如下。
extern "C"
JNIEXPORT void JNICALL
Java_com_reactnativemmkv_MmkvModule_nativeInstall(
JNIEnv *env,
jobject clazz,
jlong jsiPtr,
jstring path) {
MMKV::initializeMMKV(jstringToStdString(env, path));
auto runtime = reinterpret_cast<jsi::Runtime*>(jsiPtr);
if (runtime) {
install(*runtime);
}
// if runtime was nullptr, MMKV will not be installed. This should only happen while Remote Debugging (Chrome), but will be weird either way.
}
正如你在这里看到的,这个文件的Java_com_reactnativemmkv_MmkvModule部分与本地模块命名空间相匹配。env表达式是JNI环境,clazz参数是MMKV模块。参数jsiPtr持有JavaScript Runtime实例。参数path代表文件的路径,mmkv模块存储变量。注意,最后两个参数与Java中调用的参数相同。
nativeInstall(jsContext.get(), storageDirectory);
然后,由于的reinterpret_cast方法,jsiPtr变量被转换为jsi::Runtime。如果转换操作成功,安装方法被调用。如果它失败了,说明正在使用一个不支持JSI的环境。这个环境可以是我们提到的3个环境以外的引擎,也可以是Chrome远程调试器。
现在我们来看看install方法。
这个方法中的createFromHostFunction实例与我们前面提到的添加实例中的createFromHostFunction部分非常相似。键/值对从参数变量中获取,并与MMKV命名空间中的函数一起设置。
让我们看看另一个例子,视觉相机库。
在vision_camera库中使用JSI
在这个库中,我们可以直接访问帧的宽度和高度属性,并将它们直接传输到本地处理器插件中。
const frameProcessor = useFrameProcessor((frame) => {
'worklet';
console.log(`A new ${frame.width} x ${frame.height} frame arrived!`);
const values = examplePlugin(frame);
console.log(`Return values ${JSON.stringify(values)}`);
},[]);
这个worklet表达式意味着相关将在后台运行。因此,该操作将在不阻塞主线程的情况下进行。框架参数是一个JSI主机对象。换句话说,它是一个在C++中生成的对象,可以被JavaScript直接访问。frame.height这里的访问会触发getProperty方法,这实际上是C++代码。
现在我们来看看Frame object的属性。
export interface Frame {
isValid: boolean;
width: number;
height: number;
bytesPerRow: number;
planesCount: number;
toString(): string;
close(): void;
}
我们看到,这些属性只在TypeScript端引入,在JavaScript端没有任何代码。事实上,Frame对象的属性只存在于C++端。对于这一点,我们可以通过查看HostObject文件来获得更详细的信息。
#pragma once
#import <jsi/jsi.h>
#import <CoreMedia/CMSampleBuffer.h>
#import "Frame.h"
using namespace facebook;
class JSI_EXPORT FrameHostObject: public jsi::HostObject {
public:
explicit FrameHostObject(Frame* frame): frame(frame) {}
public:
// By overriding the get and getPropertyNames functions,
// properties can be used as JS object property.
// For example, when frame.height is requested,
// the get method here is called by setting the name property "height" below.
jsi::Value get(jsi::Runtime&, const jsi::PropNameID& name) override;
std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime& rt) override;
void close();
public:
// We cannot access directly to frame object
// Because this is C++ object and JS doesn't know how to interact with this object.
// Instead we use the get methods above.
Frame* frame;
private:
void assertIsFrameStrong(jsi::Runtime& runtime, const std::string& accessedPropName);
};
现在让我们看看属性是如何调用的。
#import "FrameHostObject.h"
#import <Foundation/Foundation.h>
#import <jsi/jsi.h>
// This method returns all of keys
std::vector<jsi::PropNameID> FrameHostObject::getPropertyNames(jsi::Runtime& rt) {
std::vector<jsi::PropNameID> result;
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("toString")));
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("isValid")));
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("width")));
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("height")));
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("bytesPerRow")));
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("planesCount")));
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("close")));
return result;
}
// Returns a value based on propName'e
jsi::Value FrameHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) {
auto name = propName.utf8(runtime);
// ...
// Returns frame height
if (name == "height") {
this->assertIsFrameStrong(runtime, name);
auto imageBuffer = CMSampleBufferGetImageBuffer(frame.buffer);
auto height = CVPixelBufferGetHeight(imageBuffer);
return jsi::Value((double) height);
}
// For returning a function, we need to create host function as follows:
if (name == "close") {
auto close = [this] (jsi::Runtime& runtime, const jsi::Value&, const jsi::Value*, size_t) -> jsi::Value {
if (this->frame == nil) {
throw jsi::JSError(runtime, "Trying to close an already closed frame! Did you call frame.close() twice?");
}
this->close();
return jsi::Value::undefined();
};
return jsi::Function::createFromHostFunction(runtime, jsi::PropNameID::forUtf8(runtime, "close"), 0, close);
}
//...
return jsi::Value::undefined();
}
现在我们可以进入到自定义主机对象的创建过程。
自定义主机对象的创建
通过创建一个自定义主机对象,可以返回不同类型的数据,如下所示。
#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();
}
};
如果该函数是一个全局函数,如Promise
,那么我们可以按如下方式调用。
auto promiseCtor = runtime.global().getPropertyAsFunction(runtime, "Promise");
auto promise = promiseCtor.callAsConstructor(runtime, resolve, reject);
如果是匿名函数,如(const x = () => ...)
,我们需要把它作为参数传递给C++函数。
auto nativeFunc = jsi::Function::createFromHostFunction(runtime,
jsi::PropNameID::forAsci(runtime, "someFunc"),
1, // a function
[](jsi::Runtime& runtime,
const jsi::Value& thisValue,
constjsi::Value* arguments,
size_t count) -> jsi::Value {
auto func = arguments[0].asObject().asFunction();
return func.call(runtime, jsi::Value(42));
});
总结
有了JSI,似乎很明显,模块和使用这些模块的应用程序的性能正在提高。由于JSI只是改变基础设施,我认为它不会影响日常的React Native应用开发。然而,如果你是一个库的维护者,我认为学习一些C++并将一个简单的库迁移到JSI是很有用的。
在我的下一篇文章中再见到你...