React Native剖析
React Native 現在是異常的火爆,我司最近也完成了一個 React Native 編寫的項目,現在已經提測審覈。大家關心的蘋果會不會拒絕RN ,會不會拒絕 CodePush ,我們會用實際行動告訴大家。
本文會介紹 React Native 的工作原理,讓移動開發者從代碼上了解框架。
React
React 是 facebook 出品一個前端框架,是目前最火的框架。以組件的形式組織項目結構,代替市面上的 MVC 框架。React 的出現解決了前端許多的痛點:
- 組件化、模塊化:React 天生組件化,將頁面拆分成各個組件,項目的開發效率顯著提升,而且極大的降低了項目維護成本。
- 開發效率:React 的頁面就是組件的組合,哪個組件出現問題就治哪裏,一貼下藥。再加上 ES6 語法使用,使得項目有超高的可讀性,容易理解。
- 運行效率: React實現了 Virtual DOM ,高效的算法帶來高效頁面渲染,使得性能更優越。
- 可維護性:React 的組件化配合 Redux 的單向數據流,使得問題定位清晰明顯。
- JSX: 一種語法糖,可以將 HTML 寫在 JS 文件裏,方便、簡單。
React 更像的是 MVC 中的 View 層,Model 和 Controller 的角色則由 Redux 代替( React 跟MVC 並無關係,僅僅是爲了方便理解),單向數據流使得業務邏輯清晰明瞭。所以現在的
React 項目的常用體系是 React + Redux + webpack + ES6 + react-router 。我們的 Reat Native 項目使用的是 React + Redux + ES6 。
React Native
簡單交代了 React 的背景,下面到了咱們的主角 —— React Native 。它可以看作是 React 的親兒子,把全身的本領都傳授了下去,兒子也挺爭氣,在 iOS 端跟 Android 端也有跨越性的突破。於是乎它就有了“跨平臺”、“Javascript編寫Native項目”史詩級的技能,他的表叔
Microsoft 對它也是疼愛有加,怕他挨 Native 欺負,給它做了一個叫CodePush 的裝備,隨時升級加修復,“熱更新”這個標籤又貼到了它的身上。現在的它是集萬千寵愛於一身,要風得風,要雨得雨。我們現在就來扒一扒,看它究竟是何方神聖。
原理概述
引用React Native 從入門到原理中的一段話。
首先要明白的一點是,即使使用了 React Native,我們依然需要 UIKit 等框架,調用的是 Objective-C 代碼。總之,JavaScript 只是輔助,它只是提供了配置信息和邏輯的處理結果。React Native 與 Hybrid 完全沒有關係,它只不過是以 JavaScript 的形式告訴 Objective-C 該執行什麼代碼。
直白的說,Javascript 在上層完成邏輯處理,然後通過 Javascript 引擎,實現 Javascript 與 Objective-C 交互,調用 Objective-C 中的原生UI組件,來實現頁面的渲染。
JavaScript 是一種單線程的語言,它不具備自運行的能力,因此總是被動調用。很多介紹 React Native 的文章都會提到 “JavaScript 線程” 的概念,實際上,它表示的是 Objective-C 創建了一個單獨的線程,這個線程只用於執行 JavaScript 代碼,而且 JavaScript 代碼只會在這個線程中執行。
React Native初始化
在 AppDelegate
的didFinishLaunchingWithOptions
方法中,我們找到了RN的入口。
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"demo"
initialProperties:nil
launchOptions:launchOptions];
首先創建了一個根控制器的View,然後將RN創建的view添加到窗口上顯示。這個方法分爲兩步。在initWithBundleURL:moduleProvider:launchOptions
這個方法內,React Native創建了一個實現 Objective-C 與 Javascript 交互的全局bridge
,後續的所有交互全部都是通過這個橋接實現的。
第二步initWithBridge:moduleName:initialProperties
方法中返回了剛纔的RCTRootView
。
第一步初始化的核心方法是setUp
。這個方法主要是創建了加載main.jsbundle
的地址和創建了BatchedBridge
。這個BatchedBridge
纔是真正的主角,它的主要作用就是讀取 Javascript 對 Objective-C 的方法調用,而且它的內部持有一個
JavascriptExecutor 對象,用來執行 Javascript 代碼。
- (void)setUp
{
...
[self createBatchedBridge];
[self.batchedBridge start];
...
}
RCTBatchedBridge
RCTBatchedBridge
中最重要的就是Star
t方法。該方法主要包含以下幾步:
- 讀取Javascript代碼
- 初始化需要暴露給js調用的Native模塊
- 異步初始化 JS executor
- 異步初始化模塊配置列表
- 將配置表傳入JS端
- 調用JS代碼
下面我們詳細的解析下每個步驟
異步加載 JSBundle 。
[self loadSource:^(NSError *error, NSData *source, __unused int64_t sourceLength) { if (error) { RCTLogWarn(@"Failed to load source: %@", error); dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf stopLoadingWithError:error]; }); } sourceCode = source; dispatch_group_leave(initModulesAndLoadSource); } onProgress:^(RCTLoadingProgress *progressData) { #ifdef RCT_DEV RCTDevLoadingView *loadingView = [weakSelf moduleForClass:[RCTDevLoadingView class]]; [loadingView updateProgress:progressData]; #endif }];
初始化Native模塊化信息。
// Synchronously initialize all native modules that cannot be loaded lazily [self initModulesWithDispatchGroup:initModulesAndLoadSource];
這個方法很複雜,咱們一步步來。
首先創建了2個數組跟一字典,分別存貯 module 的類、module 數據跟兩者組合的鍵值對,這樣就形成了三份配置表。
NSMutableArray<Class> *moduleClassesByID = [NSMutableArray new]; NSMutableArray<RCTModuleData *> *moduleDataByID = [NSMutableArray new]; NSMutableDictionary<NSString *, RCTModuleData *> *moduleDataByName = [NSMutableDictionary new];
遍歷需要暴露給 Javascript 的類(也就是下文的 module ),將所有的 module 加入配置表中。每一個 module 在實例化的時候都會開一個自己的隊列,保證每個模塊內部的通信都是串行執行。感興趣的同學們可以閱讀源碼,這個段寫的非常好,處理了很多像“死鎖”這樣的多線程操作。
for (Class moduleClass in RCTGetModuleClasses()) { NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); // Check for module name collisions RCTModuleData *moduleData = moduleDataByName[moduleName]; ... moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass bridge:self]; moduleDataByName[moduleName] = moduleData; [moduleClassesByID addObject:moduleClass]; [moduleDataByID addObject:moduleData]; }
RCTRegisterModule
每個 module 都使用了RCTRegisterModule
這個宏,在這個類的 load 方法的時候註冊了自己的 moduleName 到 RCTModuleClasses
這個數組中。這樣,RN 遍歷這個數組就能找到所有註冊的 module 了。
在RCTBridgeModule.h
這個文件中我們可以看到實現。
#define RCT_EXPORT_MODULE(js_name)
RCT_EXTERN void RCTRegisterModule(Class);
+ (NSString *)moduleName { return @#js_name; }
+ (void)load { RCTRegisterModule(self); }
void RCTRegisterModule(Class moduleClass)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTModuleClasses = [NSMutableArray new];
});
RCTAssert([moduleClass conformsToProtocol:@protocol(RCTBridgeModule)],
@"%@ does not conform to the RCTBridgeModule protocol",
moduleClass);
// Register module
[RCTModuleClasses addObject:moduleClass];
}
`moduleName`方法,返回 `@#js_name`。`@# `是把宏參數
js_name 轉爲字符串,若字符串爲空則返回的就是空。在 RCTBridgeModuleNameForClass()
獲取模塊名的方法裏,如果 moduleName 長度爲0,那麼就會調用 NSStringFromClass()
方法獲取類名。
Tip:@#是將傳入單字符參數名轉換成字符。傳送門--[define宏定義的#,##,@#及符號](http://blog.csdn.net/xdsoft365/article/details/5911596)。
初始化 JavaScript 代碼的執行器,即
RCTJSCExecutor
對象
上一步是module加入到配置表中,有一個非常特殊的類叫RCTJSCExecutor
,它需要創建一個實例並保存在一個RCTModuleData
的實例中並且被RCTBatchedBridge
持有。
if (!_javaScriptExecutor) {
id<RCTJavaScriptExecutor> executorModule = [self.executorClass new];
RCTModuleData *moduleData = [[RCTModuleData alloc] initWithModuleInstance:executorModule
bridge:self];
moduleDataByName[moduleData.name] = moduleData;
[moduleClassesByID addObject:self.executorClass];
[moduleDataByID addObject:moduleData];
// NOTE: _javaScriptExecutor is a weak reference
_javaScriptExecutor = executorModule;
}被持有後,初始化
RCTJSCExecutor
。// Asynchronously initialize the JS executor dispatch_group_async(setupJSExecutorAndModuleConfig, bridgeQueue, ^{ [performanceLogger markStartForTag:RCTPLJSCExecutorSetup]; [weakSelf setUpExecutor]; [performanceLogger markStopForTag:RCTPLJSCExecutorSetup]; });
前端的同學們肯定都知道js是單線程的,所有的js代碼都是在一個單獨的線程上調用的。
在RCTModuleClasses
的setUp
方法中,開了一條專門爲 Javascript
運行的巴拿馬運河。
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block
{
if ([NSThread currentThread] != _javaScriptThread) {
[self performSelector:@selector(executeBlockOnJavaScriptQueue:)
onThread:_javaScriptThread withObject:block waitUntilDone:NO];
} else {
block();
}
}原理中的原理來了就是大家嘴中常說的“RN就是JS調OC啊”。
[self executeBlockOnJavaScriptQueue:^{ ... self->_context = [[RCTJavaScriptContext alloc] initWithJSContext:context onThread:self->_javaScriptThread]; [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptContextCreatedNotification object:context]; ... __weak RCTJSCExecutor *weakSelf = self; context[@"nativeRequireModuleConfig"] = ^NSArray *(NSString *moduleName) { RCTJSCExecutor *strongSelf = weakSelf; if (!strongSelf.valid) { return nil; } ... };
在JSThread內創建了一個
JSContext
,並且爲Contextz設置了不同的block,如上圖。這些block會集中講一下。生成配置表
上一步已經獲得了module的數據,這個一步則將數據轉成json。- (NSString *)moduleConfig { NSMutableArray<NSArray *> *config = [NSMutableArray new]; for (RCTModuleData *moduleData in _moduleDataByID) { if (self.executorClass == [RCTJSCExecutor class]) { [config addObject:@[moduleData.name]]; } else { [config addObject:RCTNullIfNil(moduleData.config)]; } } return RCTJSONStringify(@{ @"remoteModuleConfig": config, }, NULL); }
將配置表傳入JS端
在生成Native配置表跟初始化RCTJSCExecutor
完成後,就要將配置表傳入JS端了,使兩端擁有同一份配置表。- (void)injectJSONConfiguration:(NSString *)configJSON onComplete:(void (^)(NSError *))onComplete { ... [_javaScriptExecutor injectJSONText:configJSON asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback:onComplete]; }
JS擁有了變量名爲
__fbBatchedBridgeConfig
的全局變量。在JS端獲得配置表後就開始執行js內部業務邏輯了。 JS 端如何使用這個配置我們下一篇會介紹。執行 js 代碼
在所有的準備工作都完成後,就開始通過executeSourceCode
執行
js 代碼了。dispatch_group_notify(initModulesAndLoadSource, bridgeQueue, ^{ RCTBatchedBridge *strongSelf = weakSelf; if (sourceCode && strongSelf.loading) { [strongSelf executeSourceCode:sourceCode]; } });
在executeSourceCode
這個方法裏,運行指定 url 的 js 代碼。dev 模式下運行的是你的 bundleURL 下的代碼,release 環境下運行的是你已經打包好的 jsbundle ,如果你使用了 codePush 對應的運行的是你 codePush 下發的 jsbundle。executeSourceCode
開啓了一個 NSRunLoop
不斷的打印引入 native 模塊中的 log 。
小結
本文一步步剖析了 ReactNative 這個偉大的框架,當然我這只是蜻蜓點水, ReactNative 還有許多值得我們學習的東西沒有指出來,希望大家都去看看源碼提高自己。後續,我會繼續寫文講解下ReactNative如何將一個View頁面展示出來和其他的一些小細節的東西。
PS
今天是2017-06-02,我們的APP已經過審,大家放心可用~