本文假設你已經有必定的React Native基礎,而且想要了解React Native的JS和原生代碼之間是如何交互的。javascript
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_MODULE
和RCT_EXPORT_METHOD
。看看他們是如何工做的。正則表達式
這個宏的功能就和它名字說的同樣,處處一個模塊。可是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); }
它的做用:
首先它聲明瞭RCTRegisterModule
爲extern
方法,也就是說這個方法的實如今編譯的時候不可知,而在link的時候纔可知。
聲明瞭一個方法moduleName
,這個方法返回可選的宏定義參數js_name
,通常是你但願有一個專門的模塊名稱,而不是默認的ObjC類名的時候使用。
最後,聲明瞭一個load
方法(當app被加載進內存的時候,load方法也會被調用)。在這個方法裏調用RCTRegisterModule
方法來讓RN的bridge感知到這個模塊。
這個宏更有意思,它並給你的模塊添加任何實際的方法。它建立了一個新的方法,這個新的方法基本上是這樣的:
+ (NSArray *)__rct_export__120 { return @[@"", @"log: (NSString *)message"]; }
這個被load
方法生成的方法的名稱由前綴(__rct_export__
)和一個可選的js_name
(如今是空的)和聲明的行號(好比12)和__COUNTER__
宏拼接在一塊兒組成。
這個新生成的方法的做用就是返回一個數組,這個數組包含一個可選的js_name
(在本例中是空的)和方法的簽名。簽名說的那一堆是爲了不方法崩潰。
即便是這麼複雜的生成算法,若是你使用了*category*的話也不免會有兩個方法的名稱是同樣的。不過這個機率很是低,而且也不會產生什麼不可控的行爲。雖然Xcode會這麼警告。
這一步只作一件事,那就是給React Native的橋接模塊提供信息。這樣它就能夠找到原生模塊裏export出來的所有信息:modules和methods,並且這些所有發生在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執行器有一個setUp
方法。用這個方法能夠執行不少耗費資源的任務,好比在後臺線程裏初始化JavaScriptCore
。因爲只有active的執行器才能夠接受到setUp
的調用,因此也節約了不少的資源。
JSGlobalContextRef ctx = JSGlobalContextCreate(NULL); _context = [[RCTJavaScriptContext alloc] initWithJSContext:ctx];
模塊的配置都是Json形式的,如:
{ "remoteModuleConfig": { "Logger": { "constants": { /* If we had exported constants... */ }, "moduleID": 1, "methods": { "requestPermissions": { "type": "remote", "methodID": 1 } } } } }
這些都做爲全局變量存儲在JavaScript VM裏,所以當橋接器的Js側代碼初始化完畢的時候它能夠用這些信息來建立原生模塊。
能夠得到代碼的地方只有兩個,在開發的時候從packager下載代碼,在產品環境下從磁盤加載代碼。
一旦全部的準備工做就緒,咱們就能夠把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);
這個時候,上例中的原生模塊就能夠在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); } }
本文還只是對橋接器如何工做的一個簡單描述。但願對各位能有所幫助。