Skip to content

浅析react-native-jsi

Published: at 02:00 PM

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 三个特点:

  1. 异步。这些消息队列是异步的,无法保证处理事件。
  2. 序列化。通过 JSON 格式来传递消息,每次都要经历序列化和反序列化,开销很大。
  3. 批处理。对 Native 调用进行排队,批量处理。

异步设计的好处是不阻塞,这种设计在大部分情况下性能满足需求,但是在某些情况下就会出问题,比如瀑布流滚动。

当瀑布流向下滑动的时候,需要发请求给服务端拿数据进行下一步渲染。

滚动事件发生在 UI thread,然后通过 Bridge 发给 JS thread。JS thread 监听到消息后发请求,服务端返回数据,再通过 Bridge 返回给 Native 进行渲染。由于都是异步,就会出现空白模块,导致性能问题。

从上面可以看出,性能瓶颈主要是存在 JS 线程和 Native 有交互的情况,如果不存在交互,RN 的性能良好。

与本机通信相比,此操作非常慢。出于这个原因,需要用新的架构替换 Bridge。

因此,对于 RN 的优化,主要集中在 Bridge 上,有下面 3 个原则:

  1. JS 和 Native 端不通信。最彻底的方式,消息不走 Bridge。
  2. JS 和 Native 减少通信。在两端无法避免的情况下,尽量通信减少次数。比如多个请求合并成一个。
  3. 较少 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 是新架构的核心,杰出的想法和框架。

参考链接


Previous Post
移动端 App 开发有感,写于开发跨端 RN App
Next Post
2023 年终总结