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 分支上有大量关于这个新概念的代码,他们已经在发布版本的代码当中了。
我决定研究怎么来使用它!
通过在贡献者频道(注:contributor channels,React Native 核心贡献者的内部交流频道)询问一些专家,以及通过推特交流,我联系上了 Eric Lewis 并向他寻求帮助,该如何不使用 bridge 将 Native 代码暴露给 JavaScript。
Eric 很兴奋,提供了很多帮助支持:
牛逼!我立刻打开 XCode 开始折腾,由于太久没用 C++,我遇到了一些问题,我与 Eric 来研究问题解决。
实际上,将 native module 直接导出到 JS 并没有那么难。
你只需要在项目中写几个 C++ 类就行了。
我们决定从最简单的开始——一个只包含一个方法的类,这个方法返回一个数字。
这个类很容易写,在你的 native 项目中编写这个类:
int Test::runTest() const { return 1337; }
下面我们需要编写胶水代码,供 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)); }
下一件要做的事情是在绑定创建一个方法,向 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(); }
其中:
我们的下一个任务是使用正确的方法"安装"方法,这样 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_); }
其中:
最终代码能够成功编译了。下面我们来看 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 传递数据。