React Native橋接器初探

本文假設你已經有必定的React Native基礎,而且想要了解React Native的JS和原生代碼之間是如何交互的。javascript

React Native的工做線程

  • shadow queue:佈局在這個線程工做html

  • main thread:UIKit在這裏工做java

  • Javascript thread:Js代碼在這裏工做react

另外每個原生模塊都有本身的一個工做GCD queue,除非你明確指定它的工做隊列ios

*shadow queue*實際是一個GCD queue,而不是一個線程。

原生模塊

若是你還不知道如何建立原聲模塊,我建議你看看官方文檔git

下面是一個叫作Person的原生模塊,既能夠被js調用,也能夠調用js代碼。github

@interface Person : NSObject <RCTBridgeModule>
@end

@implementation Logger

RCT_EXPORT_MODULE()

RCT_EXPORT_METHOD(greet:(NSString *)name)
{
  NSLog(@"Hi, %@!", name);
  [_bridge.eventDispatcher sendAppEventWithName:@"greeted"
                                           body:@{ @"name": name }];
}

@end

下面,咱們主要看看代碼裏用到的兩個宏定義:RCT_EXPORT_MODULERCT_EXPORT_METHOD。看看他們是如何工做的。正則表達式

RCT_EXPORT_MODULE([js_name])

這個宏的功能就和它名字說的同樣,處處一個模塊。可是export是什麼意思呢?它的意思是讓React Native的bridge(橋接)感知到原生模塊。objective-c

它的定義其實很是的簡單:算法

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

它的做用:

  • 首先它聲明瞭RCTRegisterModuleextern方法,也就是說這個方法的實如今編譯的時候不可知,而在link的時候纔可知。

  • 聲明瞭一個方法moduleName,這個方法返回可選的宏定義參數js_name,通常是你但願有一個專門的模塊名稱,而不是默認的ObjC類名的時候使用。

  • 最後,聲明瞭一個load方法(當app被加載進內存的時候,load方法也會被調用)。在這個方法裏調用RCTRegisterModule方法來讓RN的bridge感知到這個模塊。

RCT_EXPORT_METHOD(method)

這個宏更有意思,它並給你的模塊添加任何實際的方法。它建立了一個新的方法,這個新的方法基本上是這樣的:

+ (NSArray *)__rct_export__120
{
    return @[@"", @"log: (NSString *)message"];
}

這個被load方法生成的方法的名稱由前綴(__rct_export__)和一個可選的js_name(如今是空的)和聲明的行號(好比12)和__COUNTER__宏拼接在一塊兒組成。

這個新生成的方法的做用就是返回一個數組,這個數組包含一個可選的js_name(在本例中是空的)和方法的簽名。簽名說的那一堆是爲了不方法崩潰。

即便是這麼複雜的生成算法,若是你使用了*category*的話也不免會有兩個方法的名稱是同樣的。不過這個機率很是低,而且也不會產生什麼不可控的行爲。雖然Xcode會這麼警告。

Runtime

這一步只作一件事,那就是給React Native的橋接模塊提供信息。這樣它就能夠找到原生模塊裏export出來的所有信息:modulesmethods,並且這些所有發生在load的時候。

下圖是React Native橋接的依賴圖

初始化模塊

方法RCTRegisterModule方法就是用來把類添加到一個數組裏,這樣React Native橋接器實例建立以後能夠找到這個模塊。它會遍歷模塊數組,建立每一個模塊的實例,並在橋接器裏保存它的引用,而且每一個模塊的實例也會保留橋接器的實例。而且該方法還會檢查模塊是否指定了運行的隊列,若是沒有指定那麼就運行在一個新建的隊列上,與其餘隊列分割。

NSMutableDictionary *modulesByName; // = ...
for (Class moduleClass in RCTGetModuleClasses()) {
  // ...
  module = [moduleClass new];
  if ([module respondsToSelector:@selector(setBridge:)]) {
    module.bridge = self;
  }
  modulesByName[moduleName] = module;
  // ...
}

配置原生模塊

一旦在後臺線程裏有了模塊實例,咱們就列出每一個模塊的所有方法,以後調用__rct_export__開始的方法,這樣咱們就有一個該方法簽名的字符串。這樣咱們後續就能夠得到參數的實際類型。在運行時,咱們只會知道參數的類型是id,按照上面的方法就能夠得到參數的實際類型,好比本例的是NSString*

unsigned int methodCount;
Method *methods = class_copyMethodList(moduleClass, &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
  Method method = methods[i];
  SEL selector = method_getName(method);
  if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) {
    IMP imp = method_getImplementation(method);
    NSArray *entries = ((NSArray *(*)(id, SEL))imp)(_moduleClass, selector);
    //...
    [moduleMethods addObject:/* Object representing the method */];
  }
}

初始化Javascript執行器

JavaScript執行器有一個setUp方法。用這個方法能夠執行不少耗費資源的任務,好比在後臺線程裏初始化JavaScriptCore。因爲只有active的執行器才能夠接受到setUp的調用,因此也節約了不少的資源。

JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);
_context = [[RCTJavaScriptContext alloc] initWithJSContext:ctx];

注入Json配置

模塊的配置都是Json形式的,如:

{
  "remoteModuleConfig": {
    "Logger": {
      "constants": { /* If we had exported constants... */ },
      "moduleID": 1,
      "methods": {
        "requestPermissions": {
          "type": "remote",
          "methodID": 1
        }
      }
    }
  }
}

這些都做爲全局變量存儲在JavaScript VM裏,所以當橋接器的Js側代碼初始化完畢的時候它能夠用這些信息來建立原生模塊。

加載JavaScript代碼

能夠得到代碼的地方只有兩個,在開發的時候從packager下載代碼,在產品環境下從磁盤加載代碼。

執行JavaScript代碼

一旦全部的準備工做就緒,咱們就能夠把App的代碼都加載到JavaScript Core裏解析,執行。在最開始執行的時候,全部的CommonJS模塊都會被註冊(如今你寫的是ES6的模塊,不是CommonJS,可是最後會轉碼爲ES5),並require入口文件。

JSValueRef jsError = NULL;
JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script);
JSStringRef jsURL = JSStringCreateWithCFString((__bridge CFStringRef)sourceURL.absoluteString);
JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, &jsError);
JSStringRelease(jsURL);
JSStringRelease(execJSString);

JavaScript模塊

這個時候,上例中的原生模塊就能夠在NativeModules對象裏調用了。

var { NativeModules } = require('react-native');
var { Person } = NativeModules;

Person.greet('Tadeu');

當你調用一個原生模塊的方法的時候,它會在一個隊列裏執行。其中包含模塊名、方法名和調用這個方法須要的所有參數。在JavaScript執行結束的時候原生代碼繼續執行。

調用週期

下面看看若是咱們調用上面的代碼會發生什麼:

代碼的調用從Js開始,以後開始原生代碼的執行。Js傳入的回調會經過橋接器(原生模塊使用_bridge實例調用enqueueJSCall:args:)傳回到JS代碼。

注意:你若是看過文檔,或者親自實踐過的話你就會知道也有從原生模塊調用JS的狀況。這個是用vSYNC實現的。可是這些爲了改善啓動時間被刪除了。

參數類型

從原生調用JS的狀況更簡單一些,參數是作爲JSON例的一個數組傳遞的。可是從JS到原生的調用裏,咱們須要原生的類型。可是,如上文所述,對於類的對象(結構體的對象),運行時並不能經過NSMethodSignature給咱們足夠的信息,咱們只有字符串類型。

咱們使用正則表達式從方法的簽名裏提取類型,而後咱們使用RCTConvert工具類來實際轉化參數的類型。這個工具類會把JSON裏的數據轉化成咱們須要的類型。

咱們使用objc_msgSend來動態調用方法。若是是struct的話,則使用NSInvocation來調用。

一旦咱們獲得了所有參數的類型,咱們使用另一個NSInvocation來調用目標模塊的方法,並傳入所有的參數。好比:

// If you had the following method in a given module, e.g. `MyModule`
RCT_EXPORT_METHOD(methodWithArray:(NSArray *) size:(CGRect)size) {}

// And called it from JS, like:
require('NativeModules').MyModule.method(['a', 1], {
  x: 0,
  y: 0,
  width: 200,
  height: 100
});

// The JS queue sent to native would then look like the following:
// ** Remember that it's a queue of calls, so all the fields are arrays **
@[
  @[ @0 ], // module IDs
  @[ @1 ], // method IDs
  @[       // arguments
    @[
      @[@"a", @1],
      @{ @"x": @0, @"y": @0, @"width": @200, @"height": @100 }
    ]
  ]
];

// This would convert into the following calls (pseudo code)
NSInvocation call
call[args][0] = GetModuleForId(@0)
call[args][1] = GetMethodForId(@1)
call[args][2] = obj_msgSend(RCTConvert, NSArray, @[@"a", @1])
call[args][3] = NSInvocation(RCTConvert, CGRect, @{ @"x": @0, ... })
call()

線程

默認狀況下,每個模塊都有本身的GCD queue。除非在模塊中經過-methodQueue方法指定模塊要運行的隊列。有一個例外是View Managers(就是繼承了RCTViewManager)的類,會默認運行在Shadow Queue裏。

目前的線程規則是這樣的:

  • -init-setBridge:保證會在main thread裏執行

  • 全部導出的方法都會在目標隊列裏執行

  • 若是你實現了RCTInvalidating協議,invalidate也會在目標隊列裏執行

  • -dealloc方法在哪一個線程執行被調用

當JS執行一堆的方法以後,這些方法會根據目標隊列分組,以後被並行分發:

// group `calls` by `queue` in `buckets`
for (id queue in buckets) {
  dispatch_block_t block = ^{
    NSOrderedSet *calls = [buckets objectForKey:queue];
    for (NSNumber *indexObj in calls) {
      // Actually call
    }
  };

  if (queue == RCTJSThread) {
    [_javaScriptExecutor executeBlockOnJavaScriptQueue:block];
  } else if (queue) {
    dispatch_async(queue, block);
  }
}

總結

本文還只是對橋接器如何工做的一個簡單描述。但願對各位能有所幫助。

原文:https://tadeuzagallo.com/blog...

相關文章
相關標籤/搜索