React 和 ReactNative 的渲染機制/ ReactNative 與原生之間的通訊 / 如何自定義封裝原生組件/RN中的多線程

RN 與native 的交互javascript

0>>> React 的渲染機制php

1>>> react-native 渲染原理 前端

2>>> react-native 如何與原生通訊java

3>>> 如何封裝一個原生視圖組件node

4>>> react-native 的線程管理react

 RN的本質是利用 js 調用 native 端的組件, 從而實現相應的功能android

// react ios

React React native 的原理是相同的,都是由 javascript 實現的虛擬DOM 來驅動界面 View 的渲染,只不過 React.js 驅動 HTML DOM 的渲染, RN 是驅動 ios/android 原生組件的渲染git

 

都知道 React  native 是虛擬 DOM 機制,是存在於內存中的 JavaScript 對象對應着真實的 DOM 節點,操做DOM 很慢,但操做JavaScript 很快,因爲採用diff算法只渲染相比以前改變的部分並非所有渲染,因此更快,  setState 時至關於建立了一個影子 DOM 而後比較新舊兩個 DOM 的區別觸發 render 函數,從新渲染須要改變部分的DOM節點github

React 是 fb 推出的一個 js前端框架, 採用組件化方式簡化了 web 開發

(1)DOM : 每一個 HTML  頁面能夠當作一個 DOM  能夠把 HTML標籤和 js 單獨封裝到一個組件類中,便於複用,方便開發

(2)更高效 原生的 web 刷新 DOM, 須要刷新整個頁面 react 只刷新部分頁面

(3) 首創了 Virtual DOM 機制, 是一個存在於內存中的 js對象,之因此很是快是由於他從不直接操做 DOM, 它和 DOM 是一一對應的關係, 當頁面發生變化時, react 會利用 DOM diff 算法,  把有變化的 DOM 進行刷新

由當前狀態決定 UI 界面 狀態發生變化時  由 diff算法決定刷新哪些 DOM 節點,  虛擬DOM是在DOM的基礎上創建了一個抽象層,對數據和狀態所作的任何改動,都會被自動且高效的同步到虛擬DOM,最後再批量同步到DOM中。

在React中,render執行的結果獲得的並非真正的DOM節點,而僅僅是JavaScript對象,稱之爲虛擬DOM

逐層進行節點比較 若是上面的同級節點不一樣,該節點及其子節點會被徹底刪除掉,不會進行進一步的遞歸比較,這樣只須要對 DOM 樹進行一次遍歷 ,便能完成整個DOM 樹的比較

 

 

DOM 結構的轉換

 

 

好比這種正常的邏輯: A.parent.remove(A)  D.append(A)

對於 React 只考慮同層的節點的位置變換 不一樣層的只有簡單的建立和刪除, 當發現節點中 A 不見了 ,直接銷燬 A 新建一個 A 節點做爲 D 的子節點

A.destroy()  A = new A()   A.append(new B())  A.append(new C())  D.append(A)

 

同級的列表節點的比較

 

如圖: 在 JQuery 中 $(B).after(F)直接插入一個節點就行了

在 React 只能告訴新的頁面是 ABFCDE組成.由 diff算法完成更新 他只能把 A 更新爲 A B 更新爲 B C 更新爲 F 這樣

若是給每一個節點一個惟一標識的 key, React 就能夠找到正確的位置去插入新的節點,這也是爲何項目中有些列表常常報警告缺乏 key, 也是潛在的性能問題, 雖然不影響運行

 

 

對於相同類型的元素, React 會先查看二者的屬性差別, 而後保留相同的底層 DOM 節點,僅僅去更新那些被修改的屬性好比只修改了 width color 等 處理完 根DOM節點後, 會根據上面的判斷邏輯對子節點進行遞歸掃描

須要注意的兩點:

1> 該算法不會去嘗試匹配那些不一樣組件類型的子元素樹。 若是你看到本身在返回類似輸出結果的兩個組件類型之間來來回回,你可能須要把它們改成相同的類型組件。

2> key屬性應該是穩定,可預測和惟一的。 不穩定的鍵(如使用Math.random()生的key)將致使許多組件實例和DOM節點進行沒必要要地重複建立,這可能致使子組件中的性能下降和state丟失。

 

React.render 渲染流程

 

1>>> React.render() 將咱們的 element 根虛擬節點渲染到 container 元素中

2>>> 根據 element 的類型不一樣, 分別實例化 ReactDOMTextComponnet ReactDOMComponent  ReactCompositeComponent類, 這些類用來管理    ReactElement ,負責將不一樣的 ReactElement 轉化爲 DOM, 並更新 DOM

3>>> ReactCompositeComponent 實例調用 mountComponent 方法後內部調用render 方法, 返回了 DOM Elements

 

 

 React 的更新機制

 

 

 

React Element 一個描述 DOM 節點或 component 實例的字面級對象, 它包含一些信息,組件的 type 和 props, 就像一個描述 DOM 節點的元素(虛擬節點), 能夠經過 React.creatElement 方法建立

 

 reactClass 平時咱們所建立的 component 組件(類或函數) ReactClass 實例化後調用 render 函數可返回 DOMElement

調用 React.render 方法, 將 element根虛擬節點渲染到 container 元素中, 根據 element 的類型不一樣,分別實例化 ReactDOMTextComponent ReactDOMComponent  ReactCompositeComponent 類, 這些類用來管理 ReactElement 負責將不一樣的 ReactElement 轉化爲 DOM 並更新 DOM  , ReactCompositeComponent 實例調用 mountComponent 方法後內部調用 render 方法, 返回 DOMElement

 

總結: (1)節點之間的比較 節點 node .包括兩種類型 一個是 React 組件 一個是 HTML 的 DOM

節點類型不一樣: 直接使用新的替換舊的

類型相同: 樣式不一樣,修改樣式便可,在 React 裏樣式並非純粹的字符串也是一個對象,修改完當前節點以後,遞歸處理子節點

組件類型相同: 使用 React 機制處理,通常使用新的 props 替換舊的 props, 並在以後調用組件的 componentWill /DidRecieveProps 方法, 以前的組件的 render 方法會被調用\

(2)列表之間的比較

一列節點中有一個發生變化, React 並無什麼好的辦法處理,循環兩個列表,找出不一樣是惟一的處理方法

可是,下降這個算法的難度的辦法是在生成一個列表時給每個節點一個惟一的 key,就能夠方便的找出哪一個節點發生變化

 

// React component的生命週期函數

 

裝載組件 

  • componentWillMount:這個方法被調用時期是組件將要被加載在視圖上以前,功能比較少,即:render一個組件前最後一次修改state的機會。
  • componentDidMount:即調用了render方法後,組件加載成功並被成功渲染出來之後所執行的hook函數,通常會將網絡請求等加載數據的操做,放在這個函數裏進行,來保證不會出現UI上的錯誤.

 

更新組件狀態 

存在期主要是用來處理與用戶的交互,如:點擊事件,都比較簡單,也不是很經常使用,具體有如下幾個過程: 

  • componentWillReceiveProps:指父元素對組件的props或state進行了修改。
  • shouldComponentUpdate:通常用於優化,能夠返回false或true來控制是否進行渲染
  • componentWillUpdate:組件刷新前調用,相似componentWillMount
  • componentDidUpdate:更新後的hook

 

卸載(刪除)組件 銷燬期,用於清理一些無用的內容,如:點擊事件Listener 定時器的移除

  • componentWillUnmount

上面函數的調用順序是:

 建立時 

  • getDefaultProps  // 在組件內 能夠 static defultProps={ name:'chris'} 組件外 MyComponent.defaultProps={name:'chris'}
  • getInitialState    // ES6 constructor(){this.state={}}
  • componentWillMount 
  • render 

 

 更新時

  • componentDidMount
  • shouldComponentUpdate 
  • componentWillUpdate 
  • render 
  • componentDidUpdate

 

//react-native 是如何作到js oc 交互的 JavascriptCore

React native 會在一開始生成兩個模塊配置表, js按照整個表就能夠找到須要調用的方法

iOS 是經過JavaScriptCore 與 oc交互

react naive 啓動流程

React -native 可以在 iOS 和安卓設備上運行起來, 是由於 rn 與 native 之間有一種交互. javascriptCore 引擎,  js告訴 oc須要執行什麼, iOS 會本身去調用 UIKit 等框架繪製界面

 

1 建立 RCTRootView

2 建立 RCTBridge 橋接對象 管理 js與 oc的交互

3 建立 RCTBatchedBridge 批量橋接對象

4 執行 [RCTBatchedBridge loadSource] 加載 js 源碼

5  執行 [RCTBatchedBridge initModulesWithDispatchGroup] 建立模塊配置表

6 執行 [RCTJSExcutor injectJSONText]  往 js 中插入 OC 模塊表

7 執行完 js代碼 回調 OC 調用 OC 中的組件

8 完成渲染

RCTBridge 是負責雙方通訊的橋樑, 真正起做用的是RCTBatchedBridge類, 能夠看一下他都作了什麼?

當建立完模塊的實例對象以後,會將該實例保存到一個RCTModuleData對象中,RCTModuleData裏包含模塊的類名,名稱,方法列表,實例對象、該模塊代碼執行的隊列以及配置信息等,js層就是根據這個對象來查詢該模塊的相關信息。

 

// react-native 加載 js源碼流程

 

// react-native UI控件渲染流程

用戶能看到的一切內容都源於 RootView, 實際上在建立 RootView 以前, RN 先建立了一個 Bridge 對象. 他是 OC與 JS 交互的橋樑, 整個 RN 的初始化過程其實也就是在建立整個橋樑對象

初始化方法的核心是 setUp 方法, setUp 方法主要任務是建立BatchedBridge, BatchedBridge的做用是批量讀取 js對 OC 的方法調用,建立BatchedBridge的關鍵是 start 方法, 分爲5個步驟:

1>> 讀取 js源碼

2>> 初始化模塊信息

3>> 初始化 JS 代碼執行器 RCTJSCExecutor 對象

4>> 生產模塊列表並寫入 JS 端

5>> 執行 js 源碼 調用 OC

 

在調用 OC 以前,  JS 會解析出方法的 MoudleID  MethodID Argument 並放入 MessageQueue. 等待 OC 拿走,或者超時後主動發送給 OC

OC負責調用的方法是 handleBuffer, 他的參數是一個含有四個元素的數組, 每一個元素也是一個數組,分別存放 moudleID methodID  params , 函數內部在每一次調用方法的時候調用_ handleRequestNumber: MoudleID: MethodID: params: 方法, 經過查找模塊配置表找出要調用的方法, 並經過 runtime 動態調用 [method invokeWithBridge:self moudle:moudleDate.instance arguments:params]  processMethodSignature ,他會根據 js 的 callbackID 建立一個 block, 並在調用完函數以後執行這個 block

 

 

 

1. [RCTRootView runApplication]  通知 js 運行 APP

2. [RCTBatchedBridge _processResponse:json error:error] 處理執行完js 代碼返回的響應 , 包含須要添加多少個子控件的信息

3 . [RCTBatchedBridge batchDidComplete] 批量橋接對象調用批量處理完成方法

4 . [RCTUIManager batchDidComplete]  RCTUIManager 調用批量處理完成的方法,就會開始去加載 rootView 的子控件

5 . [RCTUIManager creatView: viewName :rootTags: props:] 經過 js 執行 OC 代碼, 讓 UIManager 建立子控件 View

6 .[RCTUIManager _layoutAndMount] 佈局子組件

7 .[RCTUIManager setChildren: reactTags: ] 給 RCTRootView 對應的 RCTRootShadowView 設置子組件

8 .[RCTRootShadowView insertReactSubView:view atIndex:index++] 遍歷子組件數組, 給 RCTRootShadowView 插入全部子控件

9.[RCTShadowView processUpdatedProperties:parentProperties] 處理保存在 RCTShadowView中的屬性, 就會去佈局 RCTShadowView 對應 UIView的全部子控件

10.[RCTView didUpdateReactSubviews] 給原生 View 添加子控件 完成 UI 渲染

 

render 函數的翻譯

ReactElement.createElement = function (type, config, children){ ... }

 

 

UIManager 經過調用 createView 方法建立原生的 UIView

UIManager 經過 dispatchViewManagerCommand 來實現把原生UI 的方法給 JS 響應

 

UIManager 是一個 native moudle 

  • createView(int tag, String className, int rootViewTag, ReadableMap props)
  • 建立View
  • updateView(int tag, String className, ReadableMap props)
  • 更新View
  • manageChildren(int viewTag, Array moveFrom, Array moveTo, Array addChildTags, Array addAtIndices, Array removeFrom)
  • 批量添加/刪除/移動一個view下面的view
  • measure(int reactTag, Callback callback)
  • 測量View的位置、size等,結果異步回調
  • measureInWindow(int reactTag, Callback callback)
  • 測量View相對屏幕的位置、size等,結果異步回調
  • dispatchViewManagerCommand(int reactTag, int commandId, ReadableArray commandArgs)
  • 派發View命令,也就是用來調用對應View的方法

這個模塊是NativeModule方式定義的,在RN的JS端啓動時,端上會經過JSC把收集到的模塊信息(名稱)打包到JS端全局變量global.__fbBatchedBridgeConfig中,並採用延遲加載策略:設置NativeModules.[模塊名]的getter,延遲經過JSC讀取模塊詳細信息(方法、命令號等信息)。在調用的時候會放到MessageQueue的隊列裏,批量提交,兩次批量提交限制的最小間隔爲5ms。

 

RN和原生同樣,也是先渲染內部子控件,而後再渲染外部控件。因此Component來自React的,可是UI控件是React-Native的,在render生命週期執行的時候會執行子控件的render方法,子控件會調用UIManager來把信息傳遞到原始的UIManagerModule,UIManagerModule根據傳過來的Tag找到對應的UIManager,最後生成一個Operation添加到UI處理隊列中,當mDispatchUIRunnables執行runable的時候調用Operation.execute抽象方法,其實就是調用UIManager.createViewInstance來真正生成View,而後調用viewManager.updateProperties 設置View的屬性。這樣一個控件就建立出來了

 

// react- native 的事件處理流程

 

  • 1.在建立RCTRootContentView的時候,內部會建立RCTTouchHandler

          RCTTouchHandler:繼承UIGestureRecognizer,也就是它就是一個手勢

          它會做爲RCTRootContentView的手勢,這樣點擊RCTRootContentView,就會觸發RCTTouchHandler

          RCTTouchHandler:內部實現了touchBegin等觸摸方法,用來處理觸摸事件

  • 2.在建立RCTTouchHandler的時候,內部會建立RCTEventDispatcher

          RCTEventDispatcher:用來把事件處理傳遞給JS的方法處理,也就是當UI界面產生事件,就會執行JS的代碼處理。

  • 3.經過RCTRootContentView截獲點擊事件

          產生事件就會去觸犯RCTRootContentView中的RCTTouchHandler對象。

  • 4.當產生事件的時候,會執行[RCTTouchHandler touchBegin]

  • 5.RCTTouchHandler的touch方法,會執行[RCTTouchHandler _updateAndDispatchTouches:eventName:]

          內部會建立RCTTouchEvent事件對象

  • 6.[RCTEventDispatcher sendEvent:event] -> 讓事件分發對象調用發送事件對象

          內部會把事件保存到_eventQueue(事件隊列中)

  • 7.[RCTEventDispatcher flushEventsQueue] -> 讓事件分發對象沖刷事件隊列,就是獲取事件隊列中全部事件執行

  • 8.[RCTEventDispatcher dispatchEvent:event] -> 遍歷事件隊列,一個一個分發事件

          分發事件的本質:就是去執行JS的代碼,相應事件。

  • 9.[RCTBatchedBridge enqueueJSCall:[[event class] moduleDotMethod] args:[event arguments]]; -> 讓橋架對象調用JS處理事件

          本質:就是產生事件調用JS代碼

  • 10.這樣就能完成把UI事件交給JS代碼響應

 

RN與原生(iOS)之間的通訊

OC端與 JS 端分別有一個Bridge, 兩個 Bridge 都保存了一樣一份模塊配置表, OC 要告訴 JS 本身有什麼模塊 , 模塊裏有什麼方法, JS 知道這些方法才能夠調用, JS 調用 OC 模塊方法的時候, 經過 Bridge 裏的配置表把模塊ID 方法 ID 和參數傳給 OC, OC 經過配置表找到對應的方法執行, 如圖

 

詳細流程:

 

 

1.JS端調用某個OC模塊暴露出來的方法。

2.把上一步的調用分解爲ModuleName,MethodName,arguments,再扔給MessageQueue處理。

在初始化時模塊配置表上的每個模塊都生成了對應的remoteModule對象,對象裏也生成了跟模塊配置表裏一一對應的方法,這些方法裏能夠拿到自身的模塊名,方法名,並對callback進行一些處理,再移交給MessageQueue。具體實如今BatchedBridgeFactory.js的_createBridgedModule裏,整個實現區區24行代碼,感覺下JS的魔力吧。

3.在這一步把JS的callback函數緩存在MessageQueue的一個成員變量裏,用CallbackID表明callback。在經過保存在MessageQueue的模塊配置表把上一步傳進來的ModuleName和MethodName轉爲ModuleID和MethodID。

4.把上述步驟獲得的ModuleID,MethodId,CallbackID和其餘參數argus傳給OC。至於具體是怎麼傳的,後面再說。

5.OC接收到消息,經過模塊配置表拿到對應的模塊和方法。

實際上模塊配置表已經通過處理了,跟JS同樣,在初始化時OC也對模塊配置表上的每個模塊生成了對應的實例並緩存起來,模塊上的每個方法也都生成了對應的RCTModuleMethod對象,這裏經過ModuleID和MethodID取到對應的Module實例和RCTModuleMethod實例進行調用。具體實如今_handleRequestNumber:moduleID:methodID:params:。

6.RCTModuleMethod對JS傳過來的每個參數進行處理。

RCTModuleMethod能夠拿到OC要調用的目標方法的每一個參數類型,處理JS類型到目標類型的轉換,全部JS傳過來的數字都是NSNumber,這裏會轉成對應的int/long/double等類型,更重要的是會爲block類型參數的生成一個block。

例如-(void)select:(int)index response:(RCTResponseSenderBlock)callback 這個方法,拿到兩個參數的類型爲int,block,JS傳過來的兩個參數類型是NSNumber,NSString(CallbackID),這時會把NSNumber轉爲int,NSString(CallbackID)轉爲一個block,block的內容是把回調的值和CallbackID傳回給JS。

這些參數組裝完畢後,經過NSInvocation動態調用相應的OC模塊方法。

7.OC模塊方法調用完,執行block回調。

8.調用到第6步說明的RCTModuleMethod生成的block。

9.block裏帶着CallbackID和block傳過來的參數去調JS裏MessageQueue的方法invokeCallbackAndReturnFlushedQueue。

10.MessageQueue經過CallbackID找到相應的JS callback方法。

11.調用callback方法,並把OC帶過來的參數一塊兒傳過去,完成回調。

整個流程就是這樣,簡單歸納下,差很少就是:JS函數調用轉ModuleID/MethodID -> callback轉CallbackID -> OC根據ID拿到方法 -> 處理參數 -> 調用OC方法 -> 回調CallbackID -> JS經過CallbackID拿到callback執行

 

說完原理,如今就是具體js怎麼調用 OC , OC 怎麼調用 JS

  .JS調用 OC

1> 新建兩個OC 文件遵照 RCTBridgeMoudle協議,這裏以集成 native 的微博分享給 RN 調用爲例

2> 經過 RCT_EXPORT_MOUDLE()暴露當前模塊  RCT_EXPORT_METHOD()暴露 native 方法

3> JS端經過 NativeModules獲取當前 native 模塊,調用模塊的方法,傳遞參數,同時支持回調,在native方法裏添加 block 回調或者 promise 回調,便可支持OC 回調傳遞結果給 JS

4> 導出常量原生模塊能夠導出一些常量,在JS 端能夠隨時訪問,用這種方法來傳遞一些靜態數據,能夠避免經過 Bridge 進行一次來回交互 

- (NSDictionary *)constantsToExport

{

  return @{ @"firstDayOfTheWeek": @"Monday" };

}

Js端訪問 console.log(CalendarManager.firstDayOfTheWeek);

 

 

RCT_REMAP_METHOD(testPromiseEvent,

                 resolver:(RCTPromiseResolveBlock)resolve

                 rejecter:(RCTPromiseRejectBlock)reject)

{

    NSArray *events =@[@"Promise ",@"test ",@" array"];

    if (events) {

        resolve(events);

    } else {

        NSError *error=[NSError errorWithDomain:@"我是Promise回調錯誤信息..." code:101 userInfo:nil];

        reject(@"no_events", @"There were no events", error);

    }

}

// 固然也能夠用這種 promise 的回調 在 js裏可使用 . then() 或者 async await 來獲取 promise 結果

//tips: 關於 RCT_EXPORT_METHOD() 能夠暴露方法給 JS 調用, 內部實現就是利用 runtime 遍歷該實例的方法, 過濾含有__ rct _export__的而後保存到模塊配置表中

 

 

. OC 調用 JS(給 JS 發送事件)

即便沒有被js調用 本地模塊也能夠主動給 JS 發送事件通知, 最直接的方式是使用 eventDispatcher

#import "RCTBridge.h"

#import "RCTEventDispatcher.h"

 

@implementation CalendarManager

 

@synthesize bridge = _bridge;

 

//  進行設置發送事件通知給JavaScript端

- (void)calendarEventReminderReceived:(NSNotification *)notification

{

    NSString *name = [notification userInfo][@"name"];

    [self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder"

                                                 body:@{@"name": name}];

}

 

@end

在 js中能夠這樣訂閱事件import { NativeAppEventEmitter } from 'react-native';

 

var subscription = NativeAppEventEmitter.addListener(

  'EventReminder',

  (reminder) => console.log(reminder.name)

);

...

// 千萬不要忘記忘記取消訂閱, 一般在componentWillUnmount函數中實現。

subscription.remove();

 

. RN 使用原生 UI 組件

 

1> 像原生自定義 UI 組件同樣, 新建 class 自定義視圖 , 這裏以一個能夠支持手勢縮放的相冊瀏覽視圖爲例

2> 新建 xxxViewManager 繼承自 RCTViewManager

3> RCT_EXPORT_MODULE(RCTPhotoView)導出模塊 RCT_EXPORT_VIEW_PROPERTY(imgURL, NSString); RCT_EXPORT_VIEW_PROPERTY(onSingleTap, RCTBubblingEventBlock);導出屬性和方法供 js 調用

4> 重寫 -(UIView *)View{ }的方法 返回自定義的UI 組件

5> 新建一個 JS 類  利用var RCTPhotoView = requireNativeComponent('RCTPhotoView', ImageBrowserView)

導出該自定義組件

6> 在須要的地方使用該組件(使用姿式與其餘組件同樣)

 

//  ViewManager 的定義 .h

 

 

// 新建 JS 類導出該原生 UI 組件 使用的時候直接導入便可

 

 

// 使用  賦值 url屬性  實現原生組件的點擊方法的回調

 

// 最後說下 RN 的線程管理 

RN 最主要的有兩個線程, UI MainThread 和 JSThread, UIThread 建立一個事件循環以後, 就一直有個 runloop 維持, 不斷接收處理 App 事件, JSThread更像一個底層數據採集器, 不斷上傳數據和事件, ios 經過 JavascriptCore 提供的 js Bridge, UIThread將這些事件和數據轉化爲 UI 改變, UI main thread 跟 JS thread更像是CS 模型,JS thread更像服務端, UI main thread是客戶端, UI main thread 不斷詢問JS thread而且請求數據,若是數據有變,則更新UI界面。

//RCTJavaScriptContext  javascriptCore 引擎初始化

  • (instancetype)init
    {
    NSThread *javaScriptThread = [[NSThread alloc] initWithTarget:[self class]
    selector:@selector(runRunLoopThread)
    object:nil];
    javaScriptThread.name = @"com.facebook.React.JavaScript";
    [javaScriptThread start];
    return [self initWithJavaScriptThread:javaScriptThread context:nil];
    }

js 自己是單線程語言, native 是多線程機制, 那麼 js如何來使用 native 的多線程, 或者說如何讓js在其餘線程執行

全部的 JS代碼都運行在"com.facebook.React.JavaScript"後臺線程中,全部的操做都是異步,不會卡死主線程UI。而且JS調用到Native中的接口中有強制的線程檢查,若是不是在React線程中則拋出異常。 這樣有一個問題,從JS調用Native中的代碼是執行在這個後臺線程中,RCTAlertManager.alertWithArgs明顯是個操做UI的接口,執行在後臺線程會crash,在導出RCTAlertManager時,經過實現方法- (dispatch_queue_t)methodQueue,原生模塊能夠指定本身想在哪一個隊列中被執行, 因此通常在自定義 UI 組件的時候, 若是涉及到須要操做 UI 的時候, 必定記得回到主線程操做, 否則就會 crash

在 iOS 開發中,一談到線程管理,確定離不開 GCD(Grand Central Dispatch)與 NSOperation/NSOperationQueue 技術選型上的爭論。關於這二者廣泛的觀點爲:GCD 較輕量,使用起來較靈活,但在任務依賴、併發數控制、任務狀態控制(線程取消/掛起/恢復/監控)等方面存在先天不足;NSOperation/NSOperationQueue 基於 GCD 作的封裝,使用較重,在某些情景下性能不如 GCD,但在併發環境下複雜任務處理能很好地知足一些特性,業務擴展性較好。

global.nativeFlushQueueImmediate 是Native提供的接口,__nativeCall把須要調用的module,method,params都塞到隊列裏,而後傳遞到Native

__nativeCall(module, method, params, onFail, onSucc) {

this._queue[MODULE_IDS].push(module); this._queue[METHOD_IDS].push(method); this._queue[PARAMS].push(params); global.nativeFlushQueueImmediate(this._queue); this._queue = [[],[],[]]; this._lastFlush = now; 

}

Native模塊查詢接口:global.nativeRequireModuleConfig和調用接口global.nativeFlushQueueImmediate,他們是在JS引擎(JSContext)初始化時,定義到全局變量的。

先從 JS 端看起,JS 調用 Native 的邏輯在 MessageQueue.js 的 _nativeCall 方法中。在最小調用間隔(MIN_TIME_BETWEEN_FLUSHES_MS=5ms)內,JS 端會將調用信息存儲在 _queue 數組中,經過 global. nativeFlushQueueImmediate 方法來調用 Native 端的功能。global.nativeFlushQueueImmediate 方法在 iOS 端映射的是一個全局的 Block,如圖

nativeFlushQueueImmediate 在這裏只是作了一箇中轉,功能的實現是經過調用 RCTBatchedBridge.m 中的 handleBuffer 方法。在 handleBuffer 中針對每一個組件使用一個 queue 來處理對應任務。其中,這個 queue 是組件數據 RCTModuleData 中的屬性 methodQueue,後文會詳細介紹。

雖然 JS 只具有單線程操做的能力,但經過利用 Native 端多線程處理能力,仍能夠很好地處理 RN 中的任務。回到剛開始拋出的問題,RN 在這裏用 GCD 而非 NSOperationQueue 來處理線程,主要緣由有: 

    • GCD 更加輕量,更方便與 Block 結合起來進行線程操做,性能上優於 NSOperationQueue 的執行;
    • 雖然 GCD 在控制線程數上有缺陷,不如 NSOperationQueue 有直接的 API 能夠控制最大併發數,但因爲 JS 是單線程發起任務,在 5ms 內會積累的任務數創造的併發不高,不用考慮最大併發數帶來的 CPU 性能問題。
    • 關於線程依賴的處理,因爲 JS 端是在同一個線程順序執行任務的,而在 Native 端對這些任務進行了分類,針對同類別任務在同一個 FIFO 隊列中執行。這樣的應用場景及 Native 端對任務的分類處理,規避了線程依賴的複雜處理。

 // 自定義UI 組件的線程管理

Native(iOS)端處理併發任務的線程是 RCTModuleData 中的屬性 methodQueue。RCTModuleData 是對組件對象的實例(instance)、方法(methods)、所屬線程(methodQueue)等方面的描述。每個 module 都有個獨立的線程來管理,具體線程的初始化在 RCTModuleData 的 setUpMethodQueue 中進行設置

 

這個方法開放了給組件自定義線程的接口。若是組件實現了 methodQueue 方法,則獲取此方法中設置的 queue;不然默認建立一個子線程

    • (dispatch_queue_t)methodQueue
      {
      return dispatch_get_main_queue();
      }
    • (dispatch_queue_t)methodQueue
      {
      return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
      }
  • 若是不經過 methodQueue 方法設定具體的執行隊列(dispatch_queue_t),則系統會自動建立一個默認線程,線程名稱爲 ModuleNameQueue;
  • 對同類別組件進行劃分,採用相同的執行隊列(好比系統 UI 組件都是在 RCTUIManagerQueue 中執行)。這樣有兩點好處,一是爲了控制組件執行隊列的無序生長,二也能夠控制特殊狀況下的線程併發數。
  •  是否是發現除了默認的 React-native 線程和主線程還有一個 RCTJSThread ,這是個什麼東西, 看圖:

  • 說的很清楚了吧, 他不是一個線程,而是一個隊列, 可以使 method 強制回到 js線程執行, 

  • javaScriptThread 是一個 NSThread 對象,看到這裏才知道真正具有執行任務的是這裏的 JavaScriptThread,而不是前面的 RCTJSThread。在 handBuffer 方法中之因此用 RCTJSThread,而不用 nil 替代,個人見解是爲了可讀性和擴展性。可讀性是指若是在各個組件中將當前線程對象設置爲 nil,使用者會比較迷惑;擴展性是指若是後面業務有擴展,發現根據 nil 比較不能知足需求,只需修改 RCTJSThread 初始化的地方,業務調用的地方徹底沒有感知。因此你徹底能夠

    - (dispatch_queue_t)methodQueue

    {

      return RCTJSThread;

    }這樣寫 回到 js線程

// RCTUIManagerQueue

RN 中的 UI 操做都是在 RCTUIManagerQueue 中進行的, 他是一個併發隊列,可是優先級是最高的, 因爲蘋果在 iOS 8.0 以後引入了 NSQualityOfService,淡化了原有的線程優先級概念,因此 RN 在這裏優先使用了 8.0 的新功能,而對 8.0 如下的沿用原有的方式。但不論用哪一種方式,都保證 RCTUIManagerQueue 在併發隊列中優先級是最高的。到這裏或許有疑問了,UI 操做不是要在主線程裏操做嗎,這裏爲何是在一個子線程中操做?其實在此執行的是 UI 的準備工做,當真正須要把 UI 元素加入主界面,開始圖形繪製時,才須要在主線程裏操做 

 在這裡回到主線程操做

 

 

End 

相關文章
相關標籤/搜索