遷移老文章到掘金(基於RN 0.26)html
相關係列文章前端
本篇前兩部份內容簡單介紹一下ReactNative,後面的章節會把整個RN框架的iOS部分,進行代碼層面的一一梳理java
全文是否是有點太長了,我要不要分拆成幾篇文章react
函數棧代碼流程圖,因爲採用層次縮進的形式,層次關係比較深的話,不是很利於手機閱讀,git
ReactNative,動態,跨平臺,熱更新,這幾個詞如今愈來愈火了,一句使用JavaScript寫源生App
吸引力了無數人的眼球,而且誕生了這麼久也逐漸趨於穩定,攜程
,天貓
,QZone
也都在大產品線的業務上,部分模塊採用這個方案上線,而且效果獲得了驗證(見2016 GMTC 資料PPT)github
咱們把這個單詞拆解成2部分算法
熟悉前端的朋友們可能都知道React.JS
這個前端框架,沒錯整個RN框架的JS代碼部分,就是React.JS,全部這個框架的特色,完徹底全均可以在RN裏面使用(這裏還融入了Flux,很好的把傳統的MVC重組爲dispatch,store和components,Flux架構)數據庫
因此說,寫RN哪不懂了,去翻React.JS的文檔或許都能給你解答數組
以上由@彩虹 幫忙修正瀏覽器
顧名思義,純源生的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
你們能夠看到,剛纔我說的核心就是一個橋樑,不管是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的代碼模塊
這是一個極度模塊化可擴展的橋樑框架,不是說你從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使用。後面我會詳細按着代碼執行的流程給你們細化OCCode裏面的代碼,JSCode因爲我對前端理解還不太深刻,這個Blog就不會去拆解分析JS代碼了
ReactNative通訊機制能夠參考bang哥的博客 React Native通訊機制詳解
我會按着函數調用棧相似的形式梳理出一個代碼流程表,對每個調用環節進行簡單標記與做用說明,在整個表梳理完畢後,我會一一把每一個標記進行詳細的源碼分析和解釋
下面的代碼流程表,若是有類名+方法的,你能夠直接在RN源碼中定位到具體代碼段
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的,這個函數內部很複雜 我會繼續繪製一個代碼流程表
一個C函數,RCT_EXPORT_MODULE()註冊宏會在+load
時候把Module類都統一管理在一個static NSArray裏,經過RCTGetModuleClasses()能夠取出來全部的Module
此處是一個for循環,循環剛纔拿到的array,對每個註冊了得module都循環生成RCTModuleData實例
每個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];
複製代碼
這是一個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,方便快速調用
RCTJSCExecutor是一個特殊的module,是核心,因此這裏會單獨處理,生成,初始化,而且被bridge持有,方便直接調用
RCTJSCExecutor初始化
作了不少事情,須要你們仔細關注一下
建立了一個全新的NSThread,而且被持有住,綁定了一個runloop,保證這個線程不會消失,一直在loop,全部與JS的通訊,必定都經過RCTJSCExecutor來進行,因此必定是在這個NSThread線程內,只不過各個模塊的消息,會進行二次分發,不必定在此線程內
每個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側的代碼我就不梳理了。
既然整個RCTRootView都初始化完畢,而且執行了jsbundle文件了,整個RN就已經運做起來了,那麼RN運做起來後,JS的消息經過JS代碼的bridge發送出來以後,是如何被OC代碼識別,分發,最重轉向各個module模塊的業務代碼呢?咱們接下來就會梳理,這個流程的代碼
JS call OC 能夠有不少個方法,可是全部的方法必定會走到同一個函數內,這個關鍵函數就是
- (void)handleBuffer:(id)buffer batchEnded:(BOOL)batchEnded
須要說明的事,handleBuffer必定發生在RCTJSExecutor的Thread內
正所謂順藤摸瓜,我能夠順着他往上摸看看都哪裏會發起js2oc的通訊
能夠看到這裏面有不少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的邏輯
能夠看到這句代碼只發生在執行jsbundle以後,執行以後會[RCTJSExecutor flushedQueue:callback]
在callback裏調用handleBuffer,說明剛剛執行完jsbundle後會由OC主動發起一次flushedQueue,而且傳給js一個回調,js經過這個回調,會call oc,進入handleBuffer
兩個_actuallyInvoke開頭的方法,用處都是OC主動發起調用js的時候,會傳入一個call back block,js經過這個callback block回調,這兩個方法最後都會執行[RCTJSExecutor _executeJSCall:]
從上面能夠看出JS只有一個主動調用OC的方法,其餘都是經過OC主動調用JS給予的回調
咱們還能夠順着handleBuffer往下摸看看都會如何分發JS call OC的事件
以handleBuffer爲根,咱們繼續用函數站代碼流程表來梳理
analyze buffer標記:js傳過來的buffer實際上是一串calls的數組,一次性發過來好幾個消息,須要OC處理,因此會解析buffer,分別識別出每個call的module信息
NSArray<NSNumber *> *moduleIDs = [RCTConvert NSNumberArray:requestsArray[RCTBridgeFieldRequestModuleIDs]];
NSArray<NSNumber *> *methodIDs = [RCTConvert NSNumberArray:requestsArray[RCTBridgeFieldMethodIDs]];
NSArray<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的回調當作參數一併傳過來的,因此後面的流程中會特別梳理一下這種回調參數是如何實現的,
invocation預處理標記:RN會提早把即將反射調用的selector進行分析,分析有幾個參數每一個參數都是什麼類型,每種類型是否須要包裝或者轉化處理。
參數處理標記:argumentBlocks實際上是包裝或轉化處理的block函數,每種參數都有本身專屬的block,根據類型進行不一樣的包裝轉化策略
此處別的參數處理不細說了,單說一下JS回調的這種參數是怎麼操做的
[RCTBridge enqueueCallback:]
在須要的時候回調JS,而後把這個block壓入參數,等待傳給module這塊代碼各類宏嵌套,還真是挺繞的,由於宏的形式,可讀性很是之差,可是讀完了後仍是會以爲很風騷
[RCTBridgeMethod processMethodSignature]
這個方法,強烈推薦
invocation壓參標記:argumentBlocks能夠理解爲預處理專門準備的處理每一個參數的函數,那麼預處理結束後,就該循環調用argumentBlocks把每個參數處理一下,而後壓入invocation了
後面就會直接調用到你寫的業務模塊的代碼了,業務模塊經過那個callback回調也能直接calljs了
咱們編寫module,純源生native模塊的時候,有時候會有主動要call js的需求,而不是經過js給的callback calljs
這時候就須要RCTEventDispatcher
了,能夠看到他的頭文件裏都是各類sendEvent,sendXXXX的封裝,看一下具體實現就會發現,不管是怎麼封裝,最後都走到了[RCTJSExecutor enqueueJSCall:]
,追中仍是經過RCTJSExecutor,主動發起調用了JS
他有兩種方式
以前咱們提到過一個RCTDisplayLink
,沒錯他被添加到RCTJSExecutor的Thread所在的runloop之上,以渲染頻率觸發執行代碼,執行frameupDate
[RCTDisplaylink _jsThreadUpdate]
在這個方法裏,會拉取全部的須要執行frameUpdate的module,在module所在的隊列裏面dispatch_async執行didUpdateFrame方法
在各自模塊的didUpdateFrame方法內,會有本身的業務邏輯,以DisplayLink的頻率,主動call js
好比:RCTTimer模塊
最後在強調下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的控制分發。