[ReactNative翻译]React-native JSI 模块教程

392 阅读7分钟

本文由 简悦 SimpRead转码, 原文地址 ospfranco.com

JSI是JavaScript和C++之间的一个新的翻译层,它是在JS引擎上实现的......。

JSI 是 JavaScript 和 C++ 之间的一个新翻译层,它在 JS 引擎本身上实现,比 React-Native 桥接快得多。这是创建基本 JSI 模块的分步指南。

我创建了一个 SQLite react-native 库,它速度超快,这也是我能写这篇文章的原因,如果你能在上面留下一颗星,那就再好不过了!

如果你对其他 react-native 内容感兴趣,可以查看我的课程 react-native for macOS,如果你买了它,它真的能帮我写出很棒的内容!

创建基础模块

我们将使用 react-native-builder-bob 来搭建一个新的(独立的)模块,builder-bob 已经支持通过旧桥创建 cpp 模块。

从初始化开始:

npx react-native-builder-bob create react-native-sequel

在一堆问题之后,它还会问你想要哪种类型的项目,选择C++ 选项,这不会创建 JSI 模块,但会为 C++ 设置必要的编译。

进入 iOS 文件夹,修改已创建的头文件(.h)和obj-c 文件(.mm)。

注意:无论你在哪里看到 "react-native-sequel",只需替换你的软件包名称。

你应该有这样的文件

头文件,react-native-sequel.h

#import <React/RCTBridgeModule.h>
#import "react-native-sequel.h"

@interface Sequel : NSObject <RCTBridgeModule>

@property (nonatomic, assign) BOOL setBridgeOnMainQueue;

@end

实现文件,react-native-sequel.mm

#import "Sequel.h"
#import <React/RCTBridge+Private.h>
#import <React/RCTUtils.h>
#import "react-native-sequel.h"

@implementation Sequel

@synthesize bridge=_bridge;
@synthesize methodQueue = _methodQueue;

RCT_EXPORT_MODULE()

+ (BOOL)requiresMainQueueSetup {
  return YES;
}

- (void)setBridge:(RCTBridge *)bridge {
  _bridge = bridge;
  _setBridgeOnMainQueue = RCTIsMainQueue();

  RCTCxxBridge *cxxBridge = (RCTCxxBridge *)self.bridge;
  if (!cxxBridge.runtime) {
    return;
  }

  installSequel(*(facebook::jsi::Runtime *)cxxBridge.runtime);
}

- (void)invalidate {
  cleanUpSequel();
}

@end

无需赘述,只需注意以下几点:

  • 我们导入了React/RCTBridge+Private头文件,该文件公开了 jsi 绑定。
  • 重要的工作是在setBridge函数中完成的,在这里我们得到了一个对cxxBridge.runtime的引用,这是一个runtime对象,是在C++代码中进行所有操作以创建JavaScript值所必需的。我们将此运行时传递到一个 installSequel (稍后可以重命名)函数中,在此创建 JSI 绑定。

实际绑定

现在你可以进入项目根目录下的 cpp 文件夹,builder-bob 应该已经创建了一些基本的 c++ 供你使用,你可以删除它,然后创建一个头文件(在我的例子中是 react-native-sequel.h)和它的实现(react-native-sequel.cpp)。

我们的头文件

#include <jsi/jsilib.h>
#include <jsi/jsi.h>

void installSequel(facebook::jsi::Runtime& jsiRuntime);
void cleanUpSequel();

我们基本上是在 iOS 文件夹中公开桥接代码中使用的两个函数

对于我们的实现:

// Import our header file to implement the `installSequel` and `cleanUpSequel` functions
#include "react-native-sequel.h"
// sstream contains functions to manipulate strings in C++
#include <sstream>

// The namespace allows for syntactic sugar around the JSI objects. ex. call: jsi::Function instead of facebook::jsi::Function
using namespace facebook;

// We get the runtime from the obj-c code and we create our native functions here
void installSequel(jsi::Runtime& jsiRuntime) {
  // jsi::Function::createFromHostFunction will create a JavaScript function based on a "host" (read C++) function
  auto multiply = jsi::Function::createFromHostFunction(
    jsiRuntime, // JSI runtime instance
    jsi::PropNameID::forAscii(jsiRuntime, "multiply"), // Internal function name
    1, // Number of arguments in function
    // This is a C++ lambda function, the empty [] at the beginning is used to capture pointer/references so that they don't get de-allocated
    // Then you get another instance of the runtime to use inside the function, a "this" value from the javascript world, a pointer to the arguments (you can treat it as an array) and finally a count for the number of arguments
    // Finally the function needs to return a jsi::Value (read JavaScript value)
    [](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value {

      // the jsi::Value has a lot of helper methods for you to manipulate the data
      if(!arguments[0].isNumber() || !arguments[1].isNumber()) {
        jsi::detail::throwJSError(runtime, "Non number arguments passed to sequel");
      }

      double res = 42;
      return jsi::Value(res);
    }
  );

  // Registers the function on the global object
  jsiRuntime.global().setProperty(jsiRuntime, "multiply", std::move(multiply));
}

void cleanUpSequel() {
  // intentionally left blank
}

截至本文撰写时,JSI 桥接器及其绑定的文档很少,JSI 源代码 是最好的信息来源,但也有一些说明:

jsi::Value

是 javascript 值的封装器,有些值只需调用它就能直接创建,例如布尔值和数字,其他值(如字符串)则比较复杂,它们需要编码(如 UTF8)来解码/编码(这里有一个 示例)。

jsi::detail::throwJSError

向 javascript 代码抛出 JS 错误。请注意,我们创建的函数是同步函数,由于它可能会抛出错误,因此在 JavaScript 端调用该函数时,需要用 try/catch 将其封装起来。

注意 C++ 和内存管理

还有其他一些处理 JSIValues 的便捷方法,如 isNumberisString(请注意,javascript 中的数字总是双倍的)。一旦开始处理对象,事情就变得复杂了,你需要能够移动(std::move)值,这样一旦函数结束,它们就不会从内存中被清除。

Exposing a sensible API

最后,我们可以在 index.ts 文件中为该函数创建绑定(我不确定这些绑定是否也会暴露给封装应用程序,现在我创建了一个哑封装),有了 typescript 还可以在 JS 层面进行类型检查

// /src/index.tsx
declare function multiply(a: number, b: number): number;

export function multiplyA(): number {
  return multiply(2, 2);
}

最后,在使用此模块的 react-native 应用程序上:

import * as React from "react";

import { StyleSheet, View, Text } from "react-native";
import { multiplyA } from "react-native-sequel";

export default function App() {
  const [result, setResult] = React.useState<number | undefined>();

  React.useEffect(() => {
    setResult(multiplyA());
  }, []);

  return (
    <View style={styles.container}>
      <Text>Result: {result}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
  },
  box: {
    width: 60,
    height: 60,
    marginVertical: 20,
  },
});

现在我们有了一个可用的 iOS 实现,我们可以看看 android 的情况了。

我们首先要修改 android/CMakeLists.txt 中的文件,该文件告诉 android 编译过程要编译哪些 c++ 文件,你应该有这样的文件:

cmake_minimum_required(VERSION 3.4.1)

set (CMAKE_VERBOSE_MAKEFILE ON)
set (CMAKE_CXX_STANDARD 11)

include_directories(
            ../cpp
            ../../../node_modules/react-native/React
            ../../../node_modules/react-native/React/Base
            ../../../node_modules/react-native/ReactCommon/jsi
)

add_library(sequel
  SHARED
  ../../../node_modules/react-native/ReactCommon/jsi/jsi/jsi.cpp
  ../cpp/sequel.cpp
  ../cpp/sequel.h
  ../cpp/react-native-sequel.cpp
  ../cpp/react-native-sequel.h
  cpp-adapter.cpp
)

target_link_libraries(sequel)

基本上,我将 C++ 升级到了 14 版本,include 目录需要包含 .cpp 文件所在的文件夹,然后声明一个包含需要编译的确切文件的 "库",最后链接该库。

然后,我们就可以转到 android/cpp-adapter.cpp 文件,它与我们为 iOS 创建的 react-native-sequel.mm 文件类似,是注册绑定的入口点。修改该文件,使其包含 react-native-sequel.h 头文件(或任何你要调用的软件包),然后你就会得到类似这样的文件:

#include <jni.h>
#include "react-native-quick-sqlite.h"

extern "C" JNIEXPORT void JNICALL
Java_com_reactnativesequel_SequelModule_initialize(JNIEnv *env, jclass clazz, jlong jsiPtr, jstring docPath)
{
  jboolean isCopy;
  const char *docPathString = (env)->GetStringUTFChars(docPath, &isCopy); // This is might not be necessary, but my library moves files in the android file system, so this is just how to pass an android variable to the C++ size

  installSequel(*reinterpret_cast<facebook::jsi::Runtime *>(jsiPtr), docPathString);
}

extern "C" JNIEXPORT void JNICALL
Java_com_reactnativesequel_SequelModule_destruct(JNIEnv *env, jclass clazz)
{
  cleanUpSequel();
}

你可以看到我们得到了一个 JSI 桥接器实例,同样我们也得到了两个安装和清理绑定的函数,我不会用细节来烦你,唯一需要注意的细节是函数名将被转换成 Java 包名,以便稍后导入(Java_com_reactnativesequel_SequelModule_initialize -> com.reactnativesequel)

初始化 C++

前面的文件将 C++ 初始化为可调用的 Java 模块,但与 iOS 不同的是,它不会自动注册,因此需要创建一个新文件 android/src/main/java/com/reactnativesequel/SequelModule.java ,并在其中加入以下内容:

package com.reactnativesequel;

import androidx.annotation.NonNull;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;

class SequelModule extends ReactContextBaseJavaModule {
  static {
    System.loadLibrary("sequel");
  }

  private static native void initialize(long jsiPtr, String docDir);
  private static native void destruct();

  public SequelModule(ReactApplicationContext reactContext) {
    super(reactContext);
  }

  @NonNull
  @Override
  public String getName() {
    return "Sequel";
  }


  @NonNull
  @Override
  public void initialize() {
    super.initialize();

    SequelModule.initialize(
      this.getReactApplicationContext().getJavaScriptContextHolder().get(),
      this.getReactApplicationContext().getFilesDir().getAbsolutePath()
    );
  }

  @Override
  public void onCatalystInstanceDestroy() {
    SequelModule.destruct();
  }
}

builder-bob 可能创建了该文件的 kotlin 版本,你可以删除它(如果你喜欢 kotlin,也可以让它工作)。

Android 作为安卓系统,还需要一个额外的 Package 文件,即 android/src/main/java/com/reactnativesequel/SequelPackage.java

package com.reactnativesequel;

import androidx.annotation.NonNull;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;


public class SequelPackage implements ReactPackage {
  @NonNull
  @Override
  public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
    return Arrays.<NativeModule>asList(new SequelModule(reactContext));
  }

  @NonNull
  @Override
  public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
    return Collections.emptyList();
  }

就这样

完成后,iOS 和 Android 绑定现在都可以正常工作了!

部分代码(仅 iOS)github,如果你想自己探索的话。

JSI 小抄

我为 JSI/C++ 创建了一个新的小抄,如果你对开发自己的 JSI 代码感兴趣,可以在 twitter 上与我联系,我将与你分享该文档(以及我所有的 RN 笔记),只需支付少量费用。

它包含了能让你掌握 80% 问题的 20% C++,以及大量 JSI api 参考资料,因此你不必学习所有源代码就能创建自己的 JSI 模块。

面向 JS 开发人员的 JSI/C++ cheatsheet/guide 已经完成!

第一部分:面向 JS 开发人员的 C++
第二部分:工具(CMake、Android JNI、XCode 等)
第三部分:JSI 参考(如何使用 JSI)

给我发送 DM,付费后您就能获得访问权限和我的所有其他 RN 笔记 pic.twitter.com/NYvFllED09