SoFunction
Updated on 2025-04-12

Detailed explanation of management of Native Module in iOS RN startup

1. Global native module registry

RCTModuleClasses array

First of all, RN has a global static array RCTModuleClasses, which uses it to record all Native Module Classes. You can think that this is a Native Module configuration table to be started!!!

And a global method void RCTRegisterModule(Class); is provided, injecting a new module class into this RCTModuleClasses:

// 1. Global Registration Center, RN uses RCTModuleClasses to hold the global registered native modulestatic NSMutableArray<Class> *RCTModuleClasses;
static dispatch_queue_t RCTModuleClassesSyncQueue;
// 2. sync read - Global RCTModuleClassesNSArray<Class> *RCTGetModuleClasses(void) {
    __block NSArray<Class> *result;
    dispatch_sync(RCTModuleClassesSyncQueue, ^{
        result = [RCTModuleClasses copy];
    });
    return result;
}
// 3. barrier async writevoid RCTRegisterModule(Class moduleClass) {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        RCTModuleClasses = [NSMutableArray new];
        RCTModuleClassesSyncQueue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT); // sync read, barrier_async write    });
    dispatch_barrier_async(RCTModuleClassesSyncQueue, ^{
        [RCTModuleClasses addObject:moduleClass];
    });
}

2. RCTBridgeModule protocol

APP injects module into global table in pre main

If you want to implement a native module in RN, you need to implement the RCTBridgeModule protocol, and the RN official requires us to add the RCT_EXPORT_MODULE macro when implementing the protocol.

@protocol RCTBridgeModule <NSObject>
/**
 * Place this macro in your class implementation to automatically register
 * your module with the bridge when it loads. The optional js_name argument
 * will be used as the JS module name. If omitted, the JS module name will
 * match the Objective-C class name.
 */
#define RCT_EXPORT_MODULE(js_name)          \
  RCT_EXTERN void RCTRegisterModule(Class); \
  +(NSString *)moduleName                   \
  {                                         \
    return @ #js_name;                      \
  }                                         \
  +(void)load                               \
  {                                         \
    RCTRegisterModule(self);                \
  }
...
@end

In the implementation of the RCT_EXPORT_MODULE(js_name) macro, the RCTRegisterModule(self); method will be called in +load to inject the Native Module Class into the global static array RCTModuleClasses!!!

In other words, before the iOS APP starts to execute the main function, all types Class that implements the RCTBridgeModule protocol will be injected into the RCTModuleClasses array.

3. Processing of Class data in RCTModuleClasses

When the start method of RCTCxxbridge is executed, the RCTGetModuleClasses() method already has all the native module configuration tables that need to be registered with the bridge, so you can directly initialize all classes in this module configuration table!!!

//(void)[self _initializeModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO];
- (NSArray&lt;RCTModuleData *&gt; *)_initializeModules:(NSArray&lt;Class&gt; *)modules
                               withDispatchGroup:(dispatch_group_t)dispatchGroup
                                lazilyDiscovered:(BOOL)lazilyDiscovered {
    /// 1. modules Class => Packaged into intermediate management class RCTModuleData    /// 2. Then put the RCTModuleData object into several specific containers in bridge to manage    NSArray&lt;RCTModuleData *&gt; *moduleDataById = [self _registerModulesForClasses:modules lazilyDiscovered:lazilyDiscovered];
    if (lazilyDiscovered) {
        ...
    } else { 
        // When initialization, I will go here        /// 3. To handle the scene, before bridge._moduleSetupComplete, the module forces instance instantiation and manages it in moduleData        for (RCTModuleData *moduleData in _moduleDataByID) {
            if ( &amp;&amp; (! || RCTIsMainQueue())) {
                (void)[moduleData instance];
            }
        }
        /// 4. Mark the module in the bridge to complete the setup work        _moduleSetupComplete = YES;
        /// 5. Asynchronous preapre for module in bridge        [self _prepareModulesWithDispatchGroup:dispatchGroup];
    }
    /// 6. Return all moduleData arrays    return moduleDataById;
}

Mainly speaking, it is to do the following:

  • _registerModulesForClasses, main job: Module Class is packaged into RCTModuleData:

    Encapsulate each moduleClass into a RCTModuleData object moduleData

    Then setUp() on moduleData

  • Register RCTModuleData into the properties of the bridge module managed:

    NSMutableDictionary<NSString *, RCTModuleData *> *_moduleDataByName

    NSMutableArray<RCTModuleData *> *_moduleDataByID;

    NSMutableArray<Class> *_moduleClassesByID;

  • Some RCTModuleData modules need to be performed before bridge's _moduleSetupComplete = true [moduleData instance] -- (void)[moduleData instance];
  • Mark the bridge's _moduleSetupComplete, mark the bridge's module setup completed!!!
  • Asynchronous!!! Most RCTModuleData can wait until the bridge is fully initialized and enter the RCTModuleData instance -- When instantiated, this type of module will enter the dispatch group dispatchGroup, and then asynchronously go to mainQueue for (void)[moduleData instance];

Here is an important marker, = true, the native module setup of the marker is completed!!!

Asynchronous: Most moduleData instances will be performed asynchronously through dispatchGroup.

4. ModuleClasse wrapping into RCTModuleData process

We know that a native module will go through two processes when used by RN bridge

  • Analysis of module class member method
  • module instnace process

First look at the first one, what happens when wrapping Class into RCTModuleData 1. initWithModuleClass 2. setUp: Helps judge some instance methods and a large number of configurations in Class

- (instancetype)initWithModuleClass:(Class)moduleClass
                     moduleProvider:(RCTBridgeModuleProvider)moduleProvider
                             bridge:(RCTBridge *)bridge
                     moduleRegistry:(RCTModuleRegistry *)moduleRegistry
            viewRegistry_DEPRECATED:(RCTViewRegistry *)viewRegistry_DEPRECATED
                      bundleManager:(RCTBundleManager *)bundleManager
                  callableJSModules:(RCTCallableJSModules *)callableJSModules
{
  if (self = [super init]) {
    _bridge = bridge;
    _moduleClass = moduleClass;
    _moduleProvider = [moduleProvider copy];
    _moduleRegistry = moduleRegistry;
    _viewRegistry_DEPRECATED = viewRegistry_DEPRECATED;
    _bundleManager = bundleManager;
    _callableJSModules = callableJSModules;
    [self setUp];
  }
  return self;
}
- (void)setUp {
    // 1. Whether instance is implemented -batchDidComplete method    _implementsBatchDidComplete = [_moduleClass instancesRespondToSelector:@selector(batchDidComplete)];
    // 2. Whether instance is implemented -batchDidComplete method    _implementsPartialBatchDidFlush = [_moduleClass instancesRespondToSelector:@selector(partialBatchDidFlush)];
    // 3. Whether the instance is used to be exposed?    _hasConstantsToExport = [_moduleClass instancesRespondToSelector:@selector(constantsToExport)];
    // 4. Whether module is forced to be set up in main queue    const BOOL implementsRequireMainQueueSetup = [_moduleClass respondsToSelector:@selector(requiresMainQueueSetup)];
    if (implementsRequireMainQueueSetup) {
        _requiresMainQueueSetup = [_moduleClass requiresMainQueueSetup];
    } else {
        ...
        // 5. When requirements are not implemented,        // - Is there a custom -init initialization method        // - _hasConstantsToExport || When hasCustomInit , main queue setup is required        const BOOL hasCustomInit = !_instance &amp;&amp; [_moduleClass instanceMethodForSelector:@selector(init)] != objectInitMethod;
        _requiresMainQueueSetup = _hasConstantsToExport || hasCustomInit;
    }
}

Among them, the more critical one is to describe several key properties of Module Class through RCTModuleData:

  • _implementsBatchDidComplete - mark whether instance implements the key method
  • _implementsPartialBatchDidFlush - mark whether instance implements the key method
  • _hasConstantsToExport - Tag module whether there is Constants Dictionary exposed to JS
  • _requiresMainQueueSetup - Note that it will be used in _prepareModulesWithDispatchGroup, marking module instance setup Yes, whether to force call in main queue. RN will perform module instance setup in child threads by default

5. When does RCTModuleData perform module instance

Before native module is actually used by JS, RCTModuleData needs to be instantiated module -- module instnace, also known as module instance setup. As mentioned earlier, a large number of module instnace procedures of RCTModuleData will be implemented in the RCTCxxBridge._prepareModulesWithDispatchGroup(...) method:

/// RCTCxxBridge handles all RCTModuleData, and calls the `moduleData instance` method to them- (void)_prepareModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup {
    // 1. Determine whether the subsequent `module instance` is asynchronously performed based on whether there is a dispatchGroup    BOOL initializeImmediately = NO;
    if (dispatchGroup == NULL) {
        RCTAssertMainQueue();
        initializeImmediately = YES;
    }
    // 2. _moduleDataByID caches all `module class` constructed `RCTModuleData`, traversing them, constructing a block, and determining whether to execute immediately according to the initializeImmediately parameter    for (RCTModuleData *moduleData in _moduleDataByID) {
        // 3. Note that, force, that is, `module instance` is forced to perform `setup` in main queue        if () {
            // 4. The process of `module instance setup` directly and actively call [moduleData instance] and forcefully trigger `[moduleData gatherConstants]` to collect the instance constants to be exposed to JS Dictionary            dispatch_block_t block = ^{
                if ( &amp;&amp; ![ isSubclassOfClass:[RCTCxxModule class]]) {
                    (void)[moduleData instance];
                    if (!RCTIsMainQueueExecutionOfConstantsToExportDisabled()) {
                        [moduleData gatherConstants];
                    }
                }
            };
            if (initializeImmediately &amp;&amp; RCTIsMainQueue()) {
                block();
            } else {
                if (dispatchGroup) {
                    dispatch_group_async(dispatchGroup, dispatch_get_main_queue(), block);
                }
            }
            // 5. RCTCxxBridge records all modules that perform `module instance` in main queue            _modulesInitializedOnMainQueue++;
        }
    }
}

Through the above code, we can see: Only when == true, module instance will be performed in the startup stage. If there is no mandatory module instance setup on the main thread, it is lazy module instance, or a lazy instantiated module!!!

Through the previous RN code, we can see that there are the following scenarios, the module instance is forced to be module instance during the startup stage:

  • When implementing the RCTBridgeModule protocol, the +requiresMainQueueSetup method returns true
  • If the +requiresMainQueueSetup method is not implemented, it is judged that hasCustomInit || _hasConstantsToExport, that is, if there is a custom-init method, or Constants exposed to JS, it must be forced to be in the main queue setup

5. Details of RCTModuleData in module instance

Post the instance method of RCTModuleData directly, the most important one is the method - setUpInstanceAndBridge:

- (id&lt;RCTBridgeModule&gt;)instance {
    ...
    // 1. _setupComplete tag module instance whether setup is completed    if(!_setupComplete) {
        [self setUpInstanceAndBridge:requestId];
    }
    ...
    return _instance;
}
- (void)setUpInstanceAndBridge:(int32_t)requestId {
    NSString *moduleName = [self name];
    // Note: Use instanceLock to protect _instance members    {
        std::unique_lock&lt;std::mutex&gt; lock(_instanceLock);
        // 1. Determine whether moduleData._setupComplete completes setup        BOOL shouldSetup = !_setupComplete &amp;&amp; _bridge.valid;
        // 2. If moduleData._instance is not instantiated, use _moduleProvider() to instantiate it        // - If instantiation fails, you also need to mark _setupComplete = true        if (shouldSetup) {
            if (!_instance) {
                // Instantiation using moduleProvider()                _instance = _moduleProvider ? _moduleProvider() : nil;
                if (!_instance) {
                    _setupComplete = YES;
                }
            }
        }
        // 3. Inject common attributes (bridge, moduleRegistry...) into module instnace!        if (shouldSetup) {
            [self setBridgeForInstance];
            [self setModuleRegistryForInstance];
            [self setViewRegistryForInstance];
            [self setBundleManagerForInstance];
            [self setCallableJSModulesForInstance];
        }
        // 4. Initialize the methodQueue of module instance        [self setUpMethodQueue];
        // 5. Call the initialize method in module instance, if implemented        if (shouldSetup) {
            [self _initializeModule];
        }
    } // instanceLock release    // 6. When will the global module instance be notified to be completed!!!    if (_bridge.moduleSetupComplete) {
        // 6.1 Most module instances are executed in moduleSetupComplete = ture, and will actively notify the global        [self finishSetupForInstance];
    } else {  
        // 6.2 A small number of them are executed when moduleSetupComplete = false, mark _requiresMainQueueSetup = NO, so that module instance is actually only half set. When lazy instance is used, `_bridge.moduleSetupComplete` must be true, then notify and call the `finishSetupForInstance` method        _requiresMainQueueSetup = NO;
    }
}
- (void)setUpMethodQueue {
    if (_instance &amp;&amp; !_methodQueue &amp;&amp; _bridge.valid) {
        // 1. Whether instance actively specifies methodQueue, moduleData holds        BOOL implementsMethodQueue = [_instance respondsToSelector:@selector(methodQueue)];
        if (implementsMethodQueue &amp;&amp; _bridge.valid) {
            _methodQueue = _instance.methodQueue;
        }
        // 2. instance is not specified, create child thread as methodQueue        if (!_methodQueue &amp;&amp; _bridge.valid) {
            _queueName = [NSString stringWithFormat:@".%@Queue", ];
            _methodQueue = dispatch_queue_create(_queueName.UTF8String, DISPATCH_QUEUE_SERIAL);
            // 3. If the `module instance` actively implements methodQueue, use KVC to set the `module instance` methodQueue!!!            if (implementsMethodQueue) {
                @try {
                    [(id)_instance setValue:_methodQueue forKey:@"methodQueue"];
                } @catch (NSException *exception) {
                    RCTLogError();
                }
            }
      }
}
// If the `module instance` implements the `-initialize` method of the `RCTBridgeModule` protocol, it is called actively, and the `_isInitialized` flag has been executed- (void)_initializeModule {
    if (!_isInitialized &amp;&amp; [_instance respondsToSelector:@selector(initialize)]) {
        _isInitialized = YES;
        [(id&lt;RCTInitializing&gt;)_instance initialize];
    }
}
// If this method is executed, notify bridge + globally: this module `setupComplete`!!!- (void)finishSetupForInstance {
    if (!_setupComplete &amp;&amp; _instance) {
        _setupComplete = YES;
        [_bridge registerModuleForFrameUpdates:_instance withModuleData:self];
        [[NSNotificationCenter defaultCenter] postNotificationName:RCTDidInitializeModuleNotification object:_bridge userInfo:@{@"module" : _instance, @"bridge" : RCTNullIfNil(_bridge.parentBridge)}];
    }
}

During the module instance process, there are the following points to be paid attention to:

  • instance = _moduleProvider(), while _moduleProvider implementations are basically [moduleClass new], which means initialization using -init.
  • It can be seen that when injecting a large number of attributes (bridge, ModuelRegistry...) into the module instance, KVC is used and wrapped in try-catch, because the RCTBridgeModule protocol implements an interface, if the module needs to use these injected APIs, it is necessary to manually use @synthesize to generate the corresponding member variables.
  • Regarding the method queue, when JS calls the export method of module instance, it will be executed asynchronously in the method queue.
  • The -initialize that calls module instance is provided by the RCTBridgeModule protocol. Please distinguish it from +initiali... in OC

For the call timing of finishSetupForInstance, please refer to the code comment. In addition, as long as it calls, it will perform the following operations:

  • Tag _setupComplete = YES
  • Need to notify and update, FrameUpdates related module in bridge
  • Global Notification RCTDidInitializeModuleNotification

The above is the detailed explanation of the management of Native Module in iOS RN startup. For more information about managing Native Module in iOS RN startup, please follow my other related articles!