国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 系統(tǒng) > iOS > 正文

從 iOS 視角解密 React Native 中的線程

2019-11-07 23:17:28
字體:
來源:轉載
供稿:網(wǎng)友

作者簡介: 彭飛,58 同城 iOS 客戶端架構師。專注于新技術的研發(fā),主要負責 App 端組件化架構以及性能優(yōu)化,并已推廣 React Native在 58 同城 App 中業(yè)務場景的應用。 責編:唐小引,技術之路,共同進步。歡迎技術投稿、給文章糾錯,請發(fā)送郵件至tangxy@csdn.net。 聲明: 本文為《程序員》原創(chuàng)文章,未經(jīng)允許請勿轉載,更多精彩文章請訂閱 2017 年《程序員》。

【導語】React Native(后文簡稱 RN)自推出至今,已在國內(nèi)不少公司得到了推廣應用,前景頗為看好。而當前市面上對 RN 源代碼級別的研究文章較少,對理解以及應用 RN 上帶來諸多不便。線程管理是 RN 的一個基礎內(nèi)容,理清它對了解 RN 中的組件設計、事件交互、復雜任務處理有很大的幫助。由此,本文將基于 iOS 端的源代碼介紹 RN 中線程管理的相關內(nèi)容。


在 iOS 開發(fā)中,一談到線程管理,肯定離不開 GCD(Grand Central Dispatch)與 NSOperation/NSOperationQueue 技術選型上的爭論。關于這兩者普遍的觀點為:GCD 較輕量,使用起來較靈活,但在任務依賴、并發(fā)數(shù)控制、任務狀態(tài)控制(線程取消/掛起/恢復/監(jiān)控)等方面存在先天不足;NSOperation/NSOperationQueue 基于 GCD 做的封裝,使用較重,在某些情景下性能不如 GCD,但在并發(fā)環(huán)境下復雜任務處理能很好地滿足一些特性,業(yè)務擴展性較好。

但是 RN 在線程管理是如何選用 GCD 和 NSOperation 的?帶著此問題,一起從組件中的線程、JSBundle 加載中的線程以及圖片組件中的線程三個方面,逐步看看其中的線程管理細節(jié)。

組件中的線程

組件中的線程交互

RN 的本質是利用 JS 來調(diào)用 Native 端的組件,從而實現(xiàn)相應的功能。由于 RN 的 JS 端只具備單線程操作的能力,而 Native 端支持多線程操作,所以如何將 JS 的單線程與 Native 端的多線程結合起來,是 JS 與 Native 端交互的一個重要問題。圖 1,直觀展示了 RN 是如何處理的。

圖 1  JS 調(diào)用 Native 端

圖 1 JS 調(diào)用 Native 端

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

圖 2  global.nativeFlushQueueImmediate 方法映射

圖 2 global.nativeFlushQueueImmediate 方法映射

nativeFlushQueueImmediate 在這里只是做了一個中轉,功能的實現(xiàn)是通過調(diào)用 RCTBatchedBridge.m 中的 handleBuffer 方法,具體代碼如圖 3 所示。在 handleBuffer 中針對每個組件使用一個 queue 來處理對應任務。其中,這個 queue 是組件數(shù)據(jù) RCTModuleData 中的屬性 methodQueue,后文會詳細介紹。

圖 3  handleBuffer 方法調(diào)用

圖 3 handleBuffer 方法調(diào)用

從上面的代碼追蹤可以看出,雖然 JS 只具備單線程操作的能力,但通過利用 Native 端多線程處理能力,仍可以很好地處理 RN 中的任務?;氐絼傞_始拋出的議題,RN 在這里用 GCD 而非 NSOperationQueue 來處理線程,筆者認為主要原因有:

GCD 更加輕量,更方便與 Block 結合起來進行線程操作,性能上優(yōu)于 NSOperationQueue 的執(zhí)行;雖然 GCD 在控制線程數(shù)上有缺陷,不如 NSOperationQueue 有直接的 API 可以控制最大并發(fā)數(shù),但由于 JS 是單線程發(fā)起任務,在 5ms 內(nèi)會積累的任務數(shù)創(chuàng)造的并發(fā)不高,不用考慮最大并發(fā)數(shù)帶來的 CPU 性能問題。關于線程依賴的處理,由于 JS 端是在同一個線程順序執(zhí)行任務的,而在 Native 端對這些任務進行了分類(后文會有敘述),針對同類別任務在同一個 FIFO 隊列中執(zhí)行。這樣的應用場景及 Native 端對任務的分類處理,規(guī)避了線程依賴的復雜處理。

組件中線程自定義

前文提到了 Native(iOS)端處理并發(fā)任務的線程是 RCTModuleData 中的屬性 methodQueue。RCTModuleData 是對組件對象的實例(instance)、方法(methods)、所屬線程(methodQueue)等方面的描述。每一個 module 都有個獨立的線程來管理,具體線程的初始化在 RCTModuleData 的 setUpMethodQueue 中進行設置,詳細代碼可見圖 4。

圖 4  線程自定義

圖 4 線程自定義

圖 4 中的 174 行至 177 行是開放給組件自定義線程的接口。如果組件實現(xiàn)了 methodQueue 方法,則獲取此方法中設置的queue;否則默認創(chuàng)建一個子線程。問題來了,既然可以自定義線程,那 RN 中內(nèi)置組件是如何定義的,對開發(fā)過程中的自定義組件在設置線程的時候需要注意什么?

圖 5 是本地項目中實現(xiàn) methodQueue 的組件,除去以 RCTWB 開頭的自定義組件,其它都是系統(tǒng)自帶的。通過查看每一個組件 methodQueue 方法的實現(xiàn),發(fā)現(xiàn)有的是在主線程執(zhí)行,有的是在 RCTJSThread 中執(zhí)行,表 1 所示的是其中主要系統(tǒng)組件的具體情況。

表 1 RCTJSThread 中的主要系統(tǒng)組件一覽果

表 1  RCTJSThread 中的主要系統(tǒng)組件一覽果

圖 5  本地項目中實現(xiàn) methodQueue

圖 5 本地項目中實現(xiàn) methodQueue

RCTJSThread

RCTJSThread 是在 RCTBridge 中定義的一個私有屬性,如圖 6 所示。

圖 6  RCTJSThread 定義

圖 6 RCTJSThread 定義

RCTJSThread 的類型是 dispatch_queue_t,它是 GCD 中管理任務的隊列,與 block 聯(lián)合起來使用。一個 block 封裝一個特定的任務,dispatch_queue_t 一次執(zhí)行一個 block,相互獨立的 dispatch_queue_t 可以并發(fā)執(zhí)行 block。

RCTJSThread 的初始化比較有意思,并沒有采用 dispatch_queue_create 來創(chuàng)建一個 queue 實例,而是指向 KCFNull。我在整個源代碼里全局搜了一下,沒有其他的地方對 RCTJSThread 進行初始化。事實上,RCTJSThread 在設計上不是用來執(zhí)行任務的,而是用來進行比較的,看圖 7 中的代碼。

圖 7  RCTJSThread 設計

圖 7 RCTJSThread 設計

RCTBatchedBridge.m 中的 handleBuffer 是處理 JS 向 Native 端的事件請求的。在第 928 行,如果一個組件中定義的 queue 是 RCTJSThread,則在 JSExecutor 中執(zhí)行 executeBlockOnjavaScriptQueue: 方法,具體執(zhí)行代碼如圖 8 所示。

圖 8  執(zhí)行 executeBlockOnJavaScriptQueue:方法

圖 8 執(zhí)行 executeBlockOnJavascriptQueue: 方法

_javaScriptThread 是一個 NSThread 對象,看到這里才知道真正具備執(zhí)行任務的是這里的 JavaScriptThread,而不是前面的 RCTJSThread。在 handBuffer 方法中之所以用 RCTJSThread,而不用 nil 替代,我的看法是為了可讀性和擴展性??勺x性是指如果在各個組件中將當前線程對象設置為 nil,使用者會比較迷惑;擴展性是指如果后面業(yè)務有擴展,發(fā)現(xiàn)根據(jù) nil 比較不能滿足需求,只需修改 RCTJSThread 初始化的地方,業(yè)務調(diào)用的地方完全沒有感知。

RCTUIManagerQueue

RN 的 UI 組件調(diào)用都是在 RCTUIManagerQueue 完成的,關于它的創(chuàng)建如圖 9 所示。

圖 9  創(chuàng)建 RCTUIManagerQueue 代碼

圖 9 創(chuàng)建 RCTUIManagerQueue 代碼

由于蘋果在 iOS 8.0 之后引入了 NSQualityOfService,淡化了原有的線程優(yōu)先級概念,所以 RN 在這里優(yōu)先使用了 8.0 的新功能,而對 8.0 以下的沿用原有的方式。但不論用哪種方式,都保證 RCTUIManagerQueue 在并發(fā)隊列中優(yōu)先級是最高的。到這里或許有疑問了,UI 操作不是要在主線程里操作嗎,這里為什么是在一個子線程中操作?其實在此執(zhí)行的是 UI 的準備工作,當真正需要把 UI 元素加入主界面,開始圖形繪制時,才需要在主線程里操作,具體代碼見圖 10。

圖 10  UI 操作代碼

圖 10 UI 操作代碼

這里 RCTUIManagerQueue 是一個先進先出的順序隊列,保證了 UI 的順序執(zhí)行不出錯,但這里是把 UI 的一些需要準備的工作(比如計算 frame)放在一個子線程里面操作完成后,再統(tǒng)一提交給主線程進行操作的。這個過程是阻塞的,針對一些低端機型渲染復雜界面,會出現(xiàn)打開 RN 頁面的一段空白頁面的情況,這是 RN 需要優(yōu)化的一個地方。

前面介紹了組件中線程的相關情況,針對平常開發(fā)中的自定義組件,有以下兩點需要關注:

如果不通過 methodQueue 方法設定具體的執(zhí)行隊列(dispatch_queue_t),則系統(tǒng)會自動創(chuàng)建一個默認線程,線程名稱為 ModuleNameQueue;對同類別組件進行劃分,采用相同的執(zhí)行隊列(比如系統(tǒng) UI 組件都是在 RCTUIManagerQueue 中執(zhí)行)。這樣有兩點好處,一是為了控制組件執(zhí)行隊列的無序生長,二也可以控制特殊情況下的線程并發(fā)數(shù)。

JSBundle 加載中的線程操作

前面敘述的組件相關的線程情況,從業(yè)務場景方面來看,略顯簡單,下面將介紹一下場景復雜點的線程操作。

React Native 中加載過程業(yè)務邏輯比較多,需要先將 JSBundle 資源文件加載進內(nèi)存,同時解析 Native 端組件,將組件相關配置信息加載進內(nèi)存,然后再執(zhí)行 JS 代碼。圖 11 所示的 Native 端加載過程代碼,在 RCTBatchedBridge.m 的 start 方法中。其中片段 1 是將 JSBundle 文件加載進內(nèi)存,片段 2 是初始化 RN 在 Native 端組件,片段 3 是設置 JS 執(zhí)行環(huán)境以及初始化組件的配置,片段 4 是執(zhí)行 JS 代碼。這 4 個代碼片段對應 4 個任務,其中任務 4 依賴任務 1/2/3,需要在它們?nèi)繄?zhí)行完畢后才能執(zhí)行。任務 1/3 可以并行,沒有依賴關系。任務 3 依賴任務 2,需要任務 2 執(zhí)行完畢后才能開始執(zhí)行。

圖 11  Native 端加載過程代碼

圖 11 Native 端加載過程代碼

為控制任務 4 和任務 1/2/3 之間的依賴關系,定義了 dispatch_group_t initModulesAndLoadSource 管理依賴;而任務 3 依賴任務 2 是采取阻塞的方式。下面分別看各個任務中的處理情況。

先看片段 1 的代碼,如圖 12 所示。

圖 12  片段 1 代碼

圖 12 片段 1 代碼

dispatch_group_enter(group);dispatch_async(queue, ^{  dispatch_group_leave(group);});

但這里并沒有使用 dispatch_async,而是采用默認的同步方式。具體原因在于 loadSource 中有一部分屬性是下一個隊列需要使用到的,這部分屬性的初始化需要在這個隊列中進行阻塞的同步執(zhí)行。LoadSource 方法中有一部分邏輯是異步的,這部分數(shù)據(jù)可以在 initModulesAndLoadSource 的 group 合并的時候處理。 片段 2 的處理比較簡單,跳過直接看片段 3 的代碼,如圖 13 所示。

圖 13  片段 3 代碼詳情

圖 13 片段 3 代碼詳情

片段 3 中的任務是又一個復合任務,由一個新的 group(setupJSExecutorAndModuleConfig)來管理依賴。有兩個并發(fā)任務,初始化 JS Executor(119-124 行)和獲取 module 配置(127-133 行)。這兩個并發(fā)任務都放在并發(fā)隊列 bridgeQueue 中執(zhí)行,完成后進行合并處理(135-150 行)。需要注意的是片段 3 中采用 dispatch_group_async(group, queue, ^{ });來執(zhí)行隊列中的任務,其效果與前文敘述的 dispatch_group_enter/dispatch_group_leave 相同。

從上面的分析可以看出,GCD 利用 dispatch_group_t 可以很好地處理線程間的依賴關系。里面的線程操作雖不能像前文中組件的線程對開發(fā)有直接幫助,但是一個很好的利用 GCD 解決復雜任務的實例。

圖片中的線程

看過 SDWebImage 的源碼的同學知道,SDWebImage 采用的是 NSOperationQueue 來管理線程。但是 RN 在 image 組件中并沒有采用 NSOperationQueue,還是一如繼往地使用 GCD,有圖 14 為證。眼尖的同學會發(fā)現(xiàn)圖中明明有一個 NSOperationQueue 變量 _imageDecodeQueue,這是干什么用的?有興趣可以在工程中搜索一下這個變量,除了在這里定義了一下,沒有在其他任何地方使用。

圖 14  NSOperationQueue or GCD?

圖 14 NSOperationQueue or GCD?

我猜當時作者是不是也在糾結要不要使用 NSOperationQueue,而決定用 GCD 之后忘了刪掉這個變量。

既然決定了使用 GCD,就需要解決兩個棘手的問題,控制線程的并發(fā)數(shù)以及取消線程的執(zhí)行。這兩個問題也是 GCD 與 NSOperationQueue 進行比較時談論最多的問題,且普遍認為當有此類問題時,需要棄 GCD 而選 NSOperationQueue。下面就來敘述一下 RN 中是如何來解決這兩個問題的。

最大并發(fā)數(shù)的控制

首先是控制線程的并發(fā)數(shù)。在 RCTImageLoader 中有一個屬性 maxConcurrentLoadingTasks,如圖 15 所示。除此之外,還有一個控制圖片解碼任務的并發(fā)數(shù) maxConcurrentDecodingTasks。加載圖片和解碼圖片是一項非常耗內(nèi)存/CPU 的操作,所以需要根據(jù)業(yè)務需求的具體情況來靈活設定。

圖 15  maxConcurrentLoadingTasks 屬性

圖 15 maxConcurrentLoadingTasks 屬性

控制線程的最大并發(fā)數(shù)的邏輯在 RCTImageLoader 的 dequeueTasks 方法中,如圖 16 所示。并發(fā)任務存儲在數(shù)組_pendingTasks 中,當前進行中的任務數(shù)存儲在 _activeTasks 中。由于不能像 NSOperationQueue 中的任務一樣,執(zhí)行完畢后就被自動清除,在這里需要手動清除已經(jīng)執(zhí)行完畢的任務,將任務從 _pendingTasks 中移除,并改變并發(fā)任務數(shù),具體在代碼的 240 行至 251 行。

圖 16  dequeueTasks 方法定義

圖 16 dequeueTasks 方法定義

接下來看控制任務的執(zhí)行代碼,在 267 行至 276 行。遍歷需要執(zhí)行的任務數(shù)組,如果規(guī)定的條件超過了最大并發(fā)任務數(shù),中斷操作;否則直接執(zhí)行任務,同時將計數(shù)器加 1。由于所有任務都是在 _URLCacheQueue 這個順序隊列中執(zhí)行的,且一次只能執(zhí)行一個任務,所以并發(fā)的實現(xiàn)是在 RCTNetworkTask 中進行的,有興趣的同學可以深入看看。

進展到這里,最大并發(fā)數(shù)的控制還有一個關鍵任務沒有完成,就是如何保證加入隊列中的任務能全部完成。具體操作分為三個方面:首先是在加載圖片的入口方法中有一次調(diào)用,如圖 17 所示。

圖 17  圖片加載入口方法中的調(diào)用

圖 17 圖片加載入口方法中的調(diào)用

其次,需要處理等待任務。比如當前隊列中已經(jīng)有了最大并發(fā)數(shù)個任務了,下一個任務過來的時候只能暫時加入隊列等待了。如果后續(xù)沒有事件來調(diào)用 dequeueTasks 方法,超過最大并發(fā)數(shù)之外的任務將會得不到執(zhí)行。一個通用的做法是用一個定時器來維持,定時掃描任務隊列來執(zhí)行任務。但是 RN 里面借助了圖片渲染的邏輯巧妙地避開了這個,即在解碼完成時調(diào)用一次dequeueTasks 方法,這時候能保證等待任務能全部執(zhí)行完畢,具體如圖 18 所示代碼。

圖 18  解碼完成,調(diào)用 dequeueTasks 方法

圖 18 解碼完成,調(diào)用 dequeueTasks 方法

最后,還有一種情況,即在線程取消的時候也需要調(diào)用一次 dequeueTasks 方法,來保證線程取消的情況下任務也能繼續(xù)完成。這樣綜合上述三種情況的調(diào)用,加入隊列中的任務都能全部執(zhí)行完畢了。

線程的取消

如果說上面的最大并發(fā)數(shù)的控制還可以有方法自定義實現(xiàn),但是線程的取消一直是 GCD 中無法做到的,只能通過 NSOperation 的接口來實現(xiàn)。上文提到了加載圖片并發(fā)操作是在 RCTNetworkTask 中實現(xiàn)的,而 RCTNetworkTask 調(diào)用的是 RN 中 RCTNetwork 中的代碼,先來簡單介紹一下 RCTNetwork 的實現(xiàn)。

圖 19 所示的是與圖片訪問相關的實現(xiàn)類 RCTDataRequestHandler??梢钥闯觯瑪?shù)據(jù)訪問的任務是用 NSOperationQueue 來管理的,線程的取消是調(diào)用 NSOperation 的 cancel 方法來執(zhí)行的。后面介紹到的圖片下載任務的取消即基于此。

圖 19  RCTDataRequestHandler 實現(xiàn)

圖 19 RCTDataRequestHandler 實現(xiàn)

回到圖片組件中的線程取消上來。在 RCTImageLoader 的加載/解碼圖片的方法中返回參數(shù)為任務取消 block:RCTImageLoaderCancellationBlock。Block 的具體實現(xiàn)在每一個方法中,以 loadImageWithURLRequest 為例,如圖 20 所示。[task cancel]調(diào)用的是上述的 NSOperation 的 cancel 方法。

圖 20  取消加載

圖 20 取消加載

至此,RN 在優(yōu)先使用 GCD 的情況下,完成了圖片組件中的線程相關邏輯。還是回到最開始討論的話題,GCD 在控制任務狀態(tài),比如取消、懸掛、等待、監(jiān)控線程等,目前采取自定義方法沒有很好的方式實現(xiàn),還得借助于 NSOperation。而在控制最大并發(fā)數(shù)方面,RN 提供了一個很好的自定義實現(xiàn)的例子,值得學習。

結語

本文從組件、JSBundle 加載、圖片中的線程三個方面,對 RN 的源代碼實現(xiàn),以具體的實例,敘述了 RN 中線程管理的詳細情況。這三個例子,從技術實現(xiàn)上,復雜度逐步增加,覆蓋了線程中任務依賴、最大并發(fā)數(shù)的控制、線程取消等經(jīng)典討論點。特別是在任務依賴、最大并發(fā)數(shù)的控制上,給我們呈現(xiàn)了用 GCD 來解決的一個很好的實例。

參考資料

1.《Choosing Between NSOperation and Grand Central Dispatch》; 2.《NSOperation vs Grand Central Dispatch》; 3.《Is JavaScript guaranteed to be single-threaded》; 4.《淺析 RN 通訊機制》。


發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 台前县| 连城县| 阳泉市| 花垣县| 平顺县| 隆昌县| 斗六市| 吉安市| 大连市| 安顺市| 宜兴市| 横山县| 昌图县| 江陵县| 云龙县| 木兰县| 探索| 临汾市| 巩义市| 汨罗市| 阳江市| 崇州市| 永兴县| 罗源县| 望奎县| 游戏| 施甸县| 进贤县| 绍兴县| 山阳县| 长汀县| 华坪县| 满洲里市| 和硕县| 泰顺县| 高要市| 蕲春县| 永丰县| 景宁| 明水县| 达孜县|