今天在逛論壇的時候發現這篇文章寫得特別好,所以轉帖來分享給大家,順便自己也可以學習學習。原文閱讀
ReactNative 概要
ReactNative,動態,跨平臺,熱更新,這幾個詞現在越來越火了,一句使用JavaScript寫源生App吸引力了無數人的眼球,并且誕生了這么久也逐漸趨于穩定,攜程,天貓,QZone也都在大產品線的業務上,部分模塊采用這個方案上線,并且效果得到了驗證(見2016 GMTC 資料PPT)
我們把這個單詞拆解成2部分
React
熟悉前端的朋友們可能都知道React.JS
這個前端框架,沒錯整個RN框架的JS代碼部分,就是React.JS
,所有這個框架的特點,完完全全都可以在RN里面使用(這里還融入了Flux,很好的把傳統的MVC重組為dispatch,store和components,Flux架構)所以說,寫RN哪不懂了,去翻React.JS的文檔或許都能給你解答以上由@彩虹 幫忙修正Native
顧名思義,純源生的native體驗,純源生的UI組件,純原生的觸摸響應,純源生的模塊功能
那么這兩個不相干的東西是如何關聯在一起的呢?
React.JS是一個前端框架,在瀏覽器內H5開發上被廣泛使用,他在渲染render()
這個環節,在經過各種flexbox布局算法之后,要在確定的位置去繪制這個界面元素的時候,需要通過瀏覽器去實現。他在響應觸摸touchEvent()
這個環節,依然是需要瀏覽器去捕獲用戶的觸摸行為,然后回調React.JS
上面提到的都是純網頁,純H5,但如果我們把render()
這個事情攔截下來,不走瀏覽器,而是走native會怎樣呢?
當React.JS已經計算完每個頁面元素的位置大小,本來要傳給瀏覽器,讓瀏覽器進行渲染,這時候我們不傳給瀏覽器了,而是通過一個JS/OC的橋梁,去通過[[UIView alloc]initWithFrame:frame]
的OC代碼,把這個界面元素渲染了,那我們就相當于用React.JS繪制出了一個native的View
拿我們剛剛繪制出得native的View,當他發生native源生的- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
觸摸事件的時候,通過一個OC/JS的橋梁,去調用React.JS里面寫好的點擊事件JS代碼
這樣React.JS還是那個React.JS,他的使用方法沒發生變化,但是卻獲得了純源生native的體驗,native的組件渲染,native的觸摸響應
于是,這個東西就叫做React-Native
ReactNative 結構
大家可以看到,剛才我說的核心就是一個橋梁,無論是JS=>OC,還是OC=>JS。
剛才舉得例子,就相當于把純源生的UI模塊,接入這個橋梁,從而讓源生UI與React.JS融為一體。
那我們把野心放長遠點,我們不止想讓React.JS操作UI,我還想用JS操作數據庫!無論是新玩意Realm,還是老玩意CoreData,FMDB,我都希望能用JS操作應該怎么辦?好辦,把純源生的DB代碼模塊,接入這個橋梁。
如果我想讓JS操作Socket做長連接呢?好辦,把源生socket代碼模塊接入這個橋梁。如果我想讓JS能操作支付寶,微信,蘋果IAP呢?好辦,把源生支付代碼模塊接入這個橋梁。
由此可見RN就是由一個bridge橋梁,連接起了JS與native的代碼模塊
- 鏈接了哪個模塊,哪個模塊就能用JS來操作,就能動態更新
- 發現現有RN框架有些功能做不到了?擴展寫個na代碼模塊,接入這個橋梁
這是一個極度模塊化可擴展的橋梁框架,不是說你從facebook的源上拉下來RN的代碼,RN的能力就固定一成不變了,他的模塊化可擴展,讓你缺啥補上啥就好了。
ReactNative 結構圖
大家可以看這個結構圖,整個RN的結構分為四個部分,上面提到的,RN橋的模塊化可擴展性,就體現在JSBridge/OCBridge里的ModuleConfig,只要遵循RN的協議
RCTBridgeModule
去寫的OC Module對象,使用RCT_EXPORT_MODULE()
宏注冊類,使用RCT_EXPORT_METHOD()
宏注冊方法,那么這個OC Module以及他的OC Method都會被JS與OC的ModuleConfig進行統一控制
上面是RN的代碼類結構圖
- 大家可以看到
RCTRootView
是RN的根試圖- 他內部持有了一個RCTBridge,但是這個RCTBridge并沒有太多的代碼,而是持有了另一個RCTBatchBridge對象,大部分的業務邏輯都轉發給BatchBridge,BatchBridge里面寫著的大量的核心代碼
- BatchBridge會通過RCTJavaScriptLoader來加載JSBundle,在加載完畢后,這個loader也沒什么太大的用了
- BatchBridge會持有一個RCTDisplayLink,這個對象主要用于一些Timer,Navigator的Module需要按著屏幕渲染頻率回調JS用的,只是給部分Module需求使用
- RCTModuleXX所有的RN的Module組件都是RCTModuleData,無論是RN的核心系統組件,還是擴展的UI組件,API組件
- RCTJSExecutor是一個很特殊的RCTModuleData,雖然他被當做組件module一起管理,統一注冊,但他是系統組件的核心之一,他負責單獨開一個線程,執行JS代碼,處理JS回調,是bridge的核心通道
- RCTEventDispatcher也是一個很特殊的RCTModuleData,雖然他被當做組件module一起管理,統一注冊,但是他負責的是各個業務模塊通過他主動發起調用js,比如UIModule,發生了點擊事件,是通過他主動回調JS的,他回調JS也是通過RCTJSExecutor來操作,他的作用是封裝了eventDispatcher得API來方便業務Module使用
- 他內部持有了一個RCTBridge,但是這個RCTBridge并沒有太多的代碼,而是持有了另一個RCTBatchBridge對象,大部分的業務邏輯都轉發給BatchBridge,BatchBridge里面寫著的大量的核心代碼
ReactNative 初始化代碼分析
我會按著函數調用棧類似的形式梳理出一個代碼流程表,對每一個調用環節進行簡單標記與作用說明,在整個表梳理完畢后,我會一一把每個標記進行詳細的源碼分析和解釋
下面的代碼流程表,如果有類名+方法的,你可以直接在RN源碼中定位到具體代碼段
- RCTRootView-initWithBundleURLXXX(RootInit標記)
- RCTBridge-initWithBundleXXX
- RCTBridge-createBatchedBridge(BatchBridgeInit標記)
- New Displaylink(DisplaylinkInit標記)
- New dispatchQueue (dispatchQueueInit標記)
- New dispatchGroup (dispatchGroupInit標記)
- group Enter(groupEnterLoadSource標記)
- RCTBatchedBridge-loadSource (loadJS標記)
- RCTBatchedBridge-initModulesWithDispatchGroup(InitModule標記 這塊內容非常多,有個子代碼流程表)
- group Enter(groupEnterJSConfig標記)
- RCTBatchedBridge-setUpExecutor(configJSExecutor標記)
- RCTBatchedBridge-moduleConfig(moduleConfig標記)
- RCTBatchedBridge-injectJSONConfiguration(moduleConfigInject標記)
- group Notify(groupDone標記)
- RCTBatchedBridge-executeSourceCode(evaluateJS標記)
- RCTDisplayLink-addToRunLoop(addrunloop標記)
RootInit標記:所有RN都是通過init方法創建的不再贅述,URL可以是網絡url,也可以是本地filepath轉成URL
BatchBridgeInit標記:前邊說過rootview會先持有一個RCTBridge,所有的module都是直接操作bridge所提供的接口,但是這個bridge基本上不干什么核心邏輯代碼,他內部持有了一個batchbrdige,各種調用都是直接轉發給RCTBatchBrdige來操作,因此batchbridge才是核心
RCTBridge在init的時候調用[self setUp]
RCTBridge在setUp的時候調用[self createBatchedBridge]
DisplaylinkInit標記:batchbridge會首先初始化一個RCTDisplayLink這個東西在業務邏輯上不會被所有的module調用,他的作用是以設備屏幕渲染的頻率觸發一個timer,判斷是否有個別module需要按著timer去回調js,如果沒有module,這個模塊其實就是空跑一個displaylink,注意,此時只是初始化,并沒有run這個displaylink
dispatchQueueInit標記:會初始化一個GCDqueue,后面很多操作都會被扔到這個隊列里,以保證順序執行
dispatchGroupInit標記:后面接下來進行的一些列操作,都會被添加到這個GCDgroup之中,那些被我做了group Enter標記的,當group內所有事情做完之后,會觸發group Notify
groupEnterLoadSource標記:會把無論是從網絡還是從本地,拉取jsbundle這個操作,放進GCDgroup之中,這樣只有這個操作進行完了(還有其他group內操作執行完了,才會執行notify的任務)
loadJS標記:其實就是異步去拉取jsbundle,無論是本地讀還是網絡啦,[RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onComplete:onSourceLoad];只有當回調完成之后會執行dispatch_group_leave,離開group
InitModule標記:這個函數是在主線程被執行的,但是剛才生成的GCD group會被當做參數傳進內部,因為內部的一些邏輯是需要加入group的,這個函數內部很復雜 我會繼續繪制一個代碼流程表
1)RCTGetModuleClasses()
一個C函數,RCT_EXPORT_MODULE()注冊宏會在+load時候把Module類都統一管理在一個static NSArray里,通過RCTGetModuleClasses()可以取出來所有的Module
2)RCTModuleData-initWithModuleClass
此處是一個for循環,循環剛才拿到的array,對每一個注冊了得module都循環生成RCTModuleData實例
3)配置moduleConfig
每一個module在循環生成結束后,bridge會統一存儲3分配置表,包含了所有的moduleConfig的信息,便于查找和管理
//barchbridge的ivar
NSMutableDictionary<NSString *, RCTModuleData *> *_moduleDataByName;
NSArray<RCTModuleData *> *_moduleDataByID;
NSArray<Class> *_moduleClassesByID;
// Store modules
_moduleDataByID = [moduleDataByID copy];
_moduleDataByName = [moduleDataByName copy];
_moduleClassesByID = [moduleClassesByID copy];
4)RCTModuleData-instance
這是一個for循環,每一個RCTModuleData都需要循環instance一下,需要說明的是,RCTModuleData與Module不是一個東西,各類Module繼承自NSObject,RCTModuleData內部持有的instance實例才是各類Module,因此這個環節是初始化RCTModuleData真正各類Module實例的環節
通過RCTModuleData-setUpInstanceAndBridge來初始化創建真正的Module
//SOME CODE
_instance = [_moduleClass new];
//SOME CODE
[self setUpMethodQueue];
這里需要說明,每一個Module都會創建一個自己獨有的專屬的串行GCD queue,每次js拋出來的各個module的通信,都是dispatch_async,不一定從哪個線程拋出來,但可以保證每個module內的通信事件是串行順序的
每一個module都有個bridge屬性指向,rootview的bridge,方便快速調用
5)RCTJSCExecutor
RCTJSCExecutor是一個特殊的module,是核心,所以這里會單獨處理,生成,初始化,并且被bridge持有,方便直接調用
RCTJSCExecutor初始化做了很多事情,需要大家仔細關注一下
創建了一個全新的NSThread,并且被持有住,綁定了一個runloop,保證這個線程不會消失,一直在loop,所有與JS的通信,一定都通過RCTJSCExecutor來進行,所以一定是在這個NSThread線程內,只不過各個模塊的消息,會進行二次分發,不一定在此線程內
6)RCTModuleData-gatherConstants
每一個module都有自己的提供給js的接口配置表,這個方法就是讀取這個配置表,注意!這行代碼執行在主線程,但他使用dispatch_async 到mainQueue上,說明他先放過了之前的函數調用棧,等之前的函數調用棧走完,然后還是在主線程執行這個循環的gatherConstants,因此之前傳進來的GCD group派上了用場,因為只有當所有module配置都讀取并配置完畢后才可以進行 run js代碼
下面思路從子代碼流程表跳出,回到大代碼流程表的標記
groupEnterJSConfig標記:代碼到了這塊會用到剛才創建,但一直沒使用的GCD queue,并且這塊還比較復雜,在這次enter group內部,又創建了一個子group,都放在這個GCD queue里執行
如果覺得繞可以這么理解他會在專屬的隊列里執行2件事情(后面要說的2各標記),當這2個事情執行完后觸發子group notify,執行第三件事情(后面要說的第三個標記),當第三個事情執行完后leave母group,觸發母group notify
dispatch_group_enter(initModulesAndLoadSource);
dispatch_async(bridgeQueue, ^{
dispatch_group_t setupJSExecutorAndModuleConfig = dispatch_group_create();
// Asynchronously initialize the JS executor
dispatch_group_async(setupJSExecutorAndModuleConfig, bridgeQueue, ^{
RCTPerformanceLoggerStart(RCTPLJSCExecutorSetup);
[weakSelf setUpExecutor];
RCTPerformanceLoggerEnd(RCTPLJSCExecutorSetup);
});
// Asynchronously gather the module config
dispatch_group_async(setupJSExecutorAndModuleConfig, bridgeQueue, ^{
if (weakSelf.valid) {
RCTPerformanceLoggerStart(RCTPLNativeModulePrepareConfig);
config = [weakSelf moduleConfig];
RCTPerformanceLoggerEnd(RCTPLNativeModulePrepareConfig);
}
});
dispatch_group_notify(setupJSExecutorAndModuleConfig, bridgeQueue, ^{
// We're not waiting for this to complete to leave dispatch group, since
// injectJSONConfiguration and executeSourceCode will schedule operations
// on the same queue anyway.
RCTPerformanceLoggerStart(RCTPLNativeModuleInjectConfig);
[weakSelf injectJSONConfiguration:config onComplete:^(NSError *error) {
RCTPerformanceLoggerEnd(RCTPLNativeModuleInjectConfig);
if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf stopLoadingWithError:error];
});
}
}];
dispatch_group_leave(initModulesAndLoadSource);
});
});
configJSExecutor標記:再次專門處理一些JSExecutor這個RCTModuleData
1)property context懶加載,創建了一個JSContext
2)為JSContext設置了一大堆基礎block回調,都是一些RN底層的回調方法
moduleConfig標記:把剛才所有配置moduleConfig信息匯總成一個string,包括moduleID,moduleName,moduleExport接口等等
moduleConfigInject標記:把剛才的moduleConfig配置信息string,通過RCTJSExecutor,在他內部的專屬Thread內,注入到JS環境JSContext里,完成了配置表傳給JS環境的工作
groupDone標記:GCD group內所有的工作都已完成,loadjs完畢,配置module完畢,配置JSExecutor完畢,可以放心的執行JS代碼了
evaluateJS標記:通過[_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:]來在JSExecutor專屬的Thread內執行jsbundle代碼
addrunloop標記:最早創建的RCTDisplayLink一直都只是創建完畢,但并沒有運作,此時把這個displaylink綁在JSExecutor的Thread所在的runloop上,這樣displaylink開始運作
小結:
整個RN在bridge上面,單說OC側,各種GCD,線程,隊列,displaylink,還是挺復雜的,針對各個module也都是有不同的處理,把這塊梳理清楚能讓我們更加清楚OC代碼里面,RN的線程控制,更方便以后我們擴展編寫更復雜的module模塊,處理更多native的線程工作。
后面的 js call oc oc call js 我也會以同樣的方式進行梳理,讓大家清楚線程上是如何運作的
PS:JS代碼側其實bridge的設計也有一套,包括所有call oc messageQueue會有個隊列控制之類的,我對JS不是那么熟悉和理解,JS側的代碼我就不梳理了。