iOS 任務(wù)調(diào)度器:為 CPU 和內(nèi)存減負(fù)

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):dequepriority_queue

因?yàn)橐獙?shí)現(xiàn)任務(wù)淘汰,所以使用deque雙端隊(duì)列來(lái)模擬棧和隊(duì)列,而不是直接使用stackqueue。使用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 類圖:

UML類圖

嗯,這是個(gè)挺標(biāo)準(zhǔn)的策略模式。

四、線程安全

由于任務(wù)的調(diào)度可能在任意線程,所以必須要做好容器(棧、隊(duì)列、優(yōu)先隊(duì)列)訪問(wèn)的線程安全問(wèn)題,組件是使用pthread_mutex_tdispatch_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)用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,415評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,104評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,884評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,647評(píng)論 6 408
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,130評(píng)論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,366評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,887評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,737評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,939評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,174評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,586評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,827評(píng)論 1 283
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,608評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,914評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容

  • iOS多線程編程 基本知識(shí) 1. 進(jìn)程(process) 進(jìn)程是指在系統(tǒng)中正在運(yùn)行的一個(gè)應(yīng)用程序,就是一段程序的執(zhí)...
    陵無(wú)山閱讀 6,096評(píng)論 1 14
  • 從哪說(shuō)起呢? 單純講多線程編程真的不知道從哪下嘴。。 不如我直接引用一個(gè)最簡(jiǎn)單的問(wèn)題,以這個(gè)作為切入點(diǎn)好了 在ma...
    Mr_Baymax閱讀 2,804評(píng)論 1 17
  • GCD調(diào)度隊(duì)列是執(zhí)行任務(wù)的強(qiáng)大工具。調(diào)度隊(duì)列允許您相對(duì)于調(diào)度者異步或者同步的執(zhí)行任意代碼塊。您能夠使用調(diào)度隊(duì)列來(lái)執(zhí)...
    坤坤同學(xué)閱讀 6,681評(píng)論 1 3
  • 本文將會(huì)從多個(gè)方面探討NSOperation類和NSOperationQueue類的相關(guān)內(nèi)容 一、簡(jiǎn)介 NSOpe...
    雅之上善若水閱讀 1,656評(píng)論 0 3
  • 前言: 最近想回顧一下多線程問(wèn)題,看到一篇文章寫(xiě)的非常詳細(xì),為了便于以后查找以及加深印象,就照著原文摘錄了下文,原...
    FM_0138閱讀 977評(píng)論 1 1