iOS 任務排程器:為 CPU 和記憶體減負

Geek.發表於2019-04-28

前言

前些時間有好幾個技術朋友問過筆者類似的問題:主執行緒需要執行大量的任務導致卡頓如何處理?非同步任務量級過大導致 CPU 和記憶體壓力過高如何優化?

解決類似的問題可以用幾個思路:降頻、淘汰、優先順序排程。

本來解決這些問題並不需要很複雜的程式碼,但是涉及到一些 C 程式碼並且要注意執行緒安全的問題,所以筆者就做了這樣一個輪子,以解決任務排程引發的效能問題。

本文講述YBTaskScheduler的原理,讀者朋友需要有一定的 iOS 基礎,瞭解一些效能優化的知識,基本用法可以先看看 GitHubREADME,DEMO 中也有一個相簿列表的應用案例。

一、需求分析

就拿 DEMO 中的案例來說明,一個顯示相簿圖片的列表:

實現圖中業務,必然考慮到幾個耗時操作:

從相簿讀取圖片

解壓圖片

圓角處理

繪製圖片

理所當然的想到處理方案(DEMO中有實現):

非同步讀取圖片

非同步裁剪圖片為正方形(這個過程中就解壓了)

非同步裁剪圓角

回到主執行緒繪製圖片

一整套流程下來,貌似需求很好的解決了,但是當快速滑動列表時,會發現 CPU 和記憶體的佔用會比較高(這取決於從相簿中讀取並顯示多大的圖片)。當然 DEMO 中按照螢幕的物理畫素處理,就算不使用任務排程器元件快速滑動列表也基本不會有掉幀的現象。考慮到老舊裝置或者技術人員的水平,很多時候這種需求會導致嚴重的 CPU 和記憶體負擔,甚至導致閃退。

以上處理方案可能存在的效能瓶頸:

從相簿讀取圖片、裁剪圖片,處理圓角、主執行緒繪製等操作會導致 CPU 計算壓力過大。

同時解壓的圖片、同時繪製的圖片過多導致記憶體峰值飆升(更不要說做了圖片的快取)。

任何一種情況都可能導致客戶端卡死或者閃退,結合業務來分析問題,會發現優化的思路還是不難找到:

滑出螢幕的圖片不會存在繪製壓力,而當前螢幕中的圖片會在一個 RunLoop 迴圈週期繪製,可能造成掉幀。所以可以減少一個 RunLoop 迴圈週期所繪製的圖片數量。

快速滑動列表,大量的非同步任務直接交由 CPU 執行,然而滑出螢幕的圖片已經沒有處理它的意義了。所以可以提前刪除掉已經滑出螢幕的非同步任務,以此來降低 CPU 和記憶體壓力。

沒錯,YBTaskScheduler元件就是替你做了這些事情 ,而且還不止於此。

二、命令模式與 RunLoop

想要管理這些複雜的任務,並且在合適的時機呼叫它們,自然而然的就想到了命令模式。意味著任務不能直接執行,而是把任務作為一個命令裝入容器。

在 Objective-C 中,顯然 Block 程式碼塊能解決延遲執行這個問題:

[_scheduler addTask:^{/*

具體任務程式碼

解壓圖片、裁剪圖片、訪問磁碟等

*/}];
複製程式碼

然後元件將這些程式碼塊“裝起來”,元件由此“掌握”了所有的任務,可以自由的決定何時呼叫這些程式碼塊,何時對某些程式碼塊進行淘汰,還可以實現優先順序排程。

既然是命令模式,還差一個 Invoker (呼叫程式),即何時去觸發這些任務。結合 iOS 的技術特點,可以監聽 RunLoop 迴圈週期來實現:

staticvoidaddRunLoopObserver() {staticdispatch_once_tonceToken;dispatch_once(&onceToken, ^{ taskSchedulers = [NSHashTableweakObjectsHashTable];CFRunLoopObserverRefobserver =CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting | kCFRunLoopExit,true,0xFFFFFF, runLoopObserverCallBack,NULL);CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);CFRelease(observer); });}

然後在回撥函式中進行任務的排程。

三、策略模式

考慮到任務的淘汰策略和優先順序排程,必然需要一些高效資料結構來支撐,為了提高處理效率,筆者直接使用了 C++ 的資料結構:deque和priority_queue。

因為要實現任務淘汰,所以使用deque雙端佇列來模擬棧和佇列,而不是直接使用stack和queue。使用priority_queue優先佇列來處理自定義的優先順序排程,它的缺點是不能刪除低優先順序節點,為了節約時間成本姑且夠用。

具體的策略:

棧:後加入的任務先執行(可以理解為後加入的任務優先順序高),優先淘汰先加入的任務。

佇列:先加入的任務先執行(可以理解為先加入的任務優先順序高),優先淘汰後加入的任務。

優先佇列:自定義任務優先順序,不支援任務淘汰。

實際上元件是推薦使用棧和佇列這兩種策略,因為插入和取出的時間複雜度是常數級的,需要定製任務的優先順序時才考慮使用優先佇列,因為其插入複雜度是 O(logN) 的。

至此,整個元件的業務是比較清晰了,元件需要讓這三種處理方式可以自由的變動,所以採用策略模式來處理,下面是 UML 類圖:

UML類圖

嗯,這是個挺標準的策略模式。

四、執行緒安全

由於任務的排程可能在任意執行緒,所以必須要做好容器(棧、佇列、優先佇列)訪問的執行緒安全問題,元件是使用pthread_mutex_t和dispatch_once來保證執行緒安全,同時筆者儘量減少臨界區來提高效能。值得注意的是,如果不會存線上程安全的程式碼就不要去加鎖了。

關於多執行緒及安全可以看筆者的另一篇文章:iOS 如何高效的使用多執行緒,這裡就不贅述了。

後語

部分技術細節就不多說了,元件程式碼量比較少,如果感興趣可以直接看原始碼。實際上這個元件的應用場景並不是很多,在專案穩定需要做深度的效能優化時可能會比較需要它,並且希望使用它的人也能瞭解一些原理,做到胸有成竹,才能靈活的運用。

本文源為第三方轉載,原文連結:www.jianshu.com/p/f2a610c77…

文章若有不對地方,歡迎批評指正,一個小而有用QQ交流群:805558511

相關文章