React Native源碼解析-native和js通信

? React Native(以下簡稱RN)的目標是用基于react的JavaScript寫代碼,在iOS/Android平臺上原生渲染,正如他們的口號"Learn Once,Write anywhere!",只要學會了大前端,iOS/Android/Web通吃,這樣就很神奇了,react.js還是那個react.js,模塊化、虛擬DOM、JSX語法概念一樣沒少,甚至可以基于流行的flux單向數據流來架構我們的應用,而在客戶端并不是一個web頁面,而是純原生渲染,性能比純web頁提升很多,而且還順帶具有像web頁一樣的動態更新能力。

? 隨著版本的迭代更新,RN功能和相關特性也越來越多,代碼復雜度也隨之上升,這里先不論RN的爭議和發展趨勢,而來學習下其優秀的架構設計和代碼實現。我們就用一個最簡單的項目來進行剖析,運行命令react-native init RNDemo,這里我的RN版本號為:0.47.0,RN最核心的當屬js與native的通信機制,理解了這套機制則比較容易理解RN整個架構。

Native模塊

RCTBridgeModule

native導出給js的類稱之為模塊類,如RCTUIManager RCTTiming等,每個模塊類都實現了RCTBridgeModule協議

@protocol RCTBridgeModule <NSObject>

#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }

// Implemented by RCT_EXPORT_MODULE
+ (NSString *)moduleName;

@optional

@property (nonatomic, weak, readonly) RCTBridge *bridge;
@property (nonatomic, strong, readonly) dispatch_queue_t methodQueue;

#define RCT_EXPORT_METHOD(method) \
  RCT_REMAP_METHOD(, method)

#define RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(method) \
  RCT_REMAP_BLOCKING_SYNCHRONOUS_METHOD(, method)

#define RCT_REMAP_METHOD(js_name, method) \
  _RCT_EXTERN_REMAP_METHOD(js_name, method, NO) \
  - (void)method;
  
#define RCT_REMAP_BLOCKING_SYNCHRONOUS_METHOD(js_name, method) \
  _RCT_EXTERN_REMAP_METHOD(js_name, method, YES) \
  - (id)method;
  
#define RCT_EXTERN_MODULE(objc_name, objc_supername) \
  RCT_EXTERN_REMAP_MODULE(, objc_name, objc_supername)

#define RCT_EXTERN_REMAP_MODULE(js_name, objc_name, objc_supername) \
  objc_name : objc_supername \
  @end \
  @interface objc_name (RCTExternModule) <RCTBridgeModule> \
  @end \
  @implementation objc_name (RCTExternModule) \
  RCT_EXPORT_MODULE(js_name)
  
#define RCT_EXTERN_METHOD(method) \
  _RCT_EXTERN_REMAP_METHOD(, method, NO)

#define RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(method) \
  _RCT_EXTERN_REMAP_METHOD(, method, YES)

#define _RCT_EXTERN_REMAP_METHOD(js_name, method, is_blocking_synchronous_method) \
  + (NSArray *)RCT_CONCAT(__rct_export__, \
    RCT_CONCAT(js_name, RCT_CONCAT(__LINE__, __COUNTER__))) { \
    return @[@#js_name, @#method, @is_blocking_synchronous_method]; \
  }
  
- (NSArray<id<RCTBridgeMethod>> *)methodsToExport;
- (NSDictionary<NSString *, id> *)constantsToExport;
- (void)batchDidComplete;
- (void)partialBatchDidFlush;

@end

該協議定義了模塊導出方法、導出常量、模塊運行隊列等,還定義了很多宏。

RCT_EXPORT_MODULE

模塊類被加載進runtime的時候,執行load方法,將該模塊類的Class信息添加到一個全局數組RCTModuleClasses里去,RCTGetModuleClasses()方法可獲取該數組。

RCT_EXPORT_METHOD(method)

RN在e9095b2版本移除了bang神博客所說的從data數據段獲取導出方法的黑魔法,而是給每個導出方法添加一個對應的方法。

比如method為doSomething,則宏展開后為

+ (NSArray *)__rct_export__(行號和系統計數){ 
    return @[@"", @"doSomething", @(NO)];
  }
- (void)doSomething;

RCTModuleData方法- (NSArray<id<RCTBridgeMethod>> *)methods,通過遍歷模塊運行時方法列表,找到有__rct_export__前綴的方法,根據方法返回的數組實例化RCTModuleMethod,從而收集到所有RCT_EXPORT_METHOD對應的導出方法。

模塊配置

所有的模塊配置存放在ModuleRegistryC++類中,JSCExecutorgetNativeModule方法可獲得指定模塊的配置,最終是從ModuleRegistry類方法getConfig拿到。JSCNativeModules管理Native的導出模塊,JSCNativeModules構造函數傳入JsToNativeBridgegetModuleRegistry方法返回的ModuleRegistry指針,JsToNativeBridge管理js調用Native所需配置、方法等,是js調用native的native響應方。

RCTAppState模塊為例,拿到的模塊信息如下:

struct ModuleConfig {
  size_t index;
  folly::dynamic config;
};
{
  21;
  [AppState,{initialAppState:unknown},[getCurrentAppState,addListener,removeListeners]];
}

index是模塊index,config動態數組依次存放moudlename、export constants、export methodNames array 、promiseMethodId array 、syncMethodId array。

Native模塊生成

Native模塊初始化

在應用啟動delegate里,創建了一個RCTRootView實例,這個實例初始化了RCTBridge實例,在其- (void)setUp方法里:

self.batchedBridge = [[bridgeClass alloc] initWithParentBridge:self];
[self.batchedBridge start];

self.batchedBridgeRCTCxxBridge的實例,用于批量橋接,之前版本的RCTBatchedBridge已不再實現,在- (void)start方法主要步驟是:

  • 發送js即將加載通知
  • 創建常駐線程_jsThread,native和js互相調用默認會在該線程執行,也可以自己指定模塊的運行隊列

    _jsThread = [[NSThread alloc] initWithTarget:self
                                          selector:@selector(runJSRunLoop)
                                            object:nil];
    _jsThread.name = RCTJSThreadName;
    _jsThread.qualityOfService = NSOperationQualityOfServiceUserInteractive;
    [_jsThread start];
    
  • 初始化所有native modules

    - (void)_initModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup
    {
      ...
      NSArray<id<RCTBridgeModule>> *extraModules = nil;
      if (self.delegate) {
        if ([self.delegate respondsToSelector:@selector(extraModulesForBridge:)]) {
          extraModules = [self.delegate extraModulesForBridge:_parentBridge];
        }
      } else if (self.moduleProvider) {
        extraModules = self.moduleProvider();
      }
      ...
      NSMutableArray<Class> *moduleClassesByID = [NSMutableArray new];
      NSMutableArray<RCTModuleData *> *moduleDataByID = [NSMutableArray new];
      NSMutableDictionary<NSString *, RCTModuleData *> *moduleDataByName = [NSMutableDictionary new];
      ...
      // Set up moduleData for pre-initialized module instances
      for (id<RCTBridgeModule> module in extraModules) {
        Class moduleClass = [module class];
        NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);
    
        if (RCT_DEBUG) {
          ...
        }
    
        // Instantiate moduleData container
        RCTModuleData *moduleData = [[RCTModuleData alloc] initWithModuleInstance:module
                                                                           bridge:self];
        moduleDataByName[moduleName] = moduleData;
        [moduleClassesByID addObject:moduleClass];
        [moduleDataByID addObject:moduleData];
      }
      ...
      // Set up moduleData for automatically-exported modules
      for (Class moduleClass in RCTGetModuleClasses()) {
        NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);
        
        if ([moduleName isEqual:@"RCTJSCExecutor"]) {
          continue;
        }
    
        // Check for module name collisions
        RCTModuleData *moduleData = moduleDataByName[moduleName];
        if (moduleData) {
          if (moduleData.hasInstance) {
            // Existing module was preregistered, so it takes precedence
            continue;
          } else if ([moduleClass new] == nil) {
            // The new module returned nil from init, so use the old module
            continue;
          } else if ([moduleData.moduleClass new] != nil) {
            // Both modules were non-nil, so it's unclear which should take precedence
            RCTLogError(@"Attempted to register RCTBridgeModule class %@ for the "
                        "name '%@', but name was already registered by class %@",
                        moduleClass, moduleName, moduleData.moduleClass);
          }
        }
    
        // Instantiate moduleData
        // TODO #13258411: can we defer this until config generation?
        moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass
                                                         bridge:self];
        moduleDataByName[moduleName] = moduleData;
        [moduleClassesByID addObject:moduleClass];
        [moduleDataByID addObject:moduleData];
      }
      ...
    
      // Store modules
      _moduleDataByID = [moduleDataByID copy];
      _moduleDataByName = [moduleDataByName copy];
      _moduleClassesByID = [moduleClassesByID copy];
      ...
      // Dispatch module init onto main thead for those modules that require it
      for (RCTModuleData *moduleData in _moduleDataByID) {
        if (moduleData.hasInstance &&
            (!moduleData.requiresMainQueueSetup || RCTIsMainQueue())) {
          (void)[moduleData instance];
        }
      }
      ...
      // From this point on, RCTDidInitializeModuleNotification notifications will
      // be sent the first time a module is accessed.
      _moduleSetupComplete = YES;
    
      [self _prepareModulesWithDispatchGroup:dispatchGroup];
    
      ...
    }
    

    主要步驟:

    1. 得到bridgeModule數組extraModules

      從delegate實現方或傳入的moduleProvider屬性獲取extraModules,這里兩者都為空

    2. 實例化moduleClassesByID Class數組,moduleDataByID RCTModuleData*模塊數據數組,moduleDataByName名字模塊數據字典,臨時保存,作用見名思義

    3. 遍歷extraModules,填充第二步數組和字典

    4. RCTGetModuleClasses()得到聲明了RCT_EXPORT_MODULE的所有模塊Class,遍歷數組,先檢查命名沖突,再以moduleClass為參數實例化RCTModuleData,然后填充第二步數組和字典

      RCTModuleData類管理導出給js的模塊數據,包括Class信息,導出方法,導出常量等

    5. 遍歷_moduleDataByID,調用RCTModuleData對應實例的instance方法,初始化RCTModuleData

    6. 執行_prepareModulesWithDispatchGroup方法,初始化除白名單外的模塊導出常量

  • 實例化Instance類,該類在下文有介紹

    _reactInstance.reset(new Instance);
    
  • 實例化抽象工廠類JSExecutorFactory

    __weak RCTCxxBridge *weakSelf = self;
      std::shared_ptr<JSExecutorFactory> executorFactory;
      if (!self.executorClass) {
        BOOL useCustomJSC =
          [self.delegate respondsToSelector:@selector(shouldBridgeUseCustomJSC:)] &&
          [self.delegate shouldBridgeUseCustomJSC:self];
        // The arg is a cache dir.  It's not used with standard JSC.
        executorFactory.reset(new JSCExecutorFactory(folly::dynamic::object
          ("UseCustomJSC", (bool)useCustomJSC)
    #if RCT_PROFILE
          ("StartSamplingProfilerOnInit", (bool)self.devSettings.startSamplingProfilerOnLaunch)
    #endif
        ));
      } else {
        id<RCTJavaScriptExecutor> objcExecutor = [self moduleForClass:self.executorClass];
        executorFactory.reset(new RCTObjcExecutorFactory(objcExecutor, ^(NSError *error) {
          if (error) {
            [weakSelf handleError:error];
          }
        }));
      }
    

    本地JavaScriptCore運行會實例化JSCExecutorFactory, 瀏覽器遠程調試模式會實例RCTObjcExecutorFactory,這里我們就以JSCExecutorFactory為例分析,遠程調試會在另一篇做分析。

  • _jsThread線程上初始化橋接

    - (void)_initializeBridge:(std::shared_ptr<JSExecutorFactory>)executorFactory
    {
      if (!self.valid) {
        return;
      }
    
      RCTAssertJSThread();
      __weak RCTCxxBridge *weakSelf = self;
      _jsMessageThread = std::make_shared<RCTMessageThread>([NSRunLoop currentRunLoop], ^(NSError *error) {
        if (error) {
          [weakSelf handleError:error];
        }
      });
    
      RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge initializeBridge:]", nil);
      // This can only be false if the bridge was invalidated before startup completed
      if (_reactInstance) {
        // This is async, but any calls into JS are blocked by the m_syncReady CV in Instance
        _reactInstance->initializeBridge(
          std::unique_ptr<RCTInstanceCallback>(new RCTInstanceCallback(self)),
          executorFactory,
          _jsMessageThread,
          [self _buildModuleRegistry]);
    
    #if RCT_PROFILE
        ...
    #endif
      }
    
      RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
    }
    

    主要步驟:

    1. 實例化RCTMessageThread類,RCTMessageThread類封裝了在_jsThread常駐線程上的同步和異步執行任務的方法。
    2. Instance實例_reactInstance執行了初始化方法void initializeBridge(..), 注入了所需依賴,Instance是iOS/Android與javacriptCore交互的入口類。初始化了模塊注冊表ModuleRegistry實例,ModuleRegistry是C++類,ios/android均需填充模塊信息數組
  • 加載jsbundle源文件

    RCTJavaScriptLoader封裝了加載jsbundle文件的方法,主要步驟是需要下載則通過RCTMultipartDataTask下載js文件,最后返回NSData數據

  • 執行解析jsbundle

    dispatch_group_notify(prepareBridge, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
          RCTCxxBridge *strongSelf = weakSelf;
          if (sourceCode && strongSelf.loading) {
            [strongSelf executeSourceCode:sourceCode sync:NO];
          }
        });
    

    在上述組任務結束時,收到通知,在最高等級全局隊列執行加載完的jsbundle

    - (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync
    {
      // This will get called from whatever thread was actually executing JS.
      dispatch_block_t completion = ^{
        // Flush pending calls immediately so we preserve ordering
        [self _flushPendingCalls];
    
        // Perform the state update and notification on the main thread, so we can't run into
        // timing issues with RCTRootView
        dispatch_async(dispatch_get_main_queue(), ^{
          [[NSNotificationCenter defaultCenter]
           postNotificationName:RCTJavaScriptDidLoadNotification
           object:self->_parentBridge userInfo:@{@"bridge": self}];
    
          // Starting the display link is not critical to startup, so do it last
          [self ensureOnJavaScriptThread:^{
            // Register the display link to start sending js calls after everything is setup
            [self->_displayLink addToRunLoop:[NSRunLoop currentRunLoop]];
          }];
        });
      };
    
      if (sync) {
        [self executeApplicationScriptSync:sourceCode url:self.bundleURL];
        completion();
      } else {
        [self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:completion];
      }
    
    #if RCT_DEV
      ...
    #endif
    }
    

    主要步驟:

    1. 清空調用隊列_pendingCalls_pendingCount置0

    2. 發送js加載完畢通知,_jsThread線程,設置RCTDisplayLink監測js線程幀率,原理在另一篇作分析

    3. 執行解析jsbundle,這里是異步執行

      執行解析中間過程為了解耦和擴展性,引入了NativeToJsBridge等類,層次很多,可能看到這就有點暈了,來看下這塊的執行過程。

jsBundle執行

首先,上文提到的JSCExecutorFactory,運用了標準的工廠模式:

JSCExecutor是本地JavaScriptCore具體執行的產品類,是跨平臺的C++類,是一個非常重要的類。

JSCExecutor構造時向JSContext注入了全局函數

installNativeHook<&JSCExecutor::nativeFlushQueueImmediate>("nativeFlushQueueImmediate");
installNativeHook<&JSCExecutor::nativeCallSyncHook>("nativeCallSyncHook");
...
installGlobalProxy(m_context, "nativeModuleProxy",
                       exceptionWrapMethod<&JSCExecutor::getNativeModule>());
...
installNativeHook<&JSCExecutor::nativeRequire>("nativeRequire");                     

installNativeHook方法同時注冊了回調函數exceptionWrapMethod<method>(),當JS端調用對應方法,exceptionWrapMethod全局函數被調用。

? MessageQueueThreadRCTMessageThread的抽象基類,封裝了在消息隊列里同步和異步執行的方法,MessageQueueThread是需要各平臺各自實現的。

class MessageQueueThread {
 public:
  virtual ~MessageQueueThread() {}
  virtual void runOnQueue(std::function<void()>&&) = 0;
  // runOnQueueSync and quitSynchronous are dangerous.  They should only be
  // used for initialization and cleanup.
  virtual void runOnQueueSync(std::function<void()>&&) = 0;
  // Once quitSynchronous() returns, no further work should run on the queue.
  virtual void quitSynchronous() = 0;
};

Instance類依賴了很多類,它是一個C++類,是iOS/Android與javacriptCore交互的入口類,封裝了native與js的交互,包括解析js字符流,調用js方法,設置jscontext全局變量,發出回調等。

class RN_EXPORT Instance {
 public:
  ~Instance();
  void initializeBridge(
    std::unique_ptr<InstanceCallback> callback,
    std::shared_ptr<JSExecutorFactory> jsef,
    std::shared_ptr<MessageQueueThread> jsQueue,
    std::shared_ptr<ModuleRegistry> moduleRegistry);

  void setSourceURL(std::string sourceURL);

  void loadScriptFromString(
    std::unique_ptr<const JSBigString> string,
    std::string sourceURL,
    bool loadSynchronously);
  void loadUnbundle(
    std::unique_ptr<JSModulesUnbundle> unbundle,
    std::unique_ptr<const JSBigString> startupScript,
    std::string startupScriptSourceURL,
    bool loadSynchronously);
  bool supportsProfiling();
  void startProfiler(const std::string& title);
  void stopProfiler(const std::string& title, const std::string& filename);
  void setGlobalVariable(std::string propName, std::unique_ptr<const JSBigString> jsonValue);
  void *getJavaScriptContext();
  void callJSFunction(std::string&& module, std::string&& method, folly::dynamic&& params);
  void callJSCallback(uint64_t callbackId, folly::dynamic&& params);

  // This method is experimental, and may be modified or removed.
  template <typename T>
  Value callFunctionSync(const std::string& module, const std::string& method, T&& args) {
    CHECK(nativeToJsBridge_);
    return nativeToJsBridge_->callFunctionSync(module, method, std::forward<T>(args));
  }

  #ifdef WITH_JSC_MEMORY_PRESSURE
  void handleMemoryPressure(int pressureLevel);
  #endif

 private:
  void callNativeModules(folly::dynamic&& calls, bool isEndOfBatch);
  void loadApplication(
    std::unique_ptr<JSModulesUnbundle> unbundle,
    std::unique_ptr<const JSBigString> startupScript,
    std::string startupScriptSourceURL);
  void loadApplicationSync(
    std::unique_ptr<JSModulesUnbundle> unbundle,
    std::unique_ptr<const JSBigString> startupScript,
    std::string startupScriptSourceURL);

  std::shared_ptr<InstanceCallback> callback_;
  std::unique_ptr<NativeToJsBridge> nativeToJsBridge_;
  std::shared_ptr<ModuleRegistry> moduleRegistry_;

  std::mutex m_syncMutex;
  std::condition_variable m_syncCV;
  bool m_syncReady = false;
};

Instance類依賴的InstanceCallback JSExecutorFactory MessageQueueThread ModuleRegistry等類,都需要平臺各自實現,Instance是作為一個跨平臺的接口封裝類,如iOS中,callback成員變量是指向InstanceCallback的子類RCTInstanceCallbackjsef成員則動態指向JSExecutorFactory的具體工廠類,JSExecutorFactory則可以選擇對應的具體產品類。jsQueue指向RCTMessageThread類,moduleRegistry需要各平臺自行填充。Instance類函數實現基本都是通過NativeToJsBridge具體實現的,在Instance執行initializeBridge時,在MessageQueueThread同步初始化了NativeToJsBridge的實例nativeToJsBridge_,那么我們來看看NativeToJsBridge類。

? 在RCTCxxBridge類的enqueueApplicationScript:url:onComplete:方法,根據jsbundle類型去執行對應的方法。jsbundle目前有三種類型:String RAMBundle BCBundleString表示普通jsbundle,用bundle命令整合出來的。RAMBundle是用unbundle命令打出來的bundle,它除了生成整合的js文件index.ios.bundle 外,還會生成各個單獨的未整合js文件,全部放在js-modules目錄下, bundle頭四個字節固定為0xFB0BD1E5BCBundle是js字節碼bundle類型,并未用到,就以普通jsbundle為例

self->_reactInstance->loadScriptFromString(std::make_unique<NSDataBigString>(script),
                                                 sourceUrlStr.UTF8String, false);

核心是通過Instance實例去執行解析, InstanceloadScriptFromString方法調用到NativeToJsBridgeloadApplicationloadApplicationSync的方法,NativeToJsBridge實例在Instance類構造的時候在_jsThread線程初始化。

void NativeToJsBridge::loadApplication(
    std::unique_ptr<JSModulesUnbundle> unbundle,
    std::unique_ptr<const JSBigString> startupScript,
    std::string startupScriptSourceURL) {
  runOnExecutorQueue(
      [unbundleWrap=folly::makeMoveWrapper(std::move(unbundle)),
       startupScript=folly::makeMoveWrapper(std::move(startupScript)),
       startupScriptSourceURL=std::move(startupScriptSourceURL)]
        (JSExecutor* executor) mutable {
    auto unbundle = unbundleWrap.move();
    if (unbundle) {
      executor->setJSModulesUnbundle(std::move(unbundle));
    }
    executor->loadApplicationScript(std::move(*startupScript),
                                    std::move(startupScriptSourceURL));
  });
}

普通jsbundle調用到JSExecutor子類loadApplicationScript方法,以子類JSCExecutor為例:

void JSCExecutor::loadApplicationScript(std::unique_ptr<const JSBigString> script, std::string sourceURL) {
  SystraceSection s("JSCExecutor::loadApplicationScript",
                    "sourceURL", sourceURL);

  std::string scriptName = simpleBasename(sourceURL);
  ReactMarker::logTaggedMarker(ReactMarker::RUN_JS_BUNDLE_START, scriptName.c_str());
  String jsSourceURL(m_context, sourceURL.c_str());

  // TODO t15069155: reduce the number of overrides here
#ifdef WITH_FBJSCEXTENSIONS
  ...
#elif defined(__APPLE__)
  BundleHeader header;
  memcpy(&header, script->c_str(), std::min(script->size(), sizeof(BundleHeader)));
  auto scriptTag = parseTypeFromHeader(header);

  if (scriptTag == ScriptTag::BCBundle) {
    using file_ptr = std::unique_ptr<FILE, decltype(&fclose)>;
    file_ptr source(fopen(sourceURL.c_str(), "r"), fclose);
    int sourceFD = fileno(source.get());

    JSValueRef jsError;
    JSValueRef result = JSC_JSEvaluateBytecodeBundle(m_context, NULL, sourceFD, jsSourceURL, &jsError);
    if (result == nullptr) {
      throw JSException(m_context, jsError, jsSourceURL);
    }
  } else
#endif
  {
    String jsScript;
    {
      SystraceSection s_("JSCExecutor::loadApplicationScript-createExpectingAscii");
      ReactMarker::logMarker(ReactMarker::JS_BUNDLE_STRING_CONVERT_START);
      jsScript = adoptString(std::move(script));
      ReactMarker::logMarker(ReactMarker::JS_BUNDLE_STRING_CONVERT_STOP);
    }
    #ifdef WITH_FBSYSTRACE
    fbsystrace_end_section(TRACE_TAG_REACT_CXX_BRIDGE);
    #endif

    SystraceSection s_("JSCExecutor::loadApplicationScript-evaluateScript");
    evaluateScript(m_context, jsScript, jsSourceURL);
  }

  flush();

  ReactMarker::logMarker(ReactMarker::CREATE_REACT_CONTEXT_STOP);
  ReactMarker::logMarker(ReactMarker::RUN_JS_BUNDLE_STOP);
}

該方法主要步驟:

  1. 根據JSBundle header得到JSBundle類型來執行對應的js上下文。

    evaluateScript是由jshelps組件封裝的,最終調用到JavaScriptCore的方法JSEvaluateScriptjschelps主要封裝了JavaScriptCore的相關函數,以及JSStringRef的C++對象封裝String類,JSObjectRef的C++對象封裝Object類,JSValueRef的C++對象封裝Value類等。

  2. 調用flush()

    native調用jsflushedQueue方法,返回js端待調用方法隊列,然后native執行,清空該隊列

至此,Native模塊生成和相關準備工作完成,模塊配置存放在ModuleRegistry類中。

JS模塊

本地jscore運行時,當js需要調用到native模塊的時候,通過nativeModuleProxy執行native所注入方法,返回對應的模塊信息,而當遠程調試模式時,native向global.__fbBatchedBridgeConfig注入了所有模塊列表信息,同樣是由native端生成的,如下:

type ModuleConfig = [
  string, /* name */
  ?Object, /* constants */
  Array<string>, /* functions */
  Array<number>, /* promise method IDs */
  Array<number>, /* sync method IDs */
];
globalconfig

在JS中同樣也存在供native調用的模塊,node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js中,模塊保存在_lazyCallableModules中。

js modules

JS模塊生成

registerCallableModule(name: string, module: Object) {
    this._lazyCallableModules[name] = () => module;
  }

  registerLazyCallableModule(name: string, factory: void => Object) {
    let module: Object;
    let getValue: ?(void => Object) = factory;
    this._lazyCallableModules[name] = () => {
      if (getValue) {
        module = getValue();
        getValue = null;
      }
      return module;
    };
  }

通過外部注冊,填充_lazyCallableModules數組,

Native 調用 JS

在RN里,封裝了底層細節,外部暴露出的是通過RCTCxxBridge方法enqueueJSCall:method:args:completion調用,如native向js發送時間消息的方法sendEventWithName:body實現就是調用該方法。該方法實現如下:

- (void)enqueueJSCall:(NSString *)module method:(NSString *)method args:(NSArray *)args completion:(dispatch_block_t)completion
{
  if (!self.valid) {
    return;
  }

  /**
   * AnyThread
   */
  RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge enqueueJSCall:]", nil);

  RCTProfileBeginFlowEvent();
  [self _runAfterLoad:^{
    RCTProfileEndFlowEvent();

    if (self->_reactInstance) {
      self->_reactInstance->callJSFunction([module UTF8String], [method UTF8String],
                                           [RCTConvert folly_dynamic:args ?: @[]]);

      // ensureOnJavaScriptThread may execute immediately, so use jsMessageThread, to make sure
      // the block is invoked after callJSFunction
      if (completion) {
        if (self->_jsMessageThread) {
          self->_jsMessageThread->runOnQueue(completion);
        } else {
          RCTLogWarn(@"Can't invoke completion without messageThread");
        }
      }
    }
  }];

  RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}

主要通過調用Instance類的callsJSFunction方法,最終調用到JSCExecutor::callFunction方法

void JSCExecutor::callFunction(const std::string& moduleId, const std::string& methodId, const folly::dynamic& arguments) {
  SystraceSection s("JSCExecutor::callFunction");

  // This weird pattern is because Value is not default constructible.
  // The lambda is inlined, so there's no overhead.
  auto result = [&] {
    try {
      if (!m_callFunctionReturnResultAndFlushedQueueJS) {
        bindBridge();
      }
      return m_callFunctionReturnFlushedQueueJS->callAsFunction({
        Value(m_context, String::createExpectingAscii(m_context, moduleId)),
        Value(m_context, String::createExpectingAscii(m_context, methodId)),
        Value::fromDynamic(m_context, std::move(arguments))
      });
    } catch (...) {
      std::throw_with_nested(
        std::runtime_error("Error calling " + moduleId + "." + methodId));
    }
  }();

  callNativeModules(std::move(result));
}

callFunction方法先執行js端方法callFunctionReturnFlushedQueue(在MessageQueue.js文件中),返回js端消息隊列,然后native解析隊列,即調用callNativeModules,這個過程在下文JS調用Native有分析。

總體來說還是使用JSCHelpers中封裝的C++方法evaluateScript(JSContextRef, JSStringRef, JSStringRef),在常駐線程來執行js語句,返回結果native解析。

JS調用Native

node_modules/react-native/Libraries/BatchedBridge/NativeModules.js文件中:

let NativeModules : {[moduleName: string]: Object} = {};
if (global.nativeModuleProxy) {
  NativeModules = global.nativeModuleProxy;
} else {
  ...
}
 module.exports = NativeModules;

本地JavascriptCore執行時,nativeModuleProxy全局函數在JSCExecutor構造時,通過installGlobalProxy方法注入了,這里的else分支是瀏覽器遠程調試走的。當取nativeModuleProxy屬性,如執行const RCTAppState = NativeModules.AppState;JSObjectGetPropertyCallback回調在C++端被觸發,調用到JSValueRef JSCExecutor::getNativeModule(JSObjectRef object, JSStringRef propertyName)方法,該方法通過JSCNativeModulesgetModule方法拿到native對應配置,如第一節?模塊配置中拿到對應的配置表。

? js端也有 有BatchedBridge概念,node_modules/react-native/Libraries/BatchedBridge/BatchedBridge.js中,const BatchedBridge = new MessageQueue();BatchedBridge對象實際上是MessageQueue的實例,轉到當前目錄下的MessageQueue.js文件。

? js需要調用native方法的時候,調用enqueueNativeCall函數,比如js端執行方法:

UIManager.createView(tag, this.viewConfig.uiViewClassName, nativeTopRootTag, updatePayload)

這段代碼是在ReactNativeStack-dev.js中,用于js端通告native創建視圖,UIManager實際是NativeModules對象,本地JavacriptCore運行時,NativeModules對象方法在native的JSCExecutor::getNativeModule方法中通過調用js方法global.__fbGenNativeModule建立,global.__fbGenNativeModule即指向genModule方法對象,genModule方法中調用genMethod,genMethod中持有閉包,將native方法調用通過BatchedBridge.enqueueNativeCall(moduleID, methodID, args, onFail, onSuccess);方法加入隊列處理,故上述方法調用最終通過enqueueNativeCall調用。

enqueueNativeCall(moduleID: number, methodID: number, params: Array<any>, onFail: ?Function, onSucc: ?Function) {
    if (onFail || onSucc) {
      if (__DEV__) {
        ...
      }
      // Encode callIDs into pairs of callback identifiers by shifting left and using the rightmost bit
      // to indicate fail (0) or success (1)
      onFail && params.push(this._callID << 1);
      onSucc && params.push((this._callID << 1) | 1);
      this._successCallbacks[this._callID] = onSucc;
      this._failureCallbacks[this._callID] = onFail;
    }

    if (__DEV__) {
      ...
    }
    this._callID++;

    this._queue[MODULE_IDS].push(moduleID);
    this._queue[METHOD_IDS].push(methodID);

    if (__DEV__) {
      ...
    }
    this._queue[PARAMS].push(params);

    const now = new Date().getTime();
    if (global.nativeFlushQueueImmediate &&
        (now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS ||
         this._inCall === 0)) {
      var queue = this._queue;
      this._queue = [[], [], [], this._callID];
      this._lastFlush = now;
      global.nativeFlushQueueImmediate(queue);
    }
    Systrace.counterEvent('pending_js_to_native_queue', this._queue[0].length);
    if (__DEV__ && this.__spy && isFinite(moduleID)) {
      ...
    } else if (this.__spy) {
      this.__spy({type: TO_NATIVE, module: moduleID + '', method: methodID, args: params});
    }
  }

? enqueueNativeCall_queue依次插入moduleID methodID params ,flushedQueue方法會把當前的_callID插入到_queue最后,緊接著判斷相鄰兩次flushQueue時間超過MIN_TIME_BETWEEN_FLUSHES_MS即5ms,或者當前沒有正在處理的方法,則執行全局nativeFlushQueueImmediate函數。nativeFlushQueueImmediate函數傳入_queue參數,它在native端之前通過installNativeHook注入了,js端調用后native端收到函數回調,最終對應執行JSCExecutor類的nativeFlushQueueImmediate方法,該方法最終調用到JsToNativeBridgecallNativeModules方法,callNativeModules解析出js透傳的參數_queue,然后動態調用方法。

for (auto& call : parseMethodCalls(std::move(calls))) {
      m_registry->callNativeMethod(call.moduleId, call.methodId, std::move(call.arguments), call.callId);
    }

m_registry是ModuleRegistry的實例

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);
}

invoke方法即以反射去動態執行方法,具體執行方法各平臺各自實現,iOS上實際執行的類是RCTNativeModule,它的invoke方法在參數對應的module指定線程隊列執行invokeInner方法,然后經過轉換參數等操作,最終調用到RCTModuleMethod類的invokeWithBridge:module:arguments方法,通過NSInvocationinvokeWithTarget方法實現動態調用,并返回調用結果,中間經過了處理method name, methodSignature等過程,此處代碼可瀏覽RCTModuleMethod的類實現。另外在processMethodSignature方法中,將cbID和返回結果暫存,調用成功通過JSCExecutorm_invokeCallbackAndReturnFlushedQueueJS屬性 ,調用到js里MessageQueue類的invokeCallbackAndReturnFlushedQueue方法,js端拿到返回值,js調用native的閉環形成。

? 那么還有一個問題,js只是把消息加入了隊列,js什么時候去讓native去取js的消息隊列處理?

  1. js端超時機制

    需要注意的是,遠程調試模式并沒有超時機制,global.nativeFlushQueueImmediate始終是 undefined的。

    每次消息入隊的時候,會檢查距離上次隊列清空完成是否超過5ms,超過則調用nativeFlushQueueImmediate 清空隊列,native注冊回調被調用,否則立即入隊,由于js是單線程的,5ms內也不會積壓很多消息,所以不用擔心處理效率問題。

  2. native主動調用

    native調用js方法,native調用enqueueJSCall:method:args:completion方法會取到js消息隊列,其實包含

      folly::Optional<Object> m_invokeCallbackAndReturnFlushedQueueJS;
      folly::Optional<Object> m_callFunctionReturnFlushedQueueJS;
      folly::Optional<Object> m_flushedQueueJS;
      folly::Optional<Object> m_callFunctionReturnResultAndFlushedQueueJS;
    

    處理方法都會返回js消息隊列,即native每次調用js,都會主動去取js隊列,比如事件消息、timer等。

綜上所述,js調用native實際上是有兩種機制的:

  1. native向jscontext的 global注入全局對象,同時注冊相應的回調,如nativeFlushQueueImmediate,js函數被調用,對應native回調被響應
  2. js組成消息隊列,native調用flushedQueue主動去取

第一種是JSPatch所采用的,不過它注冊的回調是一個block, 第二種機制是最復雜的,對于模塊,需要兩端維護一份配置表,但是最高效的,js方需要執行native方法,僅需傳遞moduleId methodId arguments必要參數給native,而方法真正執行是在native方異步執行的,返回結果異步返回給js方,如果換成方式1,native方法在jscontext同步執行,明顯影響效率,而且 當短時間內有很多條消息,JS并不會去頻繁調用native,會在5ms內去累積消息,然后發送給native。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容

  • 使用App.png 本文結構 目前App的幾種常見的開發模式 關于React-Native的一點小看法 React...
    ZeroJ閱讀 5,247評論 0 22
  • React Native 是最近非常火的一個話題,介紹如何利用 React Native 進行開發的文章和書籍多如...
    零度_不結冰閱讀 685評論 0 1
  • 我為什么寫這個主題呢,這是因為今天早上有兩個同學發燒了,老師說今天這兩個同學很可能有腮腺炎的細菌。下午更恐怖...
    破雷神龍閱讀 347評論 0 4
  • correct = tf.nn.in_top_k(prediction, target, K): K --- 表示...
    律動的時間線閱讀 494評論 0 0
  • 我小的時候是城市里的孩子,但是我卻特別喜歡農村,那時候我的暑假寒假基本上都是在鄉下度過的,小伙伴們也愛跟我玩,好像...
    安然ZCR閱讀 469評論 4 5