最近在用RN进行开发,根据官方提供的文档,浅尝了一把传说中的Turbo Native Modules。

C++ Turbo Native Modules

在RN中,JS并没有直接调用原生的能力,当应用需要访问平台API时,需要让JS与Native Module通讯,让Native Module去调用系统接口并返回信息给JS。这一过程在安卓上需要编写Java/Kt代码;在苹果上,需要编写OC/Swift代码。旧版架构中,RN通过Bridge实现JS与Native Modules通讯,通讯过程中需要通过JSON来回传递数据。不过我们在编写Native模块的时候并不需要手动去解析JSON,这是RN内部就已经处理好的。另外还有一个特点,就是这个通讯过程是异步的,这使得我们编写的模块需要采用callback函数、监听事件或者Promise的方式来获取Native端返回的信息。

Turbo Native Modules是下一代Native Modules,具有懒加载、高效率(对比旧版bridge)、确保接口一致性等优点。Turbo Modules采用了JSI。JSI的出现,直接将bridge取而代之并打入冷宫,大大提高JS与Native的通讯效率,为JS和Native Modules之间打通了一条更加便捷的通道。这种体验有点类似于在浏览器中调用document对象的方法直接抚摸DOM。当新的架构(新架构采用 Turbo Module和Fabric Components)稳定下来后,旧的Native Module就会被抛弃。

实际上一开始我的目的是在RN中使用C++编写的代码,我发现有很多种方法来实现这个目的,因此我并不是很想直接在我的项目中enable Turbo Modules(我一启用就有某个模块出bug,暂时不想去修复),而是先探究了一下怎么利用JSI。现在搜索JSI,能找到不少指导如何在RN中使用JSI的文章,时效性可能稍差,直接按照上面的步骤操作可能已经不行了。在IOS上用JSI比较简单,相关的文档会更多,但是安卓上面还是要通过JNI去调用,更加麻烦一些。于是我还是转头参考了官方的Turbo modules文档。由于Turbo modules还处于实验性阶段,所以官方的文档写的还比较简略,需要自己探索native深处,但是一步步来操作还是没有太多问题的。

关于Turbo Modules,RN官网上存在两篇文档,一篇是Turbo Native Modules,另一篇是CXX Turbo Modules,直接参考后者即可。下面以安卓为例正式开始(IOS略过...)。

开始

首先准备一个比较纯净的RN项目,在android/gradle.properties中修改newArchEnabled=true,然后在项目根目录中创建一个tm文件夹,这个文件夹中就是用来存放Turbo moduleTs或flow+Js声明的。

TM - 声明与配置

tm文件夹中,需要创建以下几个文件:

NativeSampleModule.cppNativeSampleModule.h (这两个文件暂时略过,下面会给出)

NativeSampleModule.ts:必须在这里用flow+js或者TS给出传递的数据的类型,此处以TS为例,我们将测试计算斐波那契数列、传递JS数组、callback、promise。当写好这个文件后,Codegen实际上会根据你写的类型在AppSpecsJSI.h中生成一层代码,把TS中的声明转移到CPP中,保持JS和CPP两边的数据类型相通。所以,下面缩写的TS类型ObjectStruct、ConstantsStruct类型将会被导出到AppSpecsJSI.h中,你可以在该头文件中搜索到这两个类型,但是这两个类型的名称会被加上前缀(见NativeSampleModule.h)。

import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport';
// import type {TurboModule} from 'react-native'; in future versions
import { TurboModuleRegistry } from 'react-native';

export type ObjectStruct = {
  a: number,
  b: string,
  c?: string,
};

export type ConstantsStruct = {
  c1: boolean,
  c2: number,
  c3: string,
};

export interface Spec extends TurboModule {
  readonly reverseString: (input: string) => string;
  /** 计算斐波那契数列第n项 */
  readonly fibonacci: (n: number) => number;
  /** 计算斐波那契数列第n项 重复times遍 */
  readonly fibonacciWithRepeat: (n: number, times: number) => number;
  getArray: (arg: Array<ObjectStruct | null>) => Array<ObjectStruct | null>;
  getConstants: () => ConstantsStruct;
  getValueWithCallback: (callback: (value: string) => void) => void;
  promiseAssert: () => Promise<void>;
}

export default TurboModuleRegistry.getEnforcing<Spec>(
  'NativeSampleModule',
);

注:根据官方的文档,Codegen 不算是新架构的主要组成部分,它是一个帮助我们避免编写重复代码的工具。Codegen 并非必选项,您仍然可以手写它所生成的代码,但是使用它来生成脚手架代码可以帮您节省不少时间。
RN会在每次构建 App 时调用 Codegen,因此我没有完全查看Codegen的文档,而是直接通过编译App来触发Codegen生成代码。

CMakeLists.txt: 这一步定义了tm文件夹为native代码的源,并且配置了必要的依赖。

cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)

add_compile_options(
        -fexceptions
        -frtti
        -std=c++17)

file(GLOB tm_SRC CONFIGURE_DEPENDS *.cpp)
add_library(tm STATIC ${tm_SRC})

target_include_directories(tm PUBLIC .)
target_include_directories(react_codegen_AppSpecs PUBLIC .)

target_link_libraries(tm
        jsi
        react_nativemodule_core
        react_codegen_AppSpecs)

返回项目根目录,插入以下内容到项目的package.json中,这一步主要是配置 Codegen 在tm文件夹内部搜索 specs。

{
  // ...
  "codegenConfig": {
    "name": "AppSpecs",
    "type": "all",
    "jsSrcsDir": "tm",
    "android": {
      "javaPackageName": "com.facebook.fbreact.specs"
    }
  }
}

接下来创建文件夹:android/app/src/main/jni,复制node_modules/react-native/ReactAndroid/cmake-utils/default-app-setup中的CMakeLists.txtOnload.cpp到这里,并修改内容。Onload.cpp修改如下:

// ...

#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <rncli.h>
+ #include <NativeSampleModule.h>

// ...

std::shared_ptr<TurboModule> cxxModuleProvider(
    const std::string &name,
    const std::shared_ptr<CallInvoker> &jsInvoker) {
+ if (name == "NativeSampleModule") {
+   return std::make_shared<facebook::react::NativeSampleModule>(jsInvoker);
+ }
  return nullptr;
}

// ...

CMakeLists.txt修改如下:

// ...

# This file includes all the necessary to let you build your application with the New Architecture.
include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)

+ # App needs to add and link against tm (TurboModules) folder
+ add_subdirectory(${REACT_ANDROID_DIR}/../../../tm/ tm_build)
+ target_link_libraries(${CMAKE_PROJECT_NAME} tm)

android/app/build.gradle中,还需要指定一下CMakeLists.txt

android {
   externalNativeBuild {
       cmake {
           path "src/main/jni/CMakeLists.txt"
       }
   }
}

这时候,可以执行npm start然后按下a键,编译一下安卓版本(当然最后编译是会失败的),在编译前Codegen会在android/app/build/generated/source/codegen/jni下偷偷塞点东西,打开可以看到如下目录结构,是Codegen已经为我们生成好的胶水代码。

.:
AppSpecs-generated.cpp  AppSpecs.h  CMakeLists.txt  react/

./react/renderer/components/AppSpecs:
AppSpecsJSI-generated.cpp  EventEmitters.cpp  Props.h          States.cpp
AppSpecsJSI.h              EventEmitters.h    ShadowNodes.cpp  States.h
ComponentDescriptors.h     Props.cpp          ShadowNodes.h

TM - Implementation

配置部分基本完成,是时候实现方法了。在项目根目录tm目录下:

添加NativeSampleModule.h。注意Structs部分的NativeSampleModuleBaseConstantsStruct / NativeSampleModuleBaseConstantsStructBridging / NativeSampleModuleBaseObjectStruct / NativeSampleModuleBaseObjectStructBridging均来自Codegen生成的AppSpecsJSI.h

#pragma once

#if __has_include(<React-Codegen/AppSpecsJSI.h>) // CocoaPod headers on Apple
#include <React-Codegen/AppSpecsJSI.h>
#elif __has_include("AppSpecsJSI.h") // CMake headers on Android
#include "AppSpecsJSI.h"
#endif
#include <memory>
#include <string>

namespace facebook::react {

#pragma mark - Structs
using ConstantsStruct =
    NativeSampleModuleBaseConstantsStruct<bool, int32_t, std::string>;

template <>
struct Bridging<ConstantsStruct>
    : NativeSampleModuleBaseConstantsStructBridging<
          bool,
          int32_t,
          std::string> {};

using ObjectStruct = NativeSampleModuleBaseObjectStruct<
    int32_t,
    std::string,
    std::optional<std::string>>;

template <>
struct Bridging<ObjectStruct>
    : NativeSampleModuleBaseObjectStructBridging<
          int32_t,
          std::string,
          std::optional<std::string>> {};

#pragma mark - implementation
class NativeSampleModule : public NativeSampleModuleCxxSpec<NativeSampleModule> {
 public:
  NativeSampleModule(std::shared_ptr<CallInvoker> jsInvoker);

  std::string reverseString(jsi::Runtime& rt, std::string input);
  double fibonacci(jsi::Runtime&rt, double n);
  double fibonacciWithRepeat(jsi::Runtime &rt, double n, double times);
  std::vector<std::optional<ObjectStruct>> getArray(
      jsi::Runtime &rt,
      std::vector<std::optional<ObjectStruct>> arg);
  ConstantsStruct getConstants(jsi::Runtime &rt);
  void getValueWithCallback(
      jsi::Runtime &rt,
      AsyncCallback<std::string> callback);
  AsyncPromise<jsi::Value> promiseAssert(
      jsi::Runtime &rt);
};

} // namespace facebook::react

添加NativeSampleModule.cpp:

#include "NativeSampleModule.h"

namespace facebook::react
{
  NativeSampleModule::NativeSampleModule(std::shared_ptr<CallInvoker> jsInvoker)
      : NativeSampleModuleCxxSpec(std::move(jsInvoker)) {}

  std::string NativeSampleModule::reverseString(jsi::Runtime &rt, std::string input) {
    return std::string(input.rbegin(), input.rend());
  }

  double NativeSampleModule::fibonacci(jsi::Runtime &rt, double n) {
    long pre = 0, next = 1, result, N = n; // 这里用long 待会儿返回时还需要转换回Double
    if (N == 0) result = pre;
    if (N == 1) result = next;
    while (N >= 2) {
      N--;
      result = pre + next;
      pre = next;
      next = result;
    }
    return (double) result;
  }

  double NativeSampleModule::fibonacciWithRepeat(jsi::Runtime &rt, double n, double times) {
    double result = 0;
    while (times-- > 0) {
      result = NativeSampleModule::fibonacci(rt, n);
    }
    return result;
  }

  std::vector<std::optional<ObjectStruct>> NativeSampleModule::getArray(
      jsi::Runtime &rt,
      std::vector<std::optional<ObjectStruct>> arg) {
    return arg;
  }

  ConstantsStruct NativeSampleModule::getConstants(jsi::Runtime &rt) {
    return ConstantsStruct{true, 69, "react-native"};
  }

  void NativeSampleModule::getValueWithCallback(
      jsi::Runtime &rt,
      AsyncCallback<std::string> callback) {
    callback({"value from callback!"});
  }

  AsyncPromise<jsi::Value> NativeSampleModule::promiseAssert(
      jsi::Runtime &rt) {
    // react_native_assert(false && "Intentional assert from Cxx promiseAssert");

    // Asserts disabled
    auto promise = AsyncPromise<jsi::Value>(rt, jsInvoker_);
    promise.reject("Asserts disabled");
    return promise;
  };

} // namespace facebook::react

这里有2个点注意一下:

  1. android\app\build\generated\source\codegen\jni\react\renderer\components\AppSpecs\AppSpecsJSI.h中可以看到Codegen根据TS文件生成的接口,我们只需要去实现接口即可,如果TS写错了/删除了某个方法/CPP遗漏的方法未实现,就会报错。
  2. TS声明的特殊的结构类型才需要在CPP头文件中再次声明。见头文件中的#pragma mark - Structs下的内容,以及TS中的导出类型(export type)。另外,返回或者读取的数字似乎都必须是double类型的(猜测jsi::Value也可),因为JS中普通的数字都是double。关于详细的类型转换,官网上好像没有详细的介绍,我是直接从官方给的文件里面copy来的。

JS侧调用(测试)

最后,在JS端调用Turbo module试试看。App.tsx如下。

import React, { useEffect, useState } from 'react';
import {
  Text,
  View,
  Button,
} from 'react-native';
import NativeSampleModule from './tm/NativeSampleModule';

function timeIt(label: string, func: () => any, repeatCount: number = 1) {
  console.log("[timeIt] " + label);
  const startTime = new Date().getTime();
  let ret: any;
  for (let i = 0; i < repeatCount; i++)
    ret = func();
  const passTime = new Date().getTime() - startTime;
  console.log("return " + ret + " in " + passTime + "ms");
}

function JSFibonacci(n: number): number {
  let pre = 0, next = 1, result = 0;
  if (n == 0) result = pre;
  if (n == 1) result = next;
  while (n >= 2) {
    n--;
    result = pre + next;
    pre = next;
    next = result;
  }
  return result;
}

function App() {
  console.log("Note that the max safe int number is " + Number.MAX_SAFE_INTEGER.toExponential());
  const fibCountTo = 50, repeatCount = 1e6;
  const testInJs = () => {
    timeIt("Js", () => {
      return JSFibonacci(fibCountTo);
    }, repeatCount);
  }
  const testInCpp = () => {
    timeIt("Cpp", () => {
      return NativeSampleModule.fibonacci(fibCountTo);
    }, repeatCount);
  }
  const testInCppRepeat = () => {
    timeIt("CppRepeat", () => {
      return NativeSampleModule.fibonacciWithRepeat(fibCountTo, repeatCount);
    }, 1);
  }
  const getArray = () => {
    const got = NativeSampleModule.getArray([
      { a: 1, b: "from js", c: "haha" },
      { a: 2, b: "from js too", c: "good" },
      { a: 3, b: "noop", c: "heyo" }
    ]);
    console.log(got);
  }
  const getValueWithCallback = () => {
    NativeSampleModule.getValueWithCallback((value) => {
      console.log(value);
    });
  }
  const getConstants = () => {
    const got = NativeSampleModule.getConstants();
    console.log(got);
  }
  const promiseTest = () => {
    NativeSampleModule.promiseAssert().then(
      () => console.log("suc"),
      (reason) => console.log(reason)
    );
  }
  return (
    <View style={{
      flex: 1, flexDirection: "column", justifyContent: "space-evenly", alignItems: "stretch"
    }}>
      <Button
        title="testInJs"
        onPress={testInJs} />
      <Button
        title="testInCpp"
        onPress={testInCpp} />
      <Button
        title="testInCppRepeat"
        onPress={testInCppRepeat} />
      <Button
        title='getArray'
        onPress={getArray} />
      <Button
        title='getConstant'
        onPress={getConstants} />
      <Button
        title='getValueWithCallback'
        onPress={getValueWithCallback} />
      <Button
        title='promiseTest'
        onPress={promiseTest} />
    </View>
  );
}

export default App;

先测试一下计算能力,在JS和CPP中分别实现斐波那契数列计算函数,求取第50项,重复1e6次统计耗时,控制台输出结果如下。其中,前面两次测试是用纯JS实现,消耗时间不到2S;接下来两次测试在JS中循环调用CPP方法,重复计算1e6次,耗时3S+;最后两次测试,在JS侧调用一次CPP方法,但在CPP中循环计算1e6次,耗时不到200ms。由此可见,一来一回之间,通过JSI的交互还是会有一定的性能损失。

 LOG  Running "RNTMtest" with {"fabric":true,"initialProps":{"concurrentRoot":true},"rootTag":1}
 LOG  Note that the max safe int number is 9.007199254740991e+15
 LOG  [timeIt] Js
 LOG  return 12586269025 in 1907ms
 LOG  [timeIt] Js
 LOG  return 12586269025 in 1924ms
 LOG  [timeIt] Cpp
 LOG  return 12586269025 in 3083ms
 LOG  [timeIt] Cpp
 LOG  return 12586269025 in 3082ms
 LOG  [timeIt] CppRepeat
 LOG  return 12586269025 in 189ms
 LOG  [timeIt] CppRepeat
 LOG  return 12586269025 in 171ms

再试试传递对象数组、普通对象给JSI,通过JSI执行回调函数,以及返回Promise。

 LOG  [{"a": 1, "b": "from js", "c": "haha"}, {"a": 2, "b": "from js too", "c": "good"}, {"a": 3, "b": "noop", "c": "heyo"}]
 LOG  {"c1": true, "c2": 69, "c3": "react-native"}
 LOG  value from callback!
 LOG  [Error: Asserts disabled]

到这里,Turbo modules算是已经初探完毕了。实际上还有更多有趣的东西值得继续探索,我还并不完全清楚官方仓库中提供的模板代码中的每一行到底是有什么作用,或者是有什么潜在的坑,也暂时没有比较好的想法去利用这个东西做点什么,在我的项目中,使用JS进行大量的运算只会在老旧的移动端设备上(Debug模式)造成比较严重的掉帧,通过bridge的部分大部分是USB通讯部分的代码,但是对于实时性要求也不高。

如果希望继续深入研究,可以看看官方给出的这几个文件:RNNewArchitectureApp/tm at run/pure-cxx-module · react-native-community/RNNewArchitectureApp (github.com),还有Dive into RN JSI,其他:Supporting Custom C++ Types · React Native