ReactNative 新架构中 iOS 通过 JSI 调用 RN 函数

React Native 新架构简介

React Native 的新架构是为了解决旧架构中存在的性能瓶颈和开发体验问题而推出的重大升级。新架构主要包含以下几个核心组件:

  1. JSI (JavaScript Interface): 取代了旧架构中的 JSC (JavaScriptCore),提供了 JS 和原生代码之间的直接通信能力。
  2. Fabric: 新的渲染系统,通过 C++ 桥接层实现了更高效的 UI 渲染。
  3. TurboModules: 新的原生模块系统,允许更高效地调用原生 API。
  4. CodeGen: 自动代码生成工具,减少了手动编写样板代码的工作量。
  5. Hermes: Facebook 开发的专为移动应用优化的 JavaScript 引擎。

相比于旧架构,新架构最大的优势在于消除了异步桥接带来的性能损耗,允许 JavaScript 和原生代码直接通信,大幅提升了应用性能和开发体验。

JSI 是什么?

JSI (JavaScript Interface) 是 React Native 新架构中的核心组件之一,它是一个轻量级的 C++ API 层,提供了 JavaScript 和原生代码之间的直接通信能力。

与旧架构中的 Bridge 不同,JSI 允许:

  1. 同步调用: JavaScript 可以同步调用原生方法,无需通过异步消息队列。
  2. 引用共享: JavaScript 可以持有原生对象的引用,反之亦然。
  3. 引擎无关: JSI 设计为 JS 引擎无关的接口,可以兼容不同的 JS 引擎(如 JSC、Hermes 等)。
  4. 双向通信: 不仅 JS 可以调用原生代码,原生代码也可以直接调用 JS 函数。

这种设计解决了旧架构中的大量性能问题,特别是由于异步通信导致的延迟和序列化/反序列化开销。

RCTBridge 在新架构中的角色

在 React Native 的新架构中,RCTBridge 仍然存在,但其角色发生了变化。它主要承担以下职责:

  1. 兼容性层: 为了保证旧代码能在新架构中正常运行,RCTBridge 提供了向后兼容的接口。
  2. 生命周期管理: 管理 JS 运行时的生命周期,包括初始化、加载 JS bundle 和销毁。
  3. 提供 JSI 访问: 通过 RCTCxxBridge 暴露 JSI 运行时,允许原生代码访问 JS 运行时。
  4. 线程管理: 维护专用的 JavaScript 线程,确保 JSI 操作的线程安全性。React Native 的架构中有明确的线程划分,包括 JavaScript 线程、主线程(UI 线程)和 Shadow 线程,RCTBridge 通过 dispatchBlock:queue: 等方法在适当的线程上执行代码。

在新架构中,直接通信大多通过 JSI 实现,而 RCTBridge 则更多地充当一个容器和生命周期管理者的角色。特别是它的 RCTCxxBridge 子类,提供了从原生代码到 JSI 运行时的重要桥梁。

iOS 通过 JSI 调用 RN 函数的实现

接下来,我们将详细介绍如何在 iOS 端通过 JSI 调用 React Native 中定义的 JavaScript 函数。

1. 配置 AppDelegate

首先,我们需要在 AppDelegate 中进行必要的配置,包括创建 RCTBridge 实例和注册通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// AppDelegate.swift
import UIKit
import React
import CocoaLumberjack

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?
var bridge: RCTBridge?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 创建 RCTBridge 实例
bridge = RCTBridge(delegate: self, launchOptions: launchOptions)

// 注册 JavaScript 加载完成的通知
NotificationCenter.default.addObserver(self,
selector: #selector(onJSDidLoad),
name: NSNotification.Name("RCTJavaScriptDidLoadNotification"),
object: nil)

// 其他配置...
return true
}

// 其他 AppDelegate 方法...
}

// 实现 RCTBridgeDelegate 协议
extension AppDelegate: RCTBridgeDelegate {
func sourceURL(for bridge: RCTBridge) -> URL? {
#if DEBUG
// 开发环境使用本地开发服务器
let jsCodeLocation = URL(string: "http://localhost:8081/index.bundle?platform=ios")
print("React Native 开发服务器 URL: \(jsCodeLocation?.absoluteString ?? "无效URL")")
return jsCodeLocation
#else
// 生产环境使用打包后的 JS bundle
guard let bundleURL = Bundle.main.url(forResource: "main", withExtension: "jsbundle") else {
print("错误: 无法找到 main.jsbundle 文件")
return nil
}
return bundleURL
#endif
}
}

// JavaScript 加载完成的通知处理
@objc extension AppDelegate {
func onJSDidLoad() {
print("JS 加载完成")
// 此处可以添加 JS 环境准备就绪后的逻辑
// 例如:检查全局函数是否可用
let hasSayHello = JSIBridge.hasJavaScriptFunction("sayHello")
print("sayHello 函数是否存在: \(hasSayHello)")
}
}

这段配置代码包含以下关键部分:

  1. 创建 RCTBridge 实例:在应用启动时创建 RCTBridge,这是连接原生代码和 JavaScript 的桥梁。
  2. 注册通知:注册 RCTJavaScriptDidLoadNotification 通知,以便在 JavaScript 加载完成后执行相应的操作。
  3. 实现 RCTBridgeDelegate:提供 JavaScript bundle 的加载路径,区分开发环境和生产环境。
  4. JavaScript 加载完成的处理:在 onJSDidLoad 方法中处理 JavaScript 环境准备就绪后的逻辑。

这些配置是使用 JSI 调用 JavaScript 函数的前提,确保了 JavaScript 环境的正确初始化和加载。

2. 创建 JSIBridge 类

首先,我们需要创建一个桥接类,用于封装 JSI 调用的具体实现。这里我们创建了 JSIBridge 类:

1
2
3
4
5
6
7
8
9
10
11
// JSIBridge.h
@interface JSIBridge : NSObject

// 检查 JavaScript 全局函数是否存在
+ (BOOL)hasJavaScriptFunction:(NSString *)functionName;

// 调用 JavaScript 全局函数
+ (NSString *)callJavaScriptFunction:(NSString *)functionName
withArgument:(NSString *)argument;

@end

这个类提供了两个主要方法:

  • hasJavaScriptFunction: 检查指定名称的 JavaScript 全局函数是否存在
  • callJavaScriptFunction:withArgument: 调用指定名称的 JavaScript 全局函数,并传递一个字符串参数

3. 实现 JSIBridge 类

接下来,我们在 JSIBridge.mm 文件中实现这些方法。由于需要直接访问 JSI 接口,我们使用 Objective-C++ 编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// JSIBridge.mm
#import "JSIBridge.h"
#include <jsi/jsi.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTBridge.h>

using namespace facebook::jsi;
using namespace std;

@implementation JSIBridge

+ (RCTBridge *)getBridge {
return [RCTBridge currentBridge];
}

+ (BOOL)hasJavaScriptFunction:(NSString *)functionName {
RCTBridge *bridge = [self getBridge];
__block BOOL hasFunction = NO;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

[bridge dispatchBlock:^{
RCTCxxBridge* cxxbridge = (RCTCxxBridge*)bridge;

if (cxxbridge.runtime) {
Runtime& runtime = *(Runtime*)cxxbridge.runtime;
const char *name = functionName.UTF8String;

try {
Function function = runtime.global().getPropertyAsFunction(runtime, name);
hasFunction = function.isFunction(runtime);
} catch (JSError& error) {
hasFunction = NO;
}
}

dispatch_semaphore_signal(semaphore);
} queue:RCTJSThread];

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

return hasFunction;
}

+ (NSString *)callJavaScriptFunction:(NSString *)functionName
withArgument:(NSString *)argument {
RCTBridge *bridge = [self getBridge];
__block NSString *result = @"";
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

[bridge dispatchBlock:^{
RCTCxxBridge* cxxbridge = (RCTCxxBridge*)bridge;

if (cxxbridge.runtime) {
// 所有 JSI 代码都在 JS 线程上执行,避免线程重入问题
Runtime& runtime = *(Runtime*)cxxbridge.runtime;

// 获取全局函数
Function function = runtime.global().getPropertyAsFunction(runtime, functionName.UTF8String);

// 调用函数并传递参数
Value param = Value(runtime, String::createFromUtf8(runtime, argument.UTF8String));
Value jsResult = function.call(runtime, std::move(param), 1);

string str = jsResult.asString(runtime).utf8(runtime);
result = [NSString stringWithUTF8String:str.c_str()];
}

dispatch_semaphore_signal(semaphore);
} queue:RCTJSThread];

// 等待JavaScript线程执行完毕
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

return result;
}

@end

这段代码的核心逻辑包括:

  1. 获取当前 RCTBridge 实例
  2. 从 RCTBridge 中获取 RCTCxxBridge,它提供了对 JSI 运行时的访问
  3. 通过 JSI API 获取全局函数对象并调用它
  4. 使用信号量确保 JS 线程执行完毕后再返回结果(同步调用)

4. 在 JavaScript 中定义函数

在 React Native 的 JavaScript 代码中,我们需要定义全局函数,使其可以被原生代码调用。以下是一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在 index.js 或其他入口文件中

// 将函数挂载到全局对象上
global.sayHello = (name) => {
return `Hello, ${name}! From React Native.`;
};

global.getAppVersion = () => {
return "1.0.0";
};

// 更复杂的例子:调用 Redux 状态获取数据
global.getUserInfo = () => {
const store = require('./store').default;
const state = store.getState();
return JSON.stringify(state.user);
};

5. 在原生代码中调用 JavaScript 函数

现在,我们可以在任何原生代码中使用 JSIBridge 调用 JavaScript 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 在 ViewController 或其他 Swift 文件中
import Foundation

class MyViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

// 检查函数是否存在
let hasSayHello = JSIBridge.hasJavaScriptFunction("sayHello")
print("sayHello 函数存在: \(hasSayHello)")

if hasSayHello {
// 调用函数并获取结果
let result = JSIBridge.callJavaScriptFunction("sayHello", withArgument: "iOS")
print("调用结果: \(result)")
}

// 调用获取版本的函数
if JSIBridge.hasJavaScriptFunction("getAppVersion") {
let version = JSIBridge.callJavaScriptFunction("getAppVersion", withArgument: "")
print("应用版本: \(version)")
}
}
}

RCTBridge 在新架构中的实际应用

在上面的代码中,我们可以看到 RCTBridge 在新架构中的几个关键应用:

  1. 提供 JavaScript 运行时访问:通过 RCTCxxBridgeruntime 属性,我们可以获取 JSI 运行时,进而调用 JavaScript 函数。
  2. 管理 JavaScript 线程:使用 dispatchBlock:queue: 方法在 JavaScript 线程上执行代码,确保线程安全。
  3. 生命周期通知:通过 RCTJavaScriptDidLoadNotification 通知,我们可以知道 JavaScript 环境何时准备就绪。
  4. 提供 bundle 加载机制:通过 RCTBridgeDelegate 协议,我们可以控制 JavaScript bundle 的加载方式。

虽然在新架构中,直接通信主要通过 JSI 实现,但 RCTBridge 仍然在初始化阶段和生命周期管理中发挥着重要作用,是连接新旧架构的重要桥梁。

注意事项和最佳实践

在使用 JSI 进行原生与 JavaScript 通信时,需要注意以下几点:

  1. 线程安全: JSI 调用必须在 JavaScript 线程上执行,否则会导致崩溃。上面的代码通过 dispatchBlock 和信号量确保了线程安全。
  2. 错误处理: 调用 JavaScript 函数时可能会抛出异常,需要适当处理这些异常。
  3. 内存管理: JSI 允许原生代码和 JavaScript 互相持有对方的引用,需要注意避免循环引用导致的内存泄漏。
  4. 性能考虑: 虽然 JSI 提供了同步调用能力,但对于耗时操作,仍然应该考虑异步处理,避免阻塞主线程。
  5. 兼容性: 在混合使用新旧架构的过渡期,需要考虑兼容性问题。

扩展:支持更复杂的参数和返回值

上面的示例只演示了如何传递和返回简单的字符串参数,在实际应用中,我们可能需要处理更复杂的数据类型。以下是一个扩展版本,支持 JSON 对象的传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 扩展 JSIBridge,添加支持 JSON 对象的方法
+ (id)callJavaScriptFunctionWithJSON:(NSString *)functionName
jsonArgument:(NSDictionary *)argument {
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:argument
options:0
error:&error];
if (error) {
NSLog(@"JSON序列化错误: %@", error);
return nil;
}

NSString *jsonString = [[NSString alloc] initWithData:jsonData
encoding:NSUTF8StringEncoding];
NSString *resultString = [self callJavaScriptFunction:functionName
withArgument:jsonString];

// 尝试将结果解析为 JSON
NSData *resultData = [resultString dataUsingEncoding:NSUTF8StringEncoding];
id resultObject = [NSJSONSerialization JSONObjectWithData:resultData
options:0
error:&error];
if (error) {
// 如果不是有效的 JSON,则返回原始字符串
return resultString;
}

return resultObject;
}

总结

通过 JSI,React Native 的新架构为原生代码和 JavaScript 之间的通信提供了更直接、更高效的方式。本文介绍了如何在 iOS 端通过 JSI 调用 React Native 中定义的 JavaScript 函数,包括:

  1. React Native 新架构的核心组件和优势
  2. JSI 的基本概念和特性
  3. RCTBridge 在新架构中的角色变化
  4. 实现 JSI 调用的具体步骤和示例代码
  5. 使用过程中的注意事项和最佳实践

这种方法不仅提高了应用性能,还简化了原生与 JavaScript 之间的交互流程,为开发更复杂的混合应用提供了强大支持。

参考资料