ReactNative iOS源碼解析(一)

遷移老文章到掘金(基於RN 0.26)html

相關係列文章前端

本篇前兩部份內容簡單介紹一下ReactNative,後面的章節會把整個RN框架的iOS部分,進行代碼層面的一一梳理java

全文是否是有點太長了,我要不要分拆成幾篇文章react

函數棧代碼流程圖,因爲採用層次縮進的形式,層次關係比較深的話,不是很利於手機閱讀,git

ReactNative 概要

ReactNative,動態,跨平臺,熱更新,這幾個詞如今愈來愈火了,一句使用JavaScript寫源生App吸引力了無數人的眼球,而且誕生了這麼久也逐漸趨於穩定,攜程,天貓,QZone也都在大產品線的業務上,部分模塊採用這個方案上線,而且效果獲得了驗證(見2016 GMTC 資料PPT)github

咱們把這個單詞拆解成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的結構分爲四個部分,上面提到的,RN橋的模塊化可擴展性,就體如今JSBridge/OCBridge裏的ModuleConfig,只要遵循RN的協議RCTBridgeModule去寫的OC Module對象,使用RCT_EXPORT_MODULE()宏註冊類,使用RCT_EXPORT_METHOD()宏註冊方法,那麼這個OC Module以及他的OC Method都會被JS與OC的ModuleConfig進行統一控制

RN類圖

上面是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的信息,便於查找和管理

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

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信息

  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的回調當作參數一併傳過來的,因此後面的流程中會特別梳理一下這種回調參數是如何實現的,

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

相關文章
相關標籤/搜索