作者簡介: 彭飛,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 端 先從 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方法映射
nativeFlushQueueImmediate在這里只是做了一個中轉,功能的實現(xiàn)是通過調(diào)用RCTBatchedBridge.m中的handleBuffer方法,具體代碼如圖 3 所示。在handleBuffer中針對每個組件使用一個queue來處理對應任務。其中,這個queue是組件數(shù)據(jù)RCTModuleData中的屬性methodQueue,后文會詳細介紹。圖 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 中的 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)組件一覽果 
圖 5 本地項目中實現(xiàn) methodQueue RCTJSThread
RCTJSThread 是在 RCTBridge 中定義的一個私有屬性,如圖 6 所示。
圖 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 設計 
RCTBatchedBridge.m中的handleBuffer是處理 JS 向 Native 端的事件請求的。在第 928 行,如果一個組件中定義的 queue 是 RCTJSThread,則在 JSExecutor 中執(zhí)行executeBlockOnjavaScriptQueue:方法,具體執(zhí)行代碼如圖 8 所示。圖 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 代碼 由于蘋果在 iOS 8.0 之后引入了 NSQualityOfService,淡化了原有的線程優(yōu)先級概念,所以 RN 在這里優(yōu)先使用了 8.0 的新功能,而對 8.0 以下的沿用原有的方式。但不論用哪種方式,都保證 RCTUIManagerQueue 在并發(fā)隊列中優(yōu)先級是最高的。到這里或許有疑問了,UI 操作不是要在主線程里操作嗎,這里為什么是在一個子線程中操作?其實在此執(zhí)行的是 UI 的準備工作,當真正需要把 UI 元素加入主界面,開始圖形繪制時,才需要在主線程里操作,具體代碼見圖 10。
圖 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 端加載過程代碼 為控制任務 4 和任務 1/2/3 之間的依賴關系,定義了
dispatch_group_t initModulesAndLoadSource管理依賴;而任務 3 依賴任務 2 是采取阻塞的方式。下面分別看各個任務中的處理情況。先看片段 1 的代碼,如圖 12 所示。
圖 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 代碼詳情 片段 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? 我猜當時作者是不是也在糾結要不要使用 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屬性控制線程的最大并發(fā)數(shù)的邏輯在 RCTImageLoader 的
dequeueTasks方法中,如圖 16 所示。并發(fā)任務存儲在數(shù)組_pendingTasks中,當前進行中的任務數(shù)存儲在_activeTasks中。由于不能像 NSOperationQueue 中的任務一樣,執(zhí)行完畢后就被自動清除,在這里需要手動清除已經(jīng)執(zhí)行完畢的任務,將任務從_pendingTasks中移除,并改變并發(fā)任務數(shù),具體在代碼的 240 行至 251 行。圖 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)用 其次,需要處理等待任務。比如當前隊列中已經(jīng)有了最大并發(fā)數(shù)個任務了,下一個任務過來的時候只能暫時加入隊列等待了。如果后續(xù)沒有事件來調(diào)用 dequeueTasks 方法,超過最大并發(fā)數(shù)之外的任務將會得不到執(zhí)行。一個通用的做法是用一個定時器來維持,定時掃描任務隊列來執(zhí)行任務。但是 RN 里面借助了圖片渲染的邏輯巧妙地避開了這個,即在解碼完成時調(diào)用一次
dequeueTasks方法,這時候能保證等待任務能全部執(zhí)行完畢,具體如圖 18 所示代碼。圖 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) 回到圖片組件中的線程取消上來。在 RCTImageLoader 的加載/解碼圖片的方法中返回參數(shù)為任務取消 block:
RCTImageLoaderCancellationBlock。Block 的具體實現(xiàn)在每一個方法中,以 loadImageWithURLRequest 為例,如圖 20 所示。[task cancel]調(diào)用的是上述的 NSOperation 的 cancel 方法。圖 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 通訊機制》。
新聞熱點
疑難解答
圖片精選