Skip to main content

跨平台原生模块 (C++)

¥Cross-Platform Native Modules (C++)

用 C++ 编写模块是在 Android 和 iOS 之间共享与平台无关的代码的最佳方式。使用纯 C++ 模块,你只需编写一次逻辑,即可立即在所有平台上重用它,而无需编写特定于平台的代码。

¥Writing a module in C++ is the best way to share platform-agnostic code between Android and iOS. With pure C++ modules, you can write your logic only once and reuse it right away from all the platform, without the need of writing platform specific code.

在本指南中,我们将介绍如何创建纯 C++ Turbo Native 模块:

¥In this guide, we will go through the creation of a pure C++ Turbo Native Module:

  1. 创建 JS 规范

    ¥Create the JS specs

  2. 配置 Codegen 以生成脚手架

    ¥Configure Codegen to generate the scaffolding

  3. 实现原生逻辑

    ¥Implement the Native logic

  4. 在 Android 和 iOS 应用中注册模块

    ¥Register the module in the Android and iOS application

  5. 在 JS 中测试你的更改

    ¥Test your changes in JS

本指南的其余部分假设你已创建运行以下命令的应用:

¥The rest of this guide assume that you have created your application running the command:

npx @react-native-community/cli@latest init SampleApp --version 0.76.0

1. 创建 JS 规范

¥ Create the JS specs

纯 C++ Turbo Native 模块是 Turbo Native 模块。他们需要一个规范文件(也称为 spec 文件),以便 Codegen 可以为我们创建脚手架代码。规范文件也是我们用来在 JS 中访问 Turbo Native 模块的文件。

¥Pure C++ Turbo Native Modules are Turbo Native Modules. They needs a specification file (also called spec file) so that Codegen can create the scaffolding code for us. The specification file is also what we use to access the Turbo Native Module in JS.

Specs 文件需要用类型化的 JS 方言编写。React Native 目前支持 Flow 或 TypeScript。

¥Specs files need to be written in a typed JS dialect. React Native currently supports Flow or TypeScript.

  1. 在应用的根文件夹中,创建一个名为 specs 的新文件夹。

    ¥Inside the root folder of your app, create a new folder called specs.

  2. 使用以下代码创建一个名为 NativeSampleModule.ts 的新文件:

    ¥Create a new file called NativeSampleModule.ts with the following code:

警告

所有 Native Turbo Module 规范文件都必须有前缀 Native,否则 Codegen 会忽略它们。

¥All Native Turbo Module spec files must have the prefix Native, otherwise Codegen will ignore them.

specs/NativeSampleModule.ts
import {TurboModule, TurboModuleRegistry} from 'react-native';

export interface Spec extends TurboModule {
readonly reverseString: (input: string) => string;
}

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

2. 配置 Codegen

¥ Configure Codegen

下一步是在你的 package.json 中配置 Codegen。更新文件以包含:

¥The next step is to configure Codegen in your package.json. Update the file to include:

package.json
     "start": "react-native start",
"test": "jest"
},
"codegenConfig": {
"name": "AppSpecs",
"type": "modules",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.sampleapp.specs"
}
},
"dependencies": {

此配置告诉 Codegen 在 specs 文件夹中查找 specs 文件。它还指示 Codegen 仅为 modules 生成代码并将生成的代码命名空间为 AppSpecs

¥This configuration tells Codegen to look for specs files in the specs folder. It also instruct Codegen to only generate code for modules and to namespace the generated code as AppSpecs.

3. 编写原生代码

¥ Write the Native Code

编写 C++ Turbo Native 模块允许你在 Android 和 iOS 之间共享代码。因此,我们将编写一次代码,然后研究需要对平台应用哪些更改,以便可以采用 C++ 代码。

¥Writing a C++ Turbo Native Module allow you to share the code between Android an iOS. Therefore we will be writing the code once, and we will look into what changes we need to apply to the platforms so that the C++ code can be picked up.

  1. androidios 文件夹的同一级别创建一个名为 shared 的文件夹。

    ¥Create a folder named shared at the same level of the android and ios folders.

  2. shared 文件夹中,创建一个名为 NativeSampleModule.h 的新文件。

    ¥Inside the shared folder, create a new file called NativeSampleModule.h.

    shared/NativeSampleModule.h
    #pragma once

    #include <AppSpecsJSI.h>

    #include <memory>
    #include <string>

    namespace facebook::react {

    class NativeSampleModule : public NativeSampleModuleCxxSpec<NativeSampleModule> {
    public:
    NativeSampleModule(std::shared_ptr<CallInvoker> jsInvoker);

    std::string reverseString(jsi::Runtime& rt, std::string input);
    };

    } // namespace facebook::react

  3. shared 文件夹中,创建一个名为 NativeSampleModule.cpp 的新文件。

    ¥Inside the shared folder, create a new file called NativeSampleModule.cpp.

    shared/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());
    }

    } // namespace facebook::react

让我们看看我们创建的两个文件:

¥Let's have a look at the two files we created:

  • NativeSampleModule.h 文件是纯 C++ TurboModule 的头文件。include 语句确保我们包含将由 Codegen 创建的规范,其中包含我们需要实现的接口和基类。

    ¥The NativeSampleModule.h file is the header file for a Pure C++ TurboModule. The include statements make sure that we include the specs that will be created by Codegen and that contains the interface and the base class we need to implement.

  • 模块位于 facebook::react 命名空间中,可以访问该命名空间中的所有类型。

    ¥The module lives in the facebook::react namespace to have access to all the types that live in that namespace.

  • NativeSampleModule 类是实际的 Turbo Native 模块类,它扩展了 NativeSampleModuleCxxSpec 类,后者包含一些粘合代码和样板代码,使该类可以充当 Turbo Native 模块。

    ¥The class NativeSampleModule is the actual Turbo Native Module class and it extends the NativeSampleModuleCxxSpec class which contains some glue code and boilerplate code to let this class behave as a Turbo Native Module.

  • 最后,我们有构造函数,它接受指向 CallInvoker 的指针,以便在需要时与 JS 进行通信,以及我们必须实现的函数原型。

    ¥Finally, we have the constructor, that accepts a pointer to the CallInvoker, to communicate with JS if needed and the function's prototype we have to implement.

NativeSampleModule.cpp 文件是我们 Turbo Native Module 的实际实现,并实现了我们在规范中声明的构造函数和方法。

¥The NativeSampleModule.cpp files is the actual implementation of our Turbo Native Module and implements the constructor and the method that we declared in the specs.

4. 在平台中注册模块

¥ Register the Module in the platform

接下来的步骤将让我们在平台中注册模块。这是将原生代码公开给 JS 的步骤,以便 React Native 应用最终可以从 JS 层调用原生方法。

¥The next steps will let us register the module in the platform. This is the step that exposes the native code to JS so that the React Native application can finally call the native methods from the JS layer.

这是我们唯一一次必须编写一些特定于平台的代码。

¥This is the only time when we will have to write some platform-specific code.

安卓

¥Android

为了确保 Android 应用可以有效地构建 C++ Turbo Native 模块,我们需要:

¥To make sure that the Android app can effectively build the C++ Turbo Native Module, we need to:

  1. 创建一个 CMakeLists.txt 来访问我们的 C++ 代码。

    ¥Create a CMakeLists.txt to access our C++ code.

  2. 修改 build.gradle 以指向新创建的 CMakeLists.txt 文件。

    ¥Modify build.gradle to point to the newly created CMakeLists.txt file.

  3. 在我们的 Android 应用中创建一个 OnLoad.cpp 文件来注册新的 Turbo Native 模块。

    ¥Create an OnLoad.cpp file in our Android app to register the new Turbo Native Module.

1. 创建 CMakeLists.txt 文件

¥ Create the CMakeLists.txt file

Android 使用 CMake 进行构建。CMake 需要访问我们在共享文件夹中定义的文件才能构建它们。

¥Android uses CMake to build. CMake needs to access the files we defined in our shared folder, to be able to build them.

  1. 创建一个新文件夹 SampleApp/android/app/src/main/jnijni 文件夹是 Android 的 C++ 端所在的位置。

    ¥Create a new folder SampleApp/android/app/src/main/jni. The jni folder is where the C++ side of Android lives.

  2. 创建一个 CMakeLists.txt 文件并添加此上下文:

    ¥Create a CMakeLists.txt file and add this context:

CMakeLists.txt
cmake_minimum_required(VERSION 3.13)

# Define the library name here.
project(appmodules)

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

# Define where the additional source code lives. We need to crawl back the jni, main, src, app, android folders
target_sources(${CMAKE_PROJECT_NAME} PRIVATE ../../../../../shared/NativeSampleModule.cpp)

# Define where CMake can find the additional header files. We need to crawl back the jni, main, src, app, android folders
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ../../../../../shared)

CMake 文件执行以下操作:

¥The CMake file does the following things:

  • 定义 appmodules 库,其中包含所有应用 C++ 代码。

    ¥Defines the appmodules library, where all the app C++ code will be included.

  • 加载基本 React Native 的 CMake 文件。

    ¥Loads the base React Native's CMake file.

  • 添加我们需要使用 target_sources 指令构建的模块 C++ 源代码。默认情况下,React Native 已经使用默认源填充 appmodules 库,这里我们包含了我们自定义的库。你可以看到我们需要从 jni 文件夹爬回到我们的 C++ Turbo 模块所在的 shared 文件夹。

    ¥Adds the Module C++ source code that we need to build with the target_sources directives. By default React Native will already populate the appmodules library with default sources, here we include our custom one. You can see that we need to crawl back from the jni folder to the shared folder where our C++ Turbo Module lives.

  • 指定 CMake 可以在哪里找到模块头文件。同样在这种情况下,我们需要从 jni 文件夹爬回。

    ¥Specifies where CMake can find the module header files. Also in this case we need to crawl back from the jni folder.

2. 修改 build.gradle 以包含自定义 C++ 代码

¥ Modify build.gradle to include the custom C++ code

Gradle 是协调 Android 构建的工具。我们需要告诉它可以在哪里找到 CMake 文件来构建 Turbo Native 模块。

¥Gradle is the tool that orchestrates the Android build. We need to tell it where it can find the CMake files to build the Turbo Native Module.

  1. 打开 SampleApp/android/app/build.gradle 文件。

    ¥Open the SampleApp/android/app/build.gradle file.

  2. 在 Gradle 文件中现有的 android 块内添加以下块:

    ¥Add the following block into the Gradle file, within the existent android block:

android/app/build.gradle
    buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://rn.nodejs.cn/docs/signed-apk-android.
signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}

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

此块告诉 Gradle 文件在哪里查找 CMake 文件。路径相对于 build.gradle 文件所在的文件夹,因此我们需要在 jni 文件夹中添加 CMakeLists.txt 文件的路径。

¥This block tells the Gradle file where to look for the CMake file. The path is relative to the folder where the build.gradle file lives, so we need to add the path to the CMakeLists.txt files in the jni folder.

3. 注册新的 Turbo Native 模块

¥ Register the new Turbo Native Module

最后一步是在运行时注册新的 C++ Turbo Native 模块,这样当 JS 需要 C++ Turbo Native 模块时,应用就知道在哪里找到它并可以返回它。

¥The final step is to register the new C++ Turbo Native Module in the runtime, so that when JS requires the C++ Turbo Native Module, the app knows where to find it and can return it.

  1. 从文件夹 SampleApp/android/app/src/main/jni,运行以下命令:

    ¥From the folder SampleApp/android/app/src/main/jni, run the following command:

curl -O https://raw.githubusercontent.com/facebook/react-native/v0.76.0/packages/react-native/ReactAndroid/cmake-utils/default-app-setup/OnLoad.cpp
  1. 然后,按如下方式修改此文件:

    ¥Then, modify this file as it follows:

android/app/src/main/jni/OnLoad.cpp
#include <DefaultComponentsRegistry.h>
#include <DefaultTurboModuleManagerDelegate.h>
#include <autolinking.h>
#include <fbjni/fbjni.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <rncore.h>

+ // Include the NativeSampleModule header
+ #include <NativeSampleModule.h>

//...

std::shared_ptr<TurboModule> cxxModuleProvider(
const std::string& name,
const std::shared_ptr<CallInvoker>& jsInvoker) {
// Here you can provide your CXX Turbo Modules coming from
// either your application or from external libraries. The approach to follow
// is similar to the following (for a module called `NativeCxxModuleExample`):
//
// if (name == NativeCxxModuleExample::kModuleName) {
// return std::make_shared<NativeCxxModuleExample>(jsInvoker);
// }

+ // This code register the module so that when the JS side asks for it, the app can return it
+ if (name == NativeSampleModule::kModuleName) {
+ return std::make_shared<NativeSampleModule>(jsInvoker);
+ }

// And we fallback to the CXX module providers autolinked
return autolinking_cxxModuleProvider(name, jsInvoker);
}

// leave the rest of the file

这些步骤从 React Native 下载原始 OnLoad.cpp 文件,以便我们可以安全地覆盖它以在应用中加载 C++ Turbo Native 模块。

¥These steps download the original OnLoad.cpp file from React Native, so that we can safely override it to load the C++ Turbo Native Module in the app.

下载文件后,我们可以通过以下方式修改它:

¥Once we downloaded the file, we can modify it by:

  • 包括指向我们模块的头文件

    ¥Including the header file that points to our module

  • 注册 Turbo Native 模块,以便当 JS 需要它时,应用可以返回它。

    ¥Registering the Turbo Native Module so that when JS requires it, the app can return it.

现在,你可以从项目根目录运行 yarn android 以查看你的应用是否成功构建。

¥Now, you can run yarn android from the project root to see your app building successfully.

iOS

为了确保 iOS 应用可以有效地构建 C++ Turbo Native 模块,我们需要:

¥To make sure that the iOS app can effectively build the C++ Turbo Native Module, we need to:

  1. 安装 pod 并运行 Codegen。

    ¥Install pods and run Codegen.

  2. shared 文件夹添加到我们的 iOS 项目中。

    ¥Add the shared folder to our iOS project.

  3. 在应用中注册 C++ Turbo Native 模块。

    ¥Register the C++ Turbo Native Module in the application.

1. 安装 Pod 并运行 Codegen。

¥ Install Pods and Run Codegen.

我们需要运行的第一步是我们每次准备 iOS 应用时运行的常规步骤。CocoaPods 是我们用来设置和安装 React Native 依赖的工具,作为该过程的一部分,它还将为我们运行 Codegen。

¥The first step we need to run is the usual steps we run every time we have to prepare our iOS application. CocoaPods is the tool we use to setup and install React Native dependencies and, as part of the process, it will also run Codegen for us.

cd ios
bundle install
bundle exec pod install

2. 将共享文件夹添加到 iOS 项目

¥ Add the shared folder to the iOS project

此步骤将 shared 文件夹添加到项目中,使其对 xcode 可见。

¥This steps adds the shared folder to the project to make it visible to xcode.

  1. 打开 CocoPods 生成的 Xcode 工作区。

    ¥Open the CocoPods generated Xcode Workspace.

cd ios
open SampleApp.xcworkspace
  1. 单击左侧的 SampleApp 项目并选择 Add files to "Sample App"...

    ¥Click on the SampleApp project on the left and select Add files to "Sample App"....

Add Files to Sample App...

  1. 选择 shared 文件夹并单击 Add

    ¥Select the shared folder and click on Add.

Add Files to Sample App...

如果你做对了所有事情,左边的项目看起来应该是这样的:

¥If you did everything right, your project on the left should look like this:

Xcode Project

3. 在你的应用中注册 Cxx Turbo Native 模块

¥ Registering the Cxx Turbo Native Module in your app

通过最后一步,我们将告诉 iOS 应用在哪里寻找纯 C++ Turbo Native 模块。

¥With this last step, we will tell the iOS app where to look for to find the pure C++ Turbo Native Module.

在 Xcode 中,打开 AppDelegate.mm 文件并按如下方式修改:

¥In Xcode, open the AppDelegate.mm file and modify it as follows:

SampleApp/AppDelegate.mm
#import <React/RCTBundleURLProvider.h>
+ #import <RCTAppDelegate+Protected.h>
+ #import "NativeSampleModule.h"

// ...
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}

+- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
+ jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
+{
+ if (name == "NativeSampleModule") {
+ return std::make_shared<facebook::react::NativeSampleModule>(jsInvoker);
+ }
+
+ return [super getTurboModule:name jsInvoker:jsInvoker];
+}

@end

这些更改正在做一些事情:

¥These changes are doing a few things:

  1. 导入 RCTAppDelegate+Protected 标头以使 AppDelegate 看到它符合 RCTTurboModuleManagerDelegate 协议。

    ¥Importing the RCTAppDelegate+Protected header to make visible to the AppDelegate that it is conforming to the RCTTurboModuleManagerDelegate protocol.

  2. 导入纯 C++ 原生 Turbo 模块接口 NativeSampleModule.h

    ¥Importing the Pure C++ Native Turbo Module interface NativeSampleModule.h

  3. 覆盖 getTurboModule 方法用于 C++ 模块,这样当 JS 端请求名为 NativeSampleModule 的模块时,应用就知道必须返回哪个模块。

    ¥Overriding the getTurboModule method for C++ modules so that when the JS side asks for a module called NativeSampleModule, the app knows which module has to be returned.

如果你现在从 Xcode 构建应用,则应该能够成功构建。

¥If you now build your application from Xcode, you should be able to build successfully.

5. 测试你的代码

¥ Testing your Code

现在是时候从 JS 访问我们的 C++ Turbo Native 模块了。为此,我们必须修改 App.tsx 文件以导入 Turbo Native Module 并在我们的代码中调用它。

¥It's now time to access our C++ Turbo Native Module from JS. To do so, we have to modify the App.tsx file to import the Turbo Native Module and to call it in our code.

  1. 打开 App.tsx 文件。

    ¥Open the App.tsx file.

  2. 用以下代码替换模板的内容:

    ¥Replace the content of the template with the following code:

App.tsx
import React from 'react';
import {
Button,
SafeAreaView,
StyleSheet,
Text,
TextInput,
View,
} from 'react-native';
import SampleTurboModule from './specs/NativeSampleModule';

function App(): React.JSX.Element {
const [value, setValue] = React.useState('');
const [reversedValue, setReversedValue] = React.useState('');

const onPress = () => {
const revString = SampleTurboModule.reverseString(value);
setReversedValue(revString);
};

return (
<SafeAreaView style={styles.container}>
<View>
<Text style={styles.title}>
Welcome to C++ Turbo Native Module Example
</Text>
<Text>Write down here he text you want to revert</Text>
<TextInput
style={styles.textInput}
placeholder="Write your text here"
onChangeText={setValue}
value={value}
/>
<Button title="Reverse" onPress={onPress} />
<Text>Reversed text: {reversedValue}</Text>
</View>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 18,
marginBottom: 20,
},
textInput: {
borderColor: 'black',
borderWidth: 1,
borderRadius: 5,
padding: 10,
marginTop: 10,
},
});

export default App;

此应用中有趣的几行是:

¥The interesting lines in this app are:

  • import SampleTurboModule from './specs/NativeSampleModule';:此行将 Turbo Native 模块导入到应用中,

    ¥import SampleTurboModule from './specs/NativeSampleModule';: this line imports the Turbo Native Module in the app,

  • onPress 回调中的 const revString = SampleTurboModule.reverseString(value);:这是你在应用中使用 Turbo Native 模块的方式。

    ¥const revString = SampleTurboModule.reverseString(value); in the onPress callback: this is how you can use the Turbo Native Module in your app.

警告

为了本示例的目的并使其尽可能简短,我们直接在应用中导入了 spec 文件。在这种情况下,最佳做法是创建一个单独的文件来封装规范,并将该文件用于你的应用。这允许你准备规范的输入,并让你在 JS 中更好地控制。

¥For the sake of this example and to keep it as short as possible, we directly imported the spec file in our app. The best practice in this case is to create a separate file to wrap the specs and use that file into your application. This allow you to prepare the input for the specs and gives you more control over then in JS.

恭喜,你编写了第一个 C++ Turbo Native 模块!

¥Congratulation, you wrote your first C++ Turbo Native Module!