[ReactNative翻译]深入了解ReactNative原生模块

510 阅读9分钟

本文由 简悦 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,
.....
)

注册和初始化模块

  1. RCTModuleClasses 中的模块在 RCTCxxBridge.mm 中的 start 阶段注册和初始化。在initializeModules:withDispatchGroup:lazilyDiscovered中,react native 会首先注册所有这些 "自动导出 "的模块。代码在此;然后将它们存储到数组"_moduleClassesByID "中,该数组是指向 "类 "对象的指针,而"_moduleDataByID "则是指向一堆 "RCTModuleData "对象的指针。此处的代码。模块注册的反向跟踪如下:

  2. 如果模块需要在 main 线程中设置。它将保证在主线程中运行这里的代码这里React Native文档 建议我们在 requiresMainQueueSetup 方法中返回 NO 是有道理的。

如果您的模块不需要访问 UIKit,那么您应该用 NO 来回应 + requiresMainQueueSetup

  1. 当初始化 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 "方法

  1. 首先通过methodId获取methodName

  2. 获取当前模块的执行队列

  3. 构造一个块,调用 invokeInner. 在 RCTModuleMethod.mm 中,invokeInner调用 invokeWithBridge:module:arguments:

invokeInner(weakBridge, weakModuleData, methodId, std::move(params), callId, isSyncModule ? Sync : Async);

  1. 如果当前模块是在 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 提取导出方法并获取这些方法的 IMPIMP 实际上是一个 c 函数,它将 classselector 作为参数。然后,"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是一个协议。任何确认此协议的类都必须实现JSMethodNamefunctionTypeinvokeWithBridge: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:] 处设置了断点。

  1. 在初始化阶段,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);
    }
  1. 当 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));
      ...
    }
  1. 请看下面的 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

  1. JSToNativeBridge 中的 callNativeModules 首先会解析JSON数据,以获取moduleIdsmethodIdsparams等信息。此处为源代码。在构建了保存moduleIdmethodIdargumentscallIdMethodCall结构后,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 &&params,
    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 "及其实例。

  1. 它转到 RCTNativeModule.mm 中的 invoke 对象,该对象的私有变量 RCTModuleData 中包含模块信息。

  2. RCTNativeModule.mm 中的invoke 函数调用了 invokeInner。在 invokeInner 中,它会使用 methodIdRCTModuleData 对象获取 RCTBridgeMethod 对象。代码

id<RCTBridgeMethod> method = moduleData.methods[methodId];

然后,它会 调用 RCTModuleMethod.mm 中的 invokeWithBridge:module:arguments:

id result = [method invokeWithBridge:bridge module:moduleData.instance arguments:objcParams];

code ref

  1. 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 领域中特定模块中的特定方法。在此工作流中有一些 限制

  1. 原生模块在中指定,并急于初始化。React Native 的启动时间会随着原生模块数量的增加而增加,即使其中一些原生模块从未使用过。
  2. 没有简单的方法来检查 JavaScript 调用的原生模块是否真的包含在原生应用中。通过无线更新,没有简单的方法来检查新版本的 JavaScript 是否调用了原生模块中带有正确参数集的正确方法。
  3. 本地模块始终是单例的,其生命周期通常与桥接的生命周期相关联。在 React Native 桥接器可能会多次启动和关闭的棕地应用程序中,这个问题会变得更加复杂。
  4. 在启动过程中,原生模块通常是在多个包中指定的。然后,我们会多次迭代该列表,最后给桥接器一个本地模块列表。这并不需要在运行时进行。
  5. 本地模块的实际方法和常量是使用反射在运行时计算的。