GitHub 地址:YBTaskScheduler
支持 cocopods,使用簡(jiǎn)便,效率不錯(cuò),一個(gè)性能優(yōu)化的基礎(chǔ)組件。
前言
前些時(shí)間有好幾個(gè)技術(shù)朋友問(wèn)過(guò)筆者類似的問(wèn)題:主線程需要執(zhí)行大量的任務(wù)導(dǎo)致卡頓如何處理?異步任務(wù)量級(jí)過(guò)大導(dǎo)致 CPU 和內(nèi)存壓力過(guò)高如何優(yōu)化?
解決類似的問(wèn)題可以用幾個(gè)思路:降頻、淘汰、優(yōu)先級(jí)調(diào)度。
本來(lái)解決這些問(wèn)題并不需要很復(fù)雜的代碼,但是涉及到一些 C 代碼并且要注意線程安全的問(wèn)題,所以筆者就做了這樣一個(gè)輪子,以解決任務(wù)調(diào)度引發(fā)的性能問(wèn)題。
本文講述 YBTaskScheduler 的原理,讀者朋友需要有一定的 iOS 基礎(chǔ),了解一些性能優(yōu)化的知識(shí),基本用法可以先看看 GitHub README,DEMO 中也有一個(gè)相冊(cè)列表的應(yīng)用案例。
一、需求分析
就拿 DEMO 中的案例來(lái)說(shuō)明,一個(gè)顯示相冊(cè)圖片的列表:
實(shí)現(xiàn)圖中業(yè)務(wù),必然考慮到幾個(gè)耗時(shí)操作:
- 從相冊(cè)讀取圖片
- 解壓圖片
- 圓角處理
- 繪制圖片
理所當(dāng)然的想到處理方案(DEMO中有實(shí)現(xiàn)):
- 異步讀取圖片
- 異步裁剪圖片為正方形(這個(gè)過(guò)程中就解壓了)
- 異步裁剪圓角
- 回到主線程繪制圖片
一整套流程下來(lái),貌似需求很好的解決了,但是當(dāng)快速滑動(dòng)列表時(shí),會(huì)發(fā)現(xiàn) CPU 和內(nèi)存的占用會(huì)比較高(這取決于從相冊(cè)中讀取并顯示多大的圖片)。當(dāng)然 DEMO 中按照屏幕的物理像素處理,就算不使用任務(wù)調(diào)度器組件快速滑動(dòng)列表也基本不會(huì)有掉幀的現(xiàn)象。考慮到老舊設(shè)備或者技術(shù)人員的水平,很多時(shí)候這種需求會(huì)導(dǎo)致嚴(yán)重的 CPU 和內(nèi)存負(fù)擔(dān),甚至導(dǎo)致閃退。
以上處理方案可能存在的性能瓶頸:
- 從相冊(cè)讀取圖片、裁剪圖片,處理圓角、主線程繪制等操作會(huì)導(dǎo)致 CPU 計(jì)算壓力過(guò)大。
- 同時(shí)解壓的圖片、同時(shí)繪制的圖片過(guò)多導(dǎo)致內(nèi)存峰值飆升(更不要說(shuō)做了圖片的緩存)。
任何一種情況都可能導(dǎo)致客戶端卡死或者閃退,結(jié)合業(yè)務(wù)來(lái)分析問(wèn)題,會(huì)發(fā)現(xiàn)優(yōu)化的思路還是不難找到:
- 滑出屏幕的圖片不會(huì)存在繪制壓力,而當(dāng)前屏幕中的圖片會(huì)在一個(gè) RunLoop 循環(huán)周期繪制,可能造成掉幀。所以可以減少一個(gè) RunLoop 循環(huán)周期所繪制的圖片數(shù)量。
- 快速滑動(dòng)列表,大量的異步任務(wù)直接交由 CPU 執(zhí)行,然而滑出屏幕的圖片已經(jīng)沒(méi)有處理它的意義了。所以可以提前刪除掉已經(jīng)滑出屏幕的異步任務(wù),以此來(lái)降低 CPU 和內(nèi)存壓力。
沒(méi)錯(cuò), YBTaskScheduler 組件就是替你做了這些事情 ,而且還不止于此。
二、命令模式與 RunLoop
想要管理這些復(fù)雜的任務(wù),并且在合適的時(shí)機(jī)調(diào)用它們,自然而然的就想到了命令模式。意味著任務(wù)不能直接執(zhí)行,而是把任務(wù)作為一個(gè)命令裝入容器。
在 Objective-C 中,顯然 Block 代碼塊能解決延遲執(zhí)行這個(gè)問(wèn)題:
[_scheduler addTask:^{
/*
具體任務(wù)代碼
解壓圖片、裁剪圖片、訪問(wèn)磁盤(pán)等
*/
}];
然后組件將這些代碼塊“裝起來(lái)”,組件由此“掌握”了所有的任務(wù),可以自由的決定何時(shí)調(diào)用這些代碼塊,何時(shí)對(duì)某些代碼塊進(jìn)行淘汰,還可以實(shí)現(xiàn)優(yōu)先級(jí)調(diào)度。
既然是命令模式,還差一個(gè) Invoker (調(diào)用程序),即何時(shí)去觸發(fā)這些任務(wù)。結(jié)合 iOS 的技術(shù)特點(diǎn),可以監(jiān)聽(tīng) RunLoop 循環(huán)周期來(lái)實(shí)現(xiàn):
static void addRunLoopObserver() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
taskSchedulers = [NSHashTable weakObjectsHashTable];
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting | kCFRunLoopExit, true, 0xFFFFFF, runLoopObserverCallBack, NULL);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
CFRelease(observer);
});
}
然后在回調(diào)函數(shù)中進(jìn)行任務(wù)的調(diào)度。
三、策略模式
考慮到任務(wù)的淘汰策略和優(yōu)先級(jí)調(diào)度,必然需要一些高效數(shù)據(jù)結(jié)構(gòu)來(lái)支撐,為了提高處理效率,筆者直接使用了 C++ 的數(shù)據(jù)結(jié)構(gòu):deque
和priority_queue
。
因?yàn)橐獙?shí)現(xiàn)任務(wù)淘汰,所以使用deque
雙端隊(duì)列來(lái)模擬棧和隊(duì)列,而不是直接使用stack
和queue
。使用priority_queue
優(yōu)先隊(duì)列來(lái)處理自定義的優(yōu)先級(jí)調(diào)度,它的缺點(diǎn)是不能刪除低優(yōu)先級(jí)節(jié)點(diǎn),為了節(jié)約時(shí)間成本姑且夠用。
具體的策略:
- 棧:后加入的任務(wù)先執(zhí)行(可以理解為后加入的任務(wù)優(yōu)先級(jí)高),優(yōu)先淘汰先加入的任務(wù)。
- 隊(duì)列:先加入的任務(wù)先執(zhí)行(可以理解為先加入的任務(wù)優(yōu)先級(jí)高),優(yōu)先淘汰后加入的任務(wù)。
- 優(yōu)先隊(duì)列:自定義任務(wù)優(yōu)先級(jí),不支持任務(wù)淘汰。
實(shí)際上組件是推薦使用棧和隊(duì)列這兩種策略,因?yàn)椴迦牒腿〕龅臅r(shí)間復(fù)雜度是常數(shù)級(jí)的,需要定制任務(wù)的優(yōu)先級(jí)時(shí)才考慮使用優(yōu)先隊(duì)列,因?yàn)槠洳迦霃?fù)雜度是 O(logN) 的。
至此,整個(gè)組件的業(yè)務(wù)是比較清晰了,組件需要讓這三種處理方式可以自由的變動(dòng),所以采用策略模式來(lái)處理,下面是 UML 類圖:
嗯,這是個(gè)挺標(biāo)準(zhǔn)的策略模式。
四、線程安全
由于任務(wù)的調(diào)度可能在任意線程,所以必須要做好容器(棧、隊(duì)列、優(yōu)先隊(duì)列)訪問(wèn)的線程安全問(wèn)題,組件是使用pthread_mutex_t
和dispatch_once
來(lái)保證線程安全,同時(shí)筆者盡量減少臨界區(qū)來(lái)提高性能。值得注意的是,如果不會(huì)存在線程安全的代碼就不要去加鎖了。
后語(yǔ)
部分技術(shù)細(xì)節(jié)就不多說(shuō)了,組件代碼量比較少,如果感興趣可以直接看源碼。實(shí)際上這個(gè)組件的應(yīng)用場(chǎng)景并不是很多,在項(xiàng)目穩(wěn)定需要做深度的性能優(yōu)化時(shí)可能會(huì)比較需要它,并且希望使用它的人也能了解一些原理,做到胸有成竹,才能靈活的運(yùn)用。