React Native JSI 尝鲜

2019-04-19

前言

本文的原作者是 Christian Falch,文章源链接 React Native JSI Challenge,我将其翻译成中文。

回到 2018 年,Parashuram 在 React 阿姆斯特丹会议的演讲上展示了 React Native 的 Fabric 新架构,

他还写了一篇介绍文章,在今年的会议上他又带来了一个更详细的演讲

Lorenzo Sciandra 也围绕这个专题写了一系列共四篇很棒的文章,并且还发了很多推,并在 GitHub 上讨论 Fabric 是什么意思,以及所带来的性能提升。

在重构特性中,一项核心特性是避免在 JavaScript 和 Native 之间通过 bridge 传递序列化数据。(如果你不知道 React Native 的展示机制,请阅读上面的文章)。

当我刚听到新架构的时候,我立刻想到这回是一个革新,我开始查阅源代码来研究它是如何工作的。

当 React 0.59 发布的时候,我看到 master 分支上有大量关于这个新概念的代码,他们已经在发布版本的代码当中了。

我决定研究怎么来使用它!

注意:JSI 中真正有趣的是在 JS 与 Native 之间的胶水层。当你阅读关于新架构资料时,你会被 TurboModulesCodeGenFabric 这些新概念搞蒙。TurboModules 负责自动发现并暴露 module,Fabric 是新的同步视图展示引擎——他们两者都使用 JSI。CodeGen 是一个自动在 JS 与 Native 间生成胶水代码的代码生成器。

通过在贡献者频道(注:contributor channels,React Native 核心贡献者的内部交流频道)询问一些专家,以及通过推特交流,我联系上了 Eric Lewis 并向他寻求帮助,该如何不使用 bridge 将 Native 代码暴露给 JavaScript。

Eric 很兴奋,提供了很多帮助支持:

@ericlewis: 创建 c++ module 并插入 JSI 不需要额外折腾,他们很容易使用!

牛逼!我立刻打开 XCode 开始折腾,由于太久没用 C++,我遇到了一些问题,我与 Eric 来研究问题解决。

实际上,将 native module 直接导出到 JS 并没有那么难。

你只需要在项目中写几个 C++ 类就行了。

你需要导出的对象

我们决定从最简单的开始——一个只包含一个方法的类,这个方法返回一个数字。

这个类很容易写,在你的 native 项目中编写这个类:

int Test::runTest() const {
  return 1337;
}

为了让示例代码尽可能地少,所有的方法直接 inline 了。具体代码详见文章末尾。

下面我们需要编写胶水代码,供 JavaScript 和 native 之间执行方法查找和值装换——这就是用到 JSI 的地方。

绑定

绑定需要一个方法来配置,还需要一个方法来获取供 JavaScript 调用的方法。

void TestBinding::install(jsi::Runtime &runtime,
                          std::shared_ptr<TestBinding> testBinding) {
 auto testModuleName = “nativeTest”;
 auto object = jsi::Object::createFromHostObject(runtime, testBinding);
 runtime.global()
        .setProperty(runtime, testModuleName, std::move(object));
}

这就是我遇到问题的地方。为了使用 JSI 的声明,我们需要 include jsi.h。它在普通的 React Native 工程里是没有的。Eric 知道如何解决这个问题,通过编辑工程配置,添加几个额外的 header paths 和定义,我们的代码最终能够成功编译(具体详见代码仓库的提交记录)。

下一件要做的事情是在绑定创建一个方法,向 JavaScript 暴露 test 函数。样板代码写起来像这样:

jsi::Value TestBinding::get(jsi::Runtime &runtime, 
                            const jsi::PropNameID &name) {
 
 auto methodName = name.utf8(runtime);
 auto &test = *test_;
 
 if (methodName == “runTest”) {
   return jsi::Function::createFromHostFunction(runtime, name, 0,   
     [&test](jsi::Runtime &runtime, 
             const jsi::Value &thisValue, 
             const jsi::Value *arguments, size_t count) -> jsi::Value { 
       return test.runTest();
     });
 }
 
 return jsi::Value::undefined();
}

其中:

  • get 方法会在当 JavaScript 调用时被调用
  • 在这个代码中,我们封装每一个方法,并对返回值和参数进行转换

安装我们的绑定

我们的下一个任务是使用正确的方法"安装"方法,这样 React Native 知道它的存在。(这是 TurboModules 起作用的地方——我们采用一个 hacky 的解决方法)。

通过看"安装"方法的签名,我们能够看出,我们需要一个到 jsi::Runtime 对象的指针。

我们开始薅头发来想办法怎么能在 Objective-C 里传这个对象。

Eric 知道一些实现这个的窍门,我们在代码里安装了一个通知,因此我们能在 runtime 可用时得到一个回调:

[[NSNotificationCenter defaultCenter] addObserver:self 
 selector:@selector(handleJavaScriptDidLoadNotification:)
 name:RCTJavaScriptDidLoadNotification
 object:bridge];

在通知回调中,我们最终能够访问到 bridge 内部,通过导入 <React/RCTBridge+Private.h>,它暴露了一个 runtime 对象的 getter 方法。

我们最终能够把这些东西整合起来:

- (void)handleJavaScriptDidLoadNotification:(
 __unused NSNotification*)notification {
 // Get the RCTCxxBridge from bridge
 RCTCxxBridge* bridge = notification.userInfo[@”bridge”];
 
 // Get the runtime
 facebook::jsi::Runtime* runtime = 
   (facebook::jsi::Runtime*)bridge.runtime;
 
 // Create the Test object
 auto test = std::make_unique<facebook::react::Test>();
 // Create the Test binding
 std::shared_ptr<facebook::react::TestBinding> testBinding_ = 
  std::make_shared<facebook::react::TestBinding>(std::move(test));
 // Install it!!!
 facebook::react::TestBinding::install(
 (*runtime), testBinding_);
}

其中:

  • facebook::react::Test 就是我们前面编写的类
  • test 是这个类的实例
  • facebook::react::TestBinding 是我们创建的绑定类
  • testBinding_ 是这个类的实例
  • 最后一句完成了整合

JavaScript

最终代码能够成功编译了。下面我们来看 JavaScript 部分。

调用 native module 是这篇文章里最简单的部分了:

console.warn(global.nativeTest.runTest());

在模拟器里运行,我们很兴奋地看到返回数字在 React Native 提示框里展示出来了:

注意

有关我们的完整工作,请参见本文末尾的代码仓库。Commits 记录了我们的完整工作,可以很容易地重复。

重新加载和调试目前工作不起来。

重新加载不工作是因为我们每次收到 JavaScript 被加载的通知就安装我们的模块(这可以很容易通过一个标志修复)。

调试不工作的原因我们还在研究当中。欢迎提 PR。

总结

捣鼓 React Native 新架构中的东西很好玩。

尽管我们代码中包含一些 hack 来让我们的模块安装,但是我们证明了使用 JSI 从 JavaScript 调用 Native module 的能力在已经发布的 React Native 0.59 中已经提供了。

我很期待这会给扩展库开发者们带来怎样的可能性——我们应该开始准备编写高性能的同步代码,而不再通过 bridge 传递数据。