前兩部分內容簡單介紹一下ReactNative,後面的章節會把整個RN框架的iOS部分,進行程式碼層面的一一梳理
全文是不是有點太長了,我要不要分拆成幾篇文章
函式棧程式碼流程圖,由於採用層次縮排的形式,層次關係比較深的話,不是很利於手機閱讀,
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與na的程式碼模組
- 連結了哪個模組,哪個模組就能用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使用。
- BatchBridge會通過
- 他內部持有了一個
後面我會詳細按著程式碼執行的流程給大家細化OCCode裡面的程式碼,JSCode由於我對前端理解還不太深入,這個Blog就不會去拆解分析JS程式碼了
ReactNative通訊機制可以參考bang哥的部落格 React Native通訊機制詳解
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標記)
- RCTBridge-createBatchedBridge(BatchBridgeInit標記)
- RCTBridge-initWithBundleXXX
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的資訊,便於查詢和管理
1 2 3 4 5 6 7 8 |
//barchbridge的ivar NSMutableDictionaryNSString *, RCTModuleData *> *_moduleDataByName; NSArray *_moduleDataByID; NSArray *_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
1 2 3 4 |
//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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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側的程式碼我就不梳理了。
ReactNative JS call OC 程式碼分析
既然整個RCTRootView都初始化完畢,並且執行了jsbundle檔案了,整個RN就已經運作起來了,那麼RN運作起來後,JS的訊息通過JS程式碼的bridge傳送出來之後,是如何被OC程式碼識別,分發,最重轉向各個module模組的業務程式碼呢?我們接下來就會梳理,這個流程的程式碼
JS call OC 可以有很多個方法,但是所有的方法一定會走到同一個函式內,這個關鍵函式就是
- (void)handleBuffer:(id)buffer batchEnded:(BOOL)batchEnded
需要說明的事,handleBuffer一定發生在RCTJSExecutor的Thread內
正所謂順藤摸瓜,我可以順著他往上摸看看都哪裡會發起js2oc的通訊
- [RCTJSExecutor setUp]
可以看到這裡面有很多JavaScriptCore的JSContext[“xxx”]=block的用法,這個用法就是JS可以把xxx當做js裡面可以識別的function,object,來直接呼叫,從而呼叫到block得意思,可以看出來nativeFlushQueueImmediate
當js主動呼叫這個jsfunction的時候,就會下發一下資料,從而呼叫handleBuffer,可以確定的是,這個jsfunction,會在jsbunlde run起來後立刻執行一次
這個方法要特別強調一下,這是唯一個一個JS會主動呼叫OC的方法,其他的js呼叫OC,都他由OC實現傳給JS一個回撥,讓JS呼叫。
JS側主動呼叫nativeFlushQueueImmediate的邏輯
- [RCTBatchBridge enqueueApplicationScript:]
可以看到這句程式碼只發生在執行jsbundle之後,執行之後會[RCTJSExecutor flushedQueue:callback]
在callback裡呼叫handleBuffer,說明剛剛執行完jsbundle後會由OC主動發起一次flushedQueue,並且傳給js一個回撥,js通過這個回撥,會call oc,進入handleBuffer
- [RCTBatchBridge _actuallyInvokeCallback:]
- [RCTBatchBridge _actuallyInvokeAndProcessModule:]
兩個_actuallyInvoke開頭的方法,用處都是OC主動發起呼叫js的時候,會傳入一個call back block,js通過這個callback block回撥,這兩個方法最後都會執行[RCTJSExecutor _executeJSCall:]
從上面可以看出JS只有一個主動呼叫OC的方法,其他都是通過OC主動呼叫JS給予的回撥
我們還可以順著handleBuffer往下摸看看都會如何分發JS call OC的事件
以handleBuffer為根,我們繼續用函式站程式碼流程表來梳理
- RCTBatchedBridge-handlebuffer
- analyze Buffer(analyze buffer標記)
- find module(find modules標記)
- for 迴圈all calls
- dispatch async(dispatch async標記)
- [RCTBatchedBridge- handleRequestNumber:]
- [RCTBridgeMethod invokeWithBridge:](invocation標記 這個標記會複雜點,子流程表細說)
- [RCTBatchedBridge- handleRequestNumber:]
analyze buffer標記:js傳過來的buffer其實是一串calls的陣列,一次性發過來好幾個訊息,需要OC處理,所以會解析buffer,分別識別出每一個call的module資訊
1 2 3 |
NSArray *moduleIDs = [RCTConvert NSNumberArray:requestsArray[RCTBridgeFieldRequestModuleIDs]]; NSArray *methodIDs = [RCTConvert NSNumberArray:requestsArray[RCTBridgeFieldMethodIDs]]; NSArray *paramsArrays = [RCTConvert NSArrayArray:requestsArray[RCTBridgeFieldParams]]; |
find modules標記:解析了buffer之後就要查詢對應的module,不僅要找到RCTModuleData,同時還要取出RCTModuleData自己專屬的序列GCD queue
dispatch async標記:每一個module和queue都找到了就可以for迴圈了,去執行一段程式碼,尤其要注意,此處RN的處理是直接dispatch_async到系統隨機某一個空閒執行緒,因為有模組專屬queue的控制,還是可以保持不同模組內訊息順序的可控
invocation標記:這個標記的作用就是真真正正的去呼叫並且執行對應module模組的native程式碼了,也就是JS最終呼叫了OC,這個標記內部還比較複雜,裡面使用了NSInvocation去執行時查詢module類進行反射呼叫
invocation內部子流程如下
解釋一下,JS傳給OC是可以把JS的回撥當做引數一併傳過來的,所以後面的流程中會特別梳理一下這種回撥引數是如何實現的,
- [RCTBridgeMethod-processMethodSignature](invocation預處理標記)
- argumentBlocks(引數處理標記)
- 迴圈壓參(invocation壓參標記)
- 反射執行Invocation呼叫oc
invocation預處理標記:RN會提前把即將反射呼叫的selector進行分析,分析有幾個引數每個引數都是什麼型別,每種型別是否需要包裝或者轉化處理。
引數處理標記:argumentBlocks其實是包裝或轉化處理的block函式,每種引數都有自己專屬的block,根據型別進行不同的包裝轉化策略
此處別的引數處理不細說了,單說一下JS回撥的這種引數是怎麼操作的
- JS回撥通過bridge傳過來的其實是一個數字,是js回撥function的id
- 我們在開發module的時候,RN讓你宣告JS回撥的時候是宣告一個輸入引數為NSArray的block
- js回撥型引數的argumentBlocks的作用就是,把jsfunctionid進行記錄,包裝生成一個輸入引數為NSArray的block,這個block會自動的呼叫
[RCTBridge enqueueCallback:]
在需要的時候回撥JS,然後把這個block壓入引數,等待傳給module
這塊程式碼各種巨集巢狀,還真是挺繞的,因為巨集的形式,可讀性非常之差,但是讀完了後還是會覺得很風騷
[RCTBridgeMethod processMethodSignature]
這個方法,強烈推薦
invocation壓參標記:argumentBlocks可以理解為預處理專門準備的處理每個引數的函式,那麼預處理結束後,就該迴圈呼叫argumentBlocks把每一個引數處理一下,然後壓入invocation了
後面就會直接呼叫到你寫的業務模組的程式碼了,業務模組通過那個callback回撥也能直接calljs了
ReactNative OC call JS EventDispatcher程式碼分析
我們編寫module,純源生native模組的時候,有時候會有主動要call js的需求,而不是通過js給的callback calljs
這時候就需要RCTEventDispatcher
了,可以看到他的標頭檔案裡都是各種sendEvent,sendXXXX的封裝,看一下具體實現就會發現,無論是怎麼封裝,最後都走到了[RCTJSExecutor enqueueJSCall:]
,追中還是通過RCTJSExecutor,主動發起呼叫了JS
他有兩種方式
- 直接立刻傳送訊息主動callJS
- 把訊息add進一個Event佇列,然後通過flushEventsQueue一次性主動callJS
ReactNative Displaylink 程式碼分析
之前我們提到過一個RCTDisplayLink
,沒錯他被新增到RCTJSExecutor的Thread所在的runloop之上,以渲染頻率觸發執行程式碼,執行frameupDate
[RCTDisplaylink _jsThreadUpdate]
在這個方法裡,會拉取所有的需要執行frameUpdate的module,在module所在的佇列裡面dispatch_async執行didUpdateFrame方法
在各自模組的didUpdateFrame方法內,會有自己的業務邏輯,以DisplayLink的頻率,主動call js
比如:RCTTimer模組
RCTJSExecutor
最後在強調下JSBridge這個管道的執行緒控制的情況
剛才提到的無論是OC Call JS還是JS call OC,都只是在梳理程式碼流程,讓你清楚,所有JS/OC之間的通訊,都是通過RCTJSExecutor,都是在RCTJSExecutor內部所在的Thread裡面進行
如果發起呼叫方OC,並不是在JSThread執行,RCTJSExecutor就會把程式碼perform到JSThread去執行
發起呼叫方是JS的話,所有JS都是在JSThread執行,所以handleBuffer也是在JSThread執行,只是在最終分發給各個module的時候,才進行了async+queue的控制分發。