ReactNative 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與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使用

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側的代碼我就不梳理了。


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容