ReactNative iOS原始碼解析(一)

發表於2016-07-02

前兩部分內容簡單介紹一下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 結構圖

reactnativejiegou

大家可以看這個結構圖,整個RN的結構分為四個部分,上面提到的,RN橋的模組化可擴充套件性,就體現在JSBridge/OCBridge裡的ModuleConfig,只要遵循RN的協議RCTBridgeModule去寫的OC Module物件,使用RCT_EXPORT_MODULE()巨集註冊類,使用RCT_EXPORT_METHOD()巨集註冊方法,那麼這個OC Module以及他的OC Method都會被JS與OC的ModuleConfig進行統一控制

RCTRoot

上面是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通訊機制詳解

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的資訊,便於查詢和管理

  • 4)RCTModuleData-instance

這是一個for迴圈,每一個RCTModuleData都需要迴圈instance一下,需要說明的是,RCTModuleData與Module不是一個東西,各類Module繼承自NSObject,RCTModuleData內部持有的instance例項才是各類Module,因此這個環節是初始化RCTModuleData真正各類Module例項的環節

通過RCTModuleData-setUpInstanceAndBridge來初始化建立真正的Module

這裡需要說明,每一個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

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標記 這個標記會複雜點,子流程表細說)

analyze buffer標記:js傳過來的buffer其實是一串calls的陣列,一次性發過來好幾個訊息,需要OC處理,所以會解析buffer,分別識別出每一個call的module資訊

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的控制分發。

相關文章