本文由 简悦 SimpRead转码, 原文地址 suelan.github.io
RY 的博客
原生模块 是 React Native 的关键部分之一,它使 JavasScript 能够调用 iOS 或 Android 原生实现中的方法。
在 React Native 仓库中探索有关原生模块的源代码是非常有趣的。在本文中,我想谈谈 React Native 如何注册、初始化原生模块,以及从 JavaScript 端到 iOS Objective-C 模块的方法调用背后的原因。
首先,让我们看看 RCTBridgeModule,它是一个协议,提供了注册桥接模块所需的接口。
#define RCT_EXPORT_MODULE(js_name)
#define RCT_EXPORT_METHOD(method)
+ (NSString *)moduleName;
@optional
+ (BOOL)requiresMainQueueSetup;
导出和注册模块
"RCT_EXPORT_MODULE "属于 "RCTBridgeModule "协议,你可以使用这个宏来导出你的 iOS 模块。
将此宏放入你的类实现中,以便在加载模块时自动向网桥注册。可选的 js_name 参数。它将用作 JS 模块名称。如果省略,JS 模块名将与 Objective-C 类名一致。
例如,RNCAsyncStorage.m#L404, react-native-async-storage 中使用了 RCT_EXPORT_MODULE。
在编译源文件时,该宏将在预处理器阶段被以下代码替换。
RCT_EXTERN void RCTRegisterModule(Class);
+ (NSString *)moduleName { return @#js_name; }
+ (void)load { RCTRegisterModule(self); }
在 Objc 世界中,每当一个类添加到 Objective-C 运行时,都会在应用程序启动之初调用 load 方法。此时,它会调用 RCTRegisterModule 函数将类对象添加到 RCTModuleClasses 数组中。RCTModuleClasses 是一个包含已注册类列表的数组。
void RCTRegisterModule(Class moduleClass)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTModuleClasses = [NSMutableArray new];
RCTModuleClassesSyncQueue =
dispatch_queue_create("com.facebook.react.ModuleClassesSyncQueue", DISPATCH_QUEUE_CONCURRENT);
});
RCTAssert(
[moduleClass conformsToProtocol:@protocol(RCTBridgeModule)],
@"%@ does not conform to the RCTBridgeModule protocol",
moduleClass);
// Register module
dispatch_barrier_async(RCTModuleClassesSyncQueue, ^{
[RCTModuleClasses addObject:moduleClass];
});
}
将当前模块类添加到 RCTModuleClasses 数组时,会使用 dispatch_barrier_async 将此任务分派到并发队列 RCTModuleClassesSyncQueue 中。使用 dispatch_barrier_async 可以确保
RCTModuleClassesSyncQueue` 中一次执行一个异步块。
正如我们刚才提到的,RCTModuleClasses 是一个全局数组,包含已注册类的列表。
Printing description of RCTModuleClasses:
<__NSArrayM 0x7fe4dc5d6bb0>(
....
RNSVGTSpanManager,
RNSVGUseManager,
RCTBaseTextViewManager,
RCTTextViewManager,
RNLinearTextGradientViewManager,
RCTVirtualTextViewManager,
RNVirtualLinearTextGradientViewManager,
RCTAccessibilityManager,
RCTActionSheetManager,
RCTActivityIndicatorViewManager,
RCTAlertManager,
.....
)
注册和初始化模块
-
RCTModuleClasses中的模块在RCTCxxBridge.mm中的 start 阶段注册和初始化。在initializeModules:withDispatchGroup:lazilyDiscovered中,react native 会首先注册所有这些 "自动导出 "的模块。代码在此;然后将它们存储到数组"_moduleClassesByID "中,该数组是指向 "类 "对象的指针,而"_moduleDataByID "则是指向一堆 "RCTModuleData "对象的指针。此处的代码。模块注册的反向跟踪如下: -
如果模块需要在
main线程中设置。它将保证在主线程中运行。这里的代码 和 这里。React Native文档 建议我们在requiresMainQueueSetup方法中返回NO是有道理的。
如果您的模块不需要访问 UIKit,那么您应该用
NO来回应+ requiresMainQueueSetup。
- 当初始化
RNBridge时。Objc 领域中的RNBridge基本上是RCTCxxBridge.mm的包装器。此时,RN 注册额外模块。
正如Lorenzo S.在此talk中分享的那样。在 RCTCxxBridge 启动阶段初始化所有模块会减慢启动时间。本地模块越多,初始化这些模块所需的时间就越长,即使你在第一页不会使用它们。
存储模块
在注册阶段,在 [RCTCXXBridge_registerModulesForClasses:lazilyDiscovered:],react native 会将已注册类的列表存储到一个字典_moduleDataByName中。键是 moduleName,值是 RCTModuleData。
NSMutableDictionary<NSString *, RCTModuleData *> *_moduleDataByName;
请记住,在注册阶段,模块的类存储在_moduleClassesByID中。而_moduleDataByID存储的是RCTModuleData。
// Native modules
NSMutableArray<RCTModuleData *> *_moduleDataByID;
NSMutableArray<Class> *_moduleClassesByID;
RCTModuleData
现在,你可能想知道什么是 RCTModuleData?它只是一种数据结构,用于保存反应原生模块的数据。
"RCTModuleData "中的 "RCTBridgeModuleProvider "用于初始化 "RCTBridgeModule "的 "实例"。然后,RCTModuleData会保留这个实例。在初始化阶段,"RCTModuleData "会通过 "RCTBridgeModuleProvider "为 "模块类 "创建一个实例。RCTBridgeModuleProvider 所做的就是使用[moduleClass new]提供一个实例。Code ref.
- (instancetype)initWithModuleClass:(Class)moduleClass
bridge:(RCTBridge *)bridge
{
return [self initWithModuleClass:moduleClass
moduleProvider:^id<RCTBridgeModule>{ return [moduleClass new]; }
bridge:bridge];
}
按名称或类获取模块
RCTBridge bridge 是 RCTCxxBridge 的一个简单封装,使 RCTCxxBridge.mm 中的方法可用于其他 Objective-C 对象。您可以访问这两个方法来从 Objective-C 对象获取模块。
// RCTBridge
- (id)moduleForName:(NSString *)moduleName
- (id)moduleForClass:(Class)moduleClass
它们将查找 _moduleDataByName 字典,找出目标 RCTModuleData 并获取其 instance 。此处为源代码
您可能会对这些类之间的关系感兴趣。
RCTNativeModule and ModuleRegistry
在RCTNativeModule.mm中,RCTNativeModule这个c++类继承自基类NativeModule,后者保存了模块的信息。
RCTNativeModule的构造方法需要两个参数,即RCTBridge对象和RCTModuleData`对象。
在 RCTNativeModule 中调用方法
RCTNativeModule中的 "invoke "方法
-
首先通过
methodId获取methodName。 -
获取当前模块的执行队列
-
构造一个块,调用
invokeInner. 在RCTModuleMethod.mm中,invokeInner调用invokeWithBridge:module:arguments:。
invokeInner(weakBridge, weakModuleData, methodId, std::move(params), callId, isSyncModule ? Sync : Async);
- 如果当前模块是在
JSThread中执行的,那么调用方法的代码块将同步执行。但如果当前模块中的方法要在其他线程中执行,react native 会将前一个代码块分派到相关队列中。
dispatch_queue_t queue = m_moduleData.methodQueue;
const bool isSyncModule = queue == RCTJSThread;
if (isSyncModule) {
block();
BridgeNativeModulePerfLogger::syncMethodCallReturnConversionEnd(moduleName, methodName);
} else if (queue) {
BridgeNativeModulePerfLogger::asyncMethodCallDispatch(moduleName, methodName);
dispatch_async(queue, block);
}
NativeModuleProxy
-
在运行时初始化阶段,
RCTxxBridge.start->Instance.initializeBridge->NativeToJSBridge.initializeRuntime->JSIExecutor.initializeRuntime、创建一个
NativeModuleProxy对象,并将其设置为 JavaScript 运行时上下文中global对象的nativeModuleProxy属性。
void JSIExecutor::initializeRuntime() {
...
runtime_->global().setProperty(
*runtime_,
"nativeModuleProxy",
Object::createFromHostObject(
*runtime_, std::make_shared<NativeModuleProxy>(nativeModules_)));
...
}
- 在
NativeModule.js中,JavaScript 端可以从global对象中的nativeModuleProxy属性获取大量 `native 模块。
let NativeModules: {[moduleName: string]: $FlowFixMe, ...} = {};
if (global.nativeModuleProxy) {
NativeModules = global.nativeModuleProxy;
- 在 iOS 端,"NativeModuleProxy "类为 "JSINativeModules "提供了一个弱指针,而 "JSINativeModules "实际上保存了一个已注册的本地模块列表。
std::weak_ptr<JSINativeModules> weakNativeModules_;
导出方法
"RCT_EXPORT_METHOD "宏用于在 Objective realm 中导出方法,将被以下代码替换
#define RCT_REMAP_METHOD(js_name, method) \
_RCT_EXTERN_REMAP_METHOD(js_name, method, NO) \
-(void)method RCT_DYNAMIC;
...
#define _RCT_EXTERN_REMAP_METHOD(js_name, method, is_blocking_synchronous_method) \
+(const RCTMethodInfo *)RCT_CONCAT(__rct_export__, RCT_CONCAT(js_name, RCT_CONCAT(__LINE__, __COUNTER__))) \
{ \
static RCTMethodInfo config = {#js_name, #method, is_blocking_synchronous_method}; \
return &config; \
}
基本上,它的作用是构建一个 RCTMethodInfo 结构。
typedef struct RCTMethodInfo {
const char *const jsName;
const char *const objcName;
const BOOL isSync;
} RCTMethodInfo;
calculateMethods 提取导出方法并获取这些方法的 IMP。IMP 实际上是一个 c 函数,它将 class 和 selector 作为参数。然后,"RCTMethodInfo "和 "moduleClass "被用来构造 "RCTModuleMethod",并确认为 "RCTBridgeMethod"。
RCTBridgeMethod
RCTModuleData 还包含导出方法的信息。
/**
* Returns the module methods. Note that this will gather the methods the first
* time it is called and then memoize the results.
*/
@property (nonatomic, copy, readonly) NSArray<id<RCTBridgeMethod>> *methods;
/**
* Returns a map of the module methods. Note that this will gather the methods the first
* time it is called and then memoize the results.
*/
@property (nonatomic, copy, readonly) NSDictionary<NSString *, id<RCTBridgeMethod>> *methodsByName;
这两种方法的 getter methods 将生成一个 RCTBridgeMethod 对象列表。RCTBridgeMethod是一个协议。任何确认此协议的类都必须实现JSMethodName、functionType和invokeWithBridge:module:arguments: 函数。
@protocol RCTBridgeMethod <NSObject>
@property (nonatomic, readonly) const char *JSMethodName;
@property (nonatomic, readonly) RCTFunctionType functionType;
- (id)invokeWithBridge:(RCTBridge *)bridge module:(id)module arguments:(NSArray *)arguments;
@end
那么,react native 会在什么时候触发这两个获取方法呢?在 RCTUIManager 中
RCT_EXPORT_METHOD(dispatchViewManagerCommand
: (nonnull NSNumber *)reactTag commandID
: (id /*(NSString or NSNumber) */)commandID commandArgs
: (NSArray<id> *)commandArgs)
此外,RCTModuleData 还为本地模块设置并保存线程methodQueue。这个 dispatch_queue_t 对象也保留在 RCTBridgeModule.h 中,用于调用所有导出的方法。
- (void)setUpMethodQueue
{
if (_instance && !_methodQueue && _bridge.valid) {
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"[RCTModuleData setUpMethodQueue]", nil);
BOOL implementsMethodQueue = [_instance respondsToSelector:@selector(methodQueue)];
if (implementsMethodQueue && _bridge.valid) {
_methodQueue = _instance.methodQueue;
}
if (!_methodQueue && _bridge.valid) {
// Create new queue (store queueName, as it isn't retained by dispatch_queue)
_queueName = [NSString stringWithFormat:@"com.facebook.react.%@Queue", self.name];
_methodQueue = dispatch_queue_create(_queueName.UTF8String, DISPATCH_QUEUE_SERIAL);
// assign it to the module
if (implementsMethodQueue) {
@try {
[(id)_instance setValue:_methodQueue forKey:@"methodQueue"];
} @catch (NSException *exception) {
RCTLogError(
@"%@ is returning nil for its methodQueue, which is not "
"permitted. You must either return a pre-initialized "
"queue, or @synthesize the methodQueue to let the bridge "
"create a queue for you.",
self.name);
}
}
}
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
}
JS 如何在本地调用方法?
我在 [RCTModuleMethod invokeWithBridge:module:arguments:] 处设置了断点。
- 在初始化阶段,JavaScript 执行上下文的全局对象中设置了
nativeFlushQueueImmediate属性。此处引用代码。global.nativeFlushQueueImmediate(queue) "会在 JavaScript 的 "enqueueNativeCall "中被调用。代码参考 在调用之前,它会确保上次刷新是在 5 毫秒之前;然后触发nativeFlushQueueImmediate。
if (
global.nativeFlushQueueImmediate &&
now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS
) {
const queue = this._queue;
this._queue = [[], [], [], this._callID];
this._lastFlush = now;
// 🌟
global.nativeFlushQueueImmediate(queue);
}
- 当 JavaScrip 端调用方法时,会调用 JSCRuntime 中的
call函数,然后触发host 函数。在本例中,宿主函数是 C++ 领域的nativeFlushQueueImmediate。
static JSValueRef call(
JSContextRef ctx,
JSObjectRef function,
JSObjectRef thisObject,
size_t argumentCount,
const JSValueRef arguments[],
JSValueRef *exception) {
HostFunctionMetadata *metadata =
static_cast<HostFunctionMetadata *>(JSObjectGetPrivate(function));
JSCRuntime &rt = *(metadata->runtime);
...
// 🌟🌟
metadata->hostFunction_(rt, thisVal, args, argumentCount));
...
}
- 请看下面的 C++ lambda 用于创建宿主函数
nativeFlushQueueImmediate。在这个 C++ lambda 的侧面,我们可以看到它调用了 callNativeModules。
runtime_->global().setProperty(
*runtime_,
"nativeFlushQueueImmediate",
Function::createFromHostFunction(
*runtime_,
PropNameID::forAscii(*runtime_, "nativeFlushQueueImmediate"),
1,
[this](
jsi::Runtime &,
const jsi::Value &,
const jsi::Value *args,
size_t count) {
if (count != 1) {
throw std::invalid_argument(
"nativeFlushQueueImmediate arg count must be 1");
}
callNativeModules(args[0], false);
return Value::undefined();
}));
然后调用 JSToNativeBridge 中的 callNativeModules。
JSToNativeBridge中的 callNativeModules 首先会解析JSON数据,以获取moduleIds、methodIds和params等信息。此处为源代码。在构建了保存moduleId、methodId、arguments和callId的MethodCall结构后,ModuleRegistry.cpp中的callNativeModules将被调用。
private:
// This is always populated
std::vector<std::unique_ptr<NativeModule>> modules_;
...
void ModuleRegistry::callNativeMethod(
unsigned int moduleId,
unsigned int methodId,
folly::dynamic &¶ms,
int callId) {
if (moduleId >= modules_.size()) {
throw std::runtime_error(folly::to<std::string>(
"moduleId ", moduleId, " out of range [0..", modules_.size(), ")"));
}
modules_[moduleId]->invoke(methodId, std::move(params), callId);
}
"moduleId "用于在模块列表中查找 "NativeModule "对象。NativeModule 列表是在 CxxBridge 初始化阶段设置的,基本上是一个保存 C++ 领域模块信息的向量,由 Objc 领域的 _moduleDataByID数组转换而来。我们知道,"_moduleDataByID "是一个 "RCTModuleData "列表,其中包含已注册的 "RCTBridgeModule "及其实例。
-
它转到
RCTNativeModule.mm中的invoke对象,该对象的私有变量RCTModuleData中包含模块信息。 -
RCTNativeModule.mm中的invoke函数调用了invokeInner。在invokeInner中,它会使用methodId从RCTModuleData对象获取RCTBridgeMethod对象。代码
id<RCTBridgeMethod> method = moduleData.methods[methodId];
然后,它会 调用 RCTModuleMethod.mm 中的 invokeWithBridge:module:arguments:。
id result = [method invokeWithBridge:bridge module:moduleData.instance arguments:objcParams];
- invokeWithBridge:module:arguments: 使用 NSInvocation 向相关的
module对象发送信息。
一个
NSInvocation对象包含 Objective-C 消息的所有元素:目标、选择器、参数和返回值。
7.1. 解析 MethodSignature以启动 NSInvocation。
_selector = NSSelectorFromString(RCTParseMethodSignature(_methodInfo->objcName, &arguments));
....
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
...
// set selector
invocation.selector = _selector;
7.2. 在模块中触发方法
[_invocation invokeWithTarget:module];
简而言之,当 JavaScript 端调用特定本地模块中的方法时,模块和方法的索引会通过 JSCRuntime 传递给 Objc。这些信息被序列化为 JSON 对象。React Native 可以通过查找保存了注册模块和方法信息的表格,调用 Objective-C 领域中特定模块中的特定方法。在此工作流中有一些 限制。
- 原生模块在包中指定,并急于初始化。React Native 的启动时间会随着原生模块数量的增加而增加,即使其中一些原生模块从未使用过。
- 没有简单的方法来检查 JavaScript 调用的原生模块是否真的包含在原生应用中。通过无线更新,没有简单的方法来检查新版本的 JavaScript 是否调用了原生模块中带有正确参数集的正确方法。
- 本地模块始终是单例的,其生命周期通常与桥接的生命周期相关联。在 React Native 桥接器可能会多次启动和关闭的棕地应用程序中,这个问题会变得更加复杂。
- 在启动过程中,原生模块通常是在多个包中指定的。然后,我们会多次地迭代该列表,最后给桥接器一个本地模块列表。这并不需要在运行时进行。
- 本地模块的实际方法和常量是使用反射在运行时计算的。