在本系列上一篇《iOS 2D Graphic(1)—— Concept 基本概念和原理》中,我們已經(jīng)了解了關(guān)于iOS 圖形圖像的基本要素葫盼。在過去相當(dāng)長的一段時間里纺座,較之于Android,優(yōu)秀順暢的UI操作體驗一直是iOS引以為豪的地方艳悔。這個不僅和iOS的UI操作線程設(shè)計機制有關(guān)悄窃,也與iOS圖形圖像上對性能這部分的深度優(yōu)化有關(guān)讥电。但是雖然Apple替我們做了很多優(yōu)化的動作,在實際開發(fā)中轧抗,如果不注意和圖形圖像相關(guān)的性能損失點恩敌,仍然會造成App的性能問題。這篇將重點關(guān)注如何處理圖形圖像的性能問題横媚。
CPU Bound vs. GPU Bound
首先纠炮,我們需要理解兩個不同的性能影響因素:CPU約束型(CPU bound) 和 GPU約束型(GPU bound)。
我們回過頭來看看上一篇中提到的一個完整的Core Animation圖形操作灯蝴,需要經(jīng)過哪幾步:
搞清楚了這個步驟恢口,那顧名思義CPU bound和GPU bound的概念,就意味著影響性能的操作可以分為兩種:前者主要集中在CPU上绽乔,也就是說對應(yīng)于上圖中的1~2步弧蝇;后者主要集中在GPU上,對應(yīng)于上圖的第3步驟折砸。
這兩種不同的場景看疗,直接決定了使用不同的方式來性能,CPU bound的場合下睦授,需要減輕CPU的壓力两芳,而把一部分工作轉(zhuǎn)交給GPU更擅長的方式去處理;而在GPU bound的場合下去枷,需要減輕GPU的壓力怖辆,把一部分工作提前交給CPU去做預(yù)處理。當(dāng)然了删顶,你要是說那CPU和GPU都很忙怎么辦竖螃?這種情況下,就可能需要你重新設(shè)計系統(tǒng)的架構(gòu)逗余,然后不斷的調(diào)試特咆,不斷的驗證了。
那么录粱,怎么區(qū)分你的App在出現(xiàn)性能問題時腻格,是CPU bound還是GPU bound呢画拾?這個時候,你需要一些工具來幫助你分析問題的根源所在菜职。最主要的工具就是Instrument青抛。
Instrument
“工欲善其事,必先利其器”
-- 《論語》
Apple提供的Instrument是一個很強大的測試平臺工具酬核,打開Instrument蜜另,你可以看到很多小工具能針對性的提供不同的功能,這里主要強調(diào)和Graphic相關(guān)的兩個:Core Animation instrument和OpenGL ES Driver instrument愁茁。
1. Core Animation Instrument
選擇Core Animation Instrument后蚕钦,在面板內(nèi)你能看到系統(tǒng)提供的默認(rèn)兩個不同的Profiler,一個是Core Animation(圖中1)鹅很,一個是Time Profiler(圖中2)。Core Animation Profiler中罪帖,你能看到最主要的一個信息就是Frame Rate促煮。這個值是判定你的App有沒有UI性能問題的主要標(biāo)準(zhǔn),一切的一切優(yōu)化目標(biāo)都是要求Frame Rate達到60幀每秒(60 fps)整袁!所以當(dāng)你看到這個值明顯低于55-60的時候菠齿,你就可以確定你的App需要優(yōu)化。
而Time Profiler則直觀的顯示了你的CPU Utilization坐昙,在這里你能看到最耗CPU時間的操作和調(diào)用绳匀。也就是說,這里是分析CPU Bound問題的切入點炸客,在這里找到最耗時間的操作疾棵,然后再去找到應(yīng)對措施。
Core Animation 一欄在右下方痹仙,還有一個非常有用的工具集合:Color Debug Options(圖中3)是尔。這里有一系列的debug選項,這是輔助你找到和GPU Bound操作相關(guān)的信息入口开仰。比較常用的選項包括:
- Color Blend Layers
這個選項的意思是拟枚,如果該區(qū)域有圖層混合的操作,則標(biāo)記成紅色众弓,混合的圖層越多恩溅,顏色越深,否則為綠色谓娃。如果你看到你的界面有大量的深紅色區(qū)域脚乡,則表示你當(dāng)前的UI可能做顏色混合的操作會比較影響你的性能。圖層混合主要是因為layer的透明度傻粘。
屏幕上每一個點都是一個像素每窖,像素有R帮掉、G、B三種顏色構(gòu)成窒典,另外還有一個alpha值蟆炊。如果某一塊區(qū)域上覆蓋了多個layer,最后的顯示效果受到這些layer的共同混合(即Blending)的結(jié)果瀑志。舉個例子涩搓,上層是藍色(RGB=0,0,1),透明度A為50%,下層是紅色(RGB=1,0,0)劈猪, 不透明昧甘。那么最終的顯示效果是紫色(RGB=0.5,0,0.5)。這種顏色的混合需要消耗一定的GPU資源战得,不透明圖層越多充边,blending消耗越大。但是如果對于某一層layer的透明度設(shè)置為100%(不透明)常侦,則GPU會忽略下面所有的layer浇冰,從而節(jié)約了很多不必要的開銷。
- Color Hits Green and Misses Red
這個選項的意思是聋亡,Core Animation是否有使用緩存進行繪制肘习。如果成功使用了緩存,則標(biāo)記成綠色坡倔,如果當(dāng)前區(qū)域有緩存漂佩,但是當(dāng)前緩存失效了,則標(biāo)記成紅色罪塔。這個選項主要和光柵化(Rasterization)相關(guān)投蝉。光柵化是將一個layer以及它的sub layer預(yù)先完成混合和渲染,并生成一個靜態(tài)的位圖(bitmap)垢袱,然后加入緩存中。后續(xù)如果這個渲染結(jié)果不再變化咳榜,則可以復(fù)用這個緩存的位圖直接繪制在屏幕上爽锥。這對于屏幕上有大量靜態(tài)內(nèi)容時,是很好的優(yōu)化氯夷。后文將詳細介紹Rasterization的部分。
- Color Copied Images
這個選項主要檢查是否有使用不正確圖片格式。iOS推薦的圖片格式是不帶Alpha通道的PNG和JPG格式雇毫。若是其它GPU不支持的色彩格式的圖片則會標(biāo)記為青色棚放,此時只能先由CPU來進行轉(zhuǎn)換處理飘蚯,然后將處理完的圖片交給GPU局骤。青色是我們需要避免的峦甩,因為CPU實時進行處理圖片可能會阻塞主線程凯傲。
- Color Offscreen-Rendered Yellow
這個選項是用來檢查在當(dāng)前UI中哪些區(qū)域的繪制必須使用離屏渲染(Offscreen Rendered)泣洞。離屏渲染,顧名思義腿宰,是指當(dāng)前屏幕的繪制操作并不是直接發(fā)生在當(dāng)前的幀緩沖區(qū)內(nèi)吃度,而是會合并/渲染圖層樹的一部分到一個新的緩沖區(qū)椿每,然后該緩沖區(qū)被渲染到屏幕上间护。產(chǎn)生離屏渲染的原因有很多汁尺,它可以被 Core Animation 自動觸發(fā),也可以被應(yīng)用程序強制觸發(fā)狼荞。
一般情況下相味,你需要避免離屏渲染攻走,因為這是很大的消耗昔搂。直接將圖層合成到幀的緩沖區(qū)中(在屏幕上)比先創(chuàng)建屏幕外緩沖區(qū)摘符,然后渲染到紋理中,最后將結(jié)果渲染到幀的緩沖區(qū)中要廉價很多带族。因為這其中涉及兩次昂貴的環(huán)境轉(zhuǎn)換(轉(zhuǎn)換環(huán)境到屏幕外緩沖區(qū)蝙砌,然后轉(zhuǎn)換環(huán)境到幀緩沖區(qū))择克。
關(guān)于離屏渲染肚邢,如果展開來說骡湖,又是一個相對而言比較復(fù)雜的話題,我將在下一篇文章《iOS 2D Graphic(3)—— Offscreen Rendering離屏渲染》中詳細討論和總結(jié)這部分的內(nèi)容换途。
除了以上常用的選項外军拟,還有一個你可能也會用的到的選項:
- Color Misaligned Images
這個選項檢查了圖片是否被放縮,像素是否對齊肾档。被放縮的圖片會被標(biāo)記為黃色,像素不對齊則會標(biāo)注為紫色怒见。
黃色的場景比較多見遣耍,比如你將一個200200的圖片塞到了一個100100的UIImageView里舵变,那么圖片自然就會被縮放纪隙;紫色的Misaligned Image場景一般很難見到,主要表示要繪制的點無法直接映射到頻幕上的像素點麸拄,此時系統(tǒng)需要對相鄰的像素點做anti-aliasing反鋸齒計算。通常這種問題出在對某些View的Frame重新計算和設(shè)置時產(chǎn)生的秆吵。
2. OpenGL ES Driver Instrument
相對于前面介紹的Core Animation Profiler來說,OpenGL ES Driver的側(cè)重點非常集中毙芜,它的目標(biāo)很明確腋粥,就是針對GPU的使用進行檢測隘冲。你可以在右下角的Panel里選擇感興趣的指標(biāo)選項展辞,然后在下方的列表里觀察數(shù)值洽腺。一般而言蘸朋,Device Utilization度液,Renderer Utilization和Core Animation Frame Per Second是你應(yīng)該要關(guān)注的首選項,這些指標(biāo)將直接反應(yīng)當(dāng)前GPU的使用率霹购。如果你發(fā)現(xiàn)GPU的使用率明顯偏高齐疙,那么很明顯你面臨了GPU Bound的情況贞奋。然后,你可以再用前文介紹的Core Animation Profiler勾缭,使用Color Debug去分析具體可能的原因俩由。
優(yōu)化策略和方法
那么當(dāng)我們已經(jīng)知道了Graphic的性能大致上分為CPU Bound和GPU Bound兩種類型兜畸,并且我們也知道可以使用Instrument來分析具體的源頭所在膳叨,那么我們能采取哪些措施和策略來解決問題呢菲嘴?
為了能夠清晰的闡述這些方法,我們需要思考下iOS中Animation的實現(xiàn)步驟健田,因為即使是非Animation的靜態(tài)頁面妓局,我們也可以看作是只有一幀畫面的特殊Animation好爬,所以分析優(yōu)化策略,可以對照著Animation的核心步驟來進行穆桂。
我們來看上一篇中出現(xiàn)過的描述一個完整的Animation的步驟的圖:
1. 創(chuàng)建Animation,并更新視圖驼侠;
** 2. 計算Animation必要的數(shù)據(jù)并提交動畫,具體分為以下四步:**
1) 布局:【CPU & I/O bound】
?Often has expensive view creation and layer graph management
?May need to do expensive data lookup
?May block on I/O or work done in another thread or process
2) 顯示:【 CPU & Memory bound】
? -drawRect 調(diào)用在此進行
? String drawing or other expensive drawing
3)準(zhǔn)備:【CPU bound】
? 圖像解碼轉(zhuǎn)換,額外的動畫操作腻菇;
4)提交:【CPU bound】
打包Layers及動畫參數(shù)筹吐,這是遞歸操作嘉竟,如果layer層級非常復(fù)雜舍扰,則代價比較昂貴。隨后Layers通過IPC被送到Render Server** 3. Render Server進行最終渲染**
了解這上面這3大步驟个束,將整個Animation從準(zhǔn)備到完成的過程分成了兩個清晰的階段來做性能評估:“動畫的響應(yīng)速度”(Responsiveness)和“動畫的平滑性”(Smoothness)茬底,其中前者對應(yīng)于1,2步捶枢,后者對應(yīng)于第3步烂叔。于是,我們討論優(yōu)化策略都將在這兩個大方向上做文章逢防。
(1)響應(yīng)速度優(yōu)化
減少初始化(Do less set up)
? 盡量避免在layout期間讓CPU做重度運算,或者其它阻塞操作局嘁;
? 數(shù)據(jù)上盡量使用in-memory caches悦昵;
? 如果需要做數(shù)據(jù)庫查詢寡痰,盡可能確保數(shù)據(jù)庫已經(jīng)對高性能要求的部分提前做了正確的索引氓癌;
? 盡可能重用cell和view;
? 在drawRect之外做初始化疲迂,并且全局只初始化一次然后復(fù)用。例如避免CGColors, CGPaths, clipShapes的重復(fù)創(chuàng)建腰池,只初始化一次然后復(fù)用減少繪制(Reduce drawing)
-
只重繪變化的部分
? 第一黃金原則:“避免使用drawRect:”:
? 盡可能使用CALayer的屬性,而避免使用DrawRect重繪奏属。經(jīng)典案例就是設(shè)置背景顏色囱皿,使用backgroundColor而不是UIColor setFill:// bad
-(void)drawRect:(CGRect)rect {
[[UIColor redColor] setFill];
UIRectFill([self bounds]);
}
// good
[myView setBackgroundColor:[UIColor redColor]];
? 如果你必須重寫drawRect:
,則盡可能調(diào)用setNeedsDisplayInRect:
而不是-setNeedsDisplay
齿兔。這將讓Core Graphic自動為你的drawRect:代碼創(chuàng)建clipRect:而無需改動任何其它代碼,然后在繪制時自動忽略clipRect之外的部分组砚,它能夠顯著提高性能;
? 如果你無法提前預(yù)知更新的視圖范圍盆偿,那么只在需要的時候調(diào)用-setNeedsDisplay
-
**正確處理圖片(Be smart with images) **
? 使用UIImageView而不是直接繪制image。這是因為此時blending是發(fā)生在GPU中求橄,而不是提前在CPU中混合,同時Core Animation能夠高效的從UIImage(CGImage)中取出bitmap繪制到View中涵亏,然后自動的將bitmap進行緩存(Built-in bitmap caching)// bad
- (void)drawRect:(CGRect)rect
{
[self.image drawInRect:[self bounds]
blendMode:kCGBlendModeNormal alpha:1.0];
}
// good
myView.layer.contents = (id)[self.image CGImage];
? 盡可能使用沒有Alpha通道的圖片。這樣能夠最大限度的減少圖層混合裆悄。
? 使用正確的圖片格式。Apple強調(diào)在iOS中艾君,PNG和JPEG是Apple官方推薦的格式,Xcode能夠針對這兩種格式做額外的優(yōu)化虹茶,不要使用其它格式的圖片比如TIFF等董济;
? 對UI上的縮略圖使用單獨的Image虏肾,而不是將原圖縮放到小圖里。這里對縮略圖有一條規(guī)則“如果使用PNG能夠在失真允許的情況下獲得足夠好的壓縮效果吹埠,則使用PNG!”
? 關(guān)于緩存:
[UIImage imageNamed:]
將自動緩存在可刪除的內(nèi)存里(purgeable memory ),同時保存索引在 image table中以便復(fù)用做个;而[UIImage imageWithContentsOfFile:]
不會居暖!
? 當(dāng)你設(shè)置layer contents的時候,所有作為layer backstore的CGImages都會被緩存;
? 如果你手動創(chuàng)建一個CGImage钞澳,則打開kCGImageSourceShouldCache顯式地建立緩存;
? 通常情況下兰吟,不要自己手動建立Image的緩存 - (void)drawRect:(CGRect)rect
關(guān)于圖片的這部分讽膏,我們可以利用上文里提到的Instrument中的“Color Copied Image”debug選項,來檢測是否有額外的圖片操作是可以避免的拄丰。
-
異步繪制(Draw asynchronously)
這是一個比較少見的方法,但是Apple仍然提供了這一選擇俐末。當(dāng)你使用-drawInContext:
為CALayer提供Content的時候料按,Core Animation 可以通過兩種方式來進行渲染:
■ 普通繪圖(Normal drawing)會同步的阻塞當(dāng)前線程直至完成;
■ 異步繪圖(Asynchronous drawing) 會將繪圖命令發(fā)送到后臺來完成渲染
異步方式默認(rèn)是關(guān)閉的卓箫,因為它并不總是能夠提高性能。通常當(dāng)你需要在一個單一的很大的view context區(qū)域內(nèi)完成images, rectangles, shadings等等的繪制會比較有效烹卒。如果想使用異步繪制闷盔,只需要在當(dāng)前的繪制context layer內(nèi)調(diào)用:
myView.layer.drawsAsynchronously = YES;
同樣的,一定要測試才決定是否使用旅急。
-
預(yù)處理數(shù)據(jù)(Speculative preparation )
提前對將要顯示的內(nèi)容進行計算查詢逢勾,然后將數(shù)據(jù)放置在緩沖區(qū)里以便后面使用。一種經(jīng)典的場景就是藐吮,一個長長的列表顯示網(wǎng)絡(luò)的圖片溺拱,可以預(yù)先把后續(xù)需要加載的圖片的下載,解碼和繪制工作放在后臺的線程里進行異步處理谣辞,然后等到需要顯示時再在同步到UI上迫摔。關(guān)于這一點,下文第三部分將會重點強調(diào)這種并發(fā)的繪圖操作的基本方式泥从。(注意句占,這里的異步是數(shù)據(jù)的異步,而不是上文rendering本身的異步)
(2)動畫平滑性優(yōu)化
Animation的Step3發(fā)生在Render Server和GPU上:Render Server是以per layer, per frame的方式工作在CPU上躯嫉,而Rendering本身是發(fā)生在GPU上纱烘。所以這一步同樣是CPU Bound和GPU Bound同時存在。而影響動畫的平滑性和敬,更多的是GPU的負(fù)擔(dān)過重凹炸,使用OpenGL ES instrument,查看Device Utilization的使用率昼弟,能夠很好的指導(dǎo)你是否發(fā)生了GPU bound issue啤它。記住我們一切一切的目標(biāo),就是60 fps!
減少Blending
使用Instrument的(Color Blended Layers)選項可以幫助我們定位此項優(yōu)化操作的必要性变骡。找到圖層復(fù)雜的部分:
? 嘗試進行視圖層級精簡离赫;
? 盡可能的使用非透明圖層/圖片。因為Alpha通道的混合blending比繪制完全不透明的圖層要慢很多嘗試扁平化Flattening
注意這里的扁平化不是說iOS 7倡導(dǎo)的UI設(shè)計的扁平化塌碌,而是說使用一些技術(shù)將立體的多層級Layer進行融合和渲染后繪制到單一視圖上渊胸。如果你遇到的是GPU Bound的情景,F(xiàn)lattening view hierarchy 能夠顯著的幫助提高性能台妆。
? 使用離屏緩沖區(qū)對內(nèi)容進行flatten(即將當(dāng)前視圖內(nèi)容刷到單獨的Image data中)
? 盡可能縮短繪制圖像時的CGPath翎猛;當(dāng)需要畫一個很大很復(fù)雜的CGPath時,盡可能只重繪你更新的那一部分接剩,而不是整個path切厘,而將其它的部分Flatten到bitmap中;
? 常見的Flatten方法:
1)Rasterization
對于一個單一的視圖懊缺,如果視圖內(nèi)的Layer內(nèi)容層級很復(fù)雜疫稿,但是內(nèi)容(層級樹和數(shù)據(jù)等)本身并不變化(或極少變化),則可以對base layer使用Rasterization(光柵化)鹃两。Rasterization其實是將當(dāng)前l(fā)ayer及其所有sub layer的圖像全部混合繪制到一個獨立的bitmap中緩存起來遗座,后續(xù)的顯示將直接使用這個緩存的圖片在硬件中進行組合而不再重新渲染。使用光柵化的方法很簡單俊扳,在你需要的base layer上調(diào)用:
baseView.layer.rasterizationScale = [UIScreen mainScreen].scale
baseView.layer.shouldRasterize = true
但是使用rasterization需要注意:
■ 有限的緩存空間途蒋,大約是2.5x 屏幕的大小馋记;
■ 內(nèi)容一旦變化碎绎,緩存立即失效;
■ 如果當(dāng)前顯示超過100ms沒有使用到當(dāng)前緩存抗果,則Rasterized images會被清除筋帖;
■ 任何你使用shouldRasterize
的地方都必須提前設(shè)定rasterizationScale
;
■ Rasterization發(fā)生在mask被應(yīng)用之前冤馏,也就是說對于有mask的view日麸,緩存的是被遮罩之前的內(nèi)容;
你可能會想“Rasterization簡直就是神器啊逮光,那必須得用代箭!” 別急,Rasterization并不是萬能藥涕刚!
實際上這是一個 time vs. memory trade-off嗡综,用存儲空間交換渲染時間;同時是將部分Render server的工作轉(zhuǎn)移到了CPU上杜漠;又由于在更新緩存內(nèi)容時极景,有額外的offsceen操作察净,這些問題決定了太多的不合適的Rasterization會傷害到“響應(yīng)性能responsiveness”,因此盼樟,對于非GPU-bound的場景氢卡,可能會適得其反!所以在做Rasterization之前晨缴,請確保在Core Animation instrument中使用 “Color cache hits/misses” 選項來測試你的想法译秦。
2)Seperate Image
對于視圖內(nèi)的Layer或者內(nèi)容變化頻繁的場景,rasterization就不再適用了击碗,因為如果做了Rasterization筑悴,但是緩存的cache沒有被命中,將會造成比不緩存還要糟糕的消耗稍途!這時可以將整個View的內(nèi)容使用renderInContext繪制在一個獨立的圖片緩沖區(qū)內(nèi)雷猪,然后在后續(xù)的繪制過程中,直接使用這張圖片晰房,而不是每次都重復(fù)繪制整個View層級。在后面的iPaint Demo里射沟,我們將使用這個技術(shù)殊者。
減少離屏渲染Offscreen Rendering
前文已經(jīng)說過,離屏渲染因為會使得GPU從當(dāng)前幀緩沖區(qū)之外額外的緩沖區(qū)進行繪制操作验夯,然后切換環(huán)境把內(nèi)容切換回當(dāng)前幀緩沖區(qū)猖吴,會對性能造成非常大的影響。在Scroll/table view中幾乎大部分常見的性能問題都是因為存在過多的離屏渲染造成的挥转。因此在App中要盡可能的避免或者減少離屏渲染海蔽。造成離屏渲染的原因有很多,masking是最有可能的一個绑谣,陰影也是党窜,而上文提到的rasterization也會至少產(chǎn)生一次offscreen rendering,但是并不能說rasterization就不能用借宵,關(guān)鍵還是看使用的方式和場合幌衣。除此之外,還有其它一些情況會產(chǎn)生離屏渲染壤玫。本系列下一篇文章豁护,將詳細闡述離屏渲染的問題。
? 慎重使用陰影(Drop shadows)
這里單獨強調(diào)一下陰影的問題欲间。陰影是非常昂貴的渲染操作楚里,盡可能的減少陰影的設(shè)計,如果必須使用陰影猎贴,則可以通過以下的方式來提高性能:
■ 使用shadowPath來預(yù)先定義陰影的形狀班缎,而不是讓layer自己來判斷陰影的區(qū)域:
view.layer.shadowColor = [UIColor grayColor].CGColor;
view.layer.shadowOffset = CGSizeMake(5, 5);
view.layer.shadowOpacity = 0.6;
// Very good to set the shadowpath explicitly
view.layer.shadowPath = [[UIBezierPath bezierPathWithRect:view.bounds] CGPath];
■ 對于靜態(tài)的陰影視圖蝴光,在使用上述方式生成陰影之后,再使用rasterization將陰影直接flatten成靜態(tài)位圖吝梅,這樣后續(xù)的顯示不用再重復(fù)繪制陰影虱疏,而是直接使用緩存顯示。
view.layer.rasterizationScale = [UIScreen mainScreen].scale;
view.layer.shouldRasterize = YES;
在Core Animation Instrument中苏携,可以通過“Color Offscreen-Rendered Yellow”選項來識別當(dāng)前視圖中發(fā)生offscreen rendering的區(qū)域做瞪。認(rèn)真對待滑動Scrolling
? 第一黃金原則:“重用cells and views”;
? 盡可能的減少layout和drawing的時間右冻,也就是盡可能的減少視圖層次復(fù)雜度和自定義drawRect:的消耗装蓬;
? 同樣的,預(yù)先加載將要顯示的視圖數(shù)據(jù)纱扭,減少同步加載時間牍帚;
? 在視圖結(jié)構(gòu)不可避免的復(fù)雜情況時,嘗試Flatten乳蛾,但是需要測試驗證暗赶;
? 及時取消已經(jīng)滑出屏幕的cell的相關(guān)計算和繪制操作(見后文)
(3)并發(fā)
除了上面提到的具有針對性的2大部分內(nèi)容,凡是提到“性能”兩個字肃叶,有經(jīng)驗的人都會自然而然的想到另外兩個字“并發(fā)”蹂随。并發(fā)實際上是充分利用了多核CPU的并行處理能力,讓繁重的處理操作和界面響應(yīng)操作能夠分別在不同的線程里并行的完成因惭,讓界面的操作能夠及時被響應(yīng)而不被其它的耗時處理所堵塞岳锁。
根據(jù)長時高計算的操作的類別不同,可以將并發(fā)的方式分為“數(shù)據(jù)并發(fā)”和“繪圖并發(fā)”:
-
并發(fā)數(shù)據(jù)處理(Process Data Concurrently)
我們知道蹦魔,iOS的所有UI事件響應(yīng)(Touch Event)都是在主線程執(zhí)行的激率,而主線程自動維護一個隊列(Main Queue),所有的事件都會在隊列里排隊按照順序執(zhí)行勿决。簡單情況下乒躺,我們可能會將數(shù)據(jù)處理操作都放在主線程里去做,這樣很直接很簡單低缩。但是在某些情況下聪蘸,如果數(shù)據(jù)處理非常耗時,那么主線程會一直被阻塞在數(shù)據(jù)處理上表制,而UI的操作事件一直在排隊無法執(zhí)行健爬,從而造成界面的卡頓現(xiàn)象:
如上圖所示,Touch Event必須等待數(shù)據(jù)下載結(jié)束并處理完成后才能執(zhí)行么介。這個時候娜遵,為了提高UI的性能,可以手動創(chuàng)建一個NSOperationQueue(你也可以使用async dispath_queue)壤短,將數(shù)據(jù)處理放到后臺線程的隊列里设拟,當(dāng)數(shù)據(jù)處理完成后慨仿,再將結(jié)果通過主線程更新到UI上:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue setName:@”Data Processing Queue”];
[queue addOperationWithBlock:^{ processStock(someStock); }];
[queue addOperationWithBlock:^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
updateUI(someStock);
}];
}];
-
并發(fā)繪圖(Draw Concurrently)
同樣的道理可以類推到繪圖操作上。常規(guī)情況纳胧,我們將所有的繪制都放在drawRect:
里镰吆,而drawRect:
是只能執(zhí)行在主線程上。如果在drawRect:
做了大量的繪制操作跑慕,那么主線程會一直被阻塞万皿,導(dǎo)致Touch Event不能被響應(yīng)。
// Not so good if image drawing is very expensive
- (void)drawRect:(CGRect)rect {
[[UIColor greenColor] set];
UIRectFill([self bounds]);
[anImage drawAtPoint:CGPointZero];
}
這個時候核行,我們可以把繪制工作放在獨立的線程隊列里牢硅,然后把繪制結(jié)果生成一張圖片,在通過主線程芝雪,把圖片繪制在drawRect:
里達到同樣的效果减余。
// Maybe good if drawing is very expensive
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue setName:@”Drawing Queue”];
[queue addOperationWithBlock:^{
UIImage *image = [self renderInImageOfSize:size]
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[imageView setImage:image];
}];
}];- (UIImage *)renderInImageOfSize:(CGSize)size { UIGraphicsBeginImageContextWithOptions(size, NO, 0); [[UIColor greenColor] set]; UIRectFill([self bounds]); [anImage drawAtPoint:CGPointZero]; UIImage *i = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return i; }
在這種情況下,只需要注意以下兩點:
?Drawing APIs在任何隊列里調(diào)用都是安全的惩系,不一定非得是主線程隊列位岔,但是必須確保begin和end在同一個上下文中(同一個operation中)
?最終必須在Main Queue中更新圖片
-
適時取消并發(fā)操作(Canceling concurrent operations)
雖然有了并發(fā)的隊列操作,也并不是說就不管不問放任Queue中的task去執(zhí)行了堡牡,因為最終你還是需要更新UI的抒抬,為了讓UI能夠更快的響應(yīng)用戶的執(zhí)行,我們需要讓這個隊列去更快更多去執(zhí)行操作悴侵。因此在任務(wù)非常繁重的情況下,適時地取消隊列中已經(jīng)失效的操作是非常必要的拭嫁。
舉一個例子可免,如果你有一個table,每一個cell需要動態(tài)的顯示一些圖表(比如股票走勢圖做粤,或者是從網(wǎng)絡(luò)上下載更多的小圖片)浇借,按照我們之前的建議,你已經(jīng)把每一個cell的視圖更新和繪制都放在了queue中怕品,然后讓這些queue中的task異步地去更新每一個cell妇垢,這很好,table能夠繼續(xù)響應(yīng)用戶的滑動了肉康,但是當(dāng)用戶已經(jīng)選擇了一個具體的cell闯估,不再對其它的內(nèi)容感興趣時,queue中還在繼續(xù)執(zhí)行很多計算或者是下載吼和,當(dāng)用戶再添加新的操作到queue中涨薪,仍然還在等待那些他已經(jīng)不感興趣的cell內(nèi)容的更新。
顯然這是不必要的炫乓,這個時候刚夺,我們就可以取消隊列中的操作來減輕queue的壓力献丑,使得它能夠更快的響應(yīng)后續(xù)其它的操作。
iOS提供了3種相關(guān)的取消操作API:
- [NSOperationQueue cancelAllOperations]
- [NSOperationQueue cancel]
- [NSOperation isCancelled]
? 使用[NSOperationQueue cancelAllOperations]
取消隊列中的所有操作侠姑;
? 使用[NSOperation cancel]
取消單個操作创橄;
注意上述兩種方式都無法取消正在執(zhí)行的operation,如果想讓執(zhí)行中的operation被中斷莽红,必須使用[NSOperation isCancelled]
來檢測是否被取消:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *op = [[NSBlockOperation alloc] init];
__weak NSBlockOperation *weakOp = op;
[op addExecutionBlock:^{
for (int i = 0; i < 10000; i++) {
if ([weakOp isCancelled]) break;
processData(data[i]);
}
}];
[queue addOperation:op];
? 如果你在table view中使用了operation妥畏,可以在- tableView:didEndDisplayingCell:
中取消cell-related work。
2D Graphic Optimization Demo
在Apple的 **WWDC2012 Session 506 - "optimazing 2D graphics and animation performance" **上船老,有一個繪圖Demo叫iPaint咖熟,很好的示例了多種不同的優(yōu)化方法帶來的效果。但是這個Demo并沒有Code可以下載柳畔。于是我就模仿演講者在Demo過程中提到的內(nèi)容和屏幕上能夠看到的一些片段的Code馍管,自己重新寫了一個Demo。你可以在Github上下載 iPaint Demo
因為篇幅的原因薪韩,我們只來看這個Demo的一個具體示例确沸,你可以看到不同的開關(guān)對最終繪圖的性能效果影響。
首先俘陷,在打開“Calculate Dirty Rect”開關(guān)計算更新區(qū)域并使用- setNeedDisplayInRect
之前罗捎,由于打開了Flatten Long Path選項,在繪畫路徑長到一定程度的時候拉盾,會將整個視圖扁平化到一個單獨的Image中桨菜,在第一個版本的實現(xiàn)上,我們可以看到效果非常差捉偏,F(xiàn)PS直接掉到10以下:
用Instrument查看OpenGL ES Driver倒得,發(fā)現(xiàn)GPU的壓力并不是很大,但是幀率很低:
使用Time Profiler查看夭禽,發(fā)現(xiàn)CPU絕大部分的開銷都在DrawImage上面:
那么在不改變當(dāng)前Drawing策略的前提下霞掺,只是更改一個地方,打開Calculate Dirty Rect開關(guān)讹躯,每次drawInRect只是重新繪制當(dāng)前更新的一小部分區(qū)域(紅框內(nèi)的區(qū)域)菩彬,可以看到性能明顯提升:
但是,可以看到潮梯,幀率仍然不夠理想骗灶,離目標(biāo)60 fps還差得很遠。觀察CPU的使用秉馏,還是因為在Flatten Path的時候繪制圖像的時間太長矿卑,可以在Time Profiler中,CGContextDrawImage仍然花了大部分時間沃饶。如果這個時候母廷,關(guān)閉Flatten Path開關(guān)轻黑,幀率立即就上來了。
可是Flatten Path是有用的琴昆,否則隨著Path的不斷增長氓鄙,幀率還是會下降,所以關(guān)鍵問題是在Flatten的時候业舍,如何降低繪圖的開銷抖拦。這個問題一直困擾了我很久,因為WWDC視頻的代碼并沒有顯示作者是如何做Flatten的舷暮。(這個地方是不能用rasterization的态罪,因為整個Layer的圖層在不斷變化,使用光柵是沒有用的下面,必須手動將圖層上的物件繪制到一張圖片上)
最后复颈,我終于發(fā)現(xiàn)了問題所在:因為在UIKit和Core Graphic的坐標(biāo)系問題,直接使用UIGraphicsGetImageFromCurrentImageContext()
得到的圖片是顛倒的沥割,所以我原本使用的是一個Flip()函數(shù)耗啦,將CoreImage又重新繪制一次,這樣顛倒2次就得到了正確的圖片机杜≈慕玻可是這樣會造成極大的性能損失,前面Time Profiler中檢測到的CPU損耗原因就在此椒拗,于是我想到了額外的方法似将,是在drawRect中,將原先得到的顛倒的flatten image蚀苛,使用Core Animation的轉(zhuǎn)換矩陣CTM在验,直接將圖片反轉(zhuǎn)。只此改動枉阵,幀率立馬到58左右译红!
所以從這點上预茄,我們也許可以在上文中的優(yōu)化策略中再增加一條:
- 盡量使用CTM變換矩陣來直接操作圖形兴溜,而不要自己使用額外的計算和繪圖方式
2016.8.12 補充優(yōu)化內(nèi)容:
關(guān)于CAShapeLayer和CALayer:
CAShapeLayer是一個通過矢量圖形而不是bitmap來繪制的圖層子類。你指定諸如顏色和線寬等屬性耻陕,用CGPath來定義想要繪制的圖形拙徽,最后CAShapeLayer就自動渲染出來了。當(dāng)然诗宣,你也可以用Core Graphics直接向原始的CALyer的內(nèi)容中繪制一個路徑膘怕,相比直下,使用CAShapeLayer有以下一些優(yōu)點:
- 渲染快速召庞。CAShapeLayer使用了硬件加速岛心,繪制同一圖形會比用Core Graphics快很多来破。
- 高效使用內(nèi)存。一個CAShapeLayer不需要像普通CALayer一樣創(chuàng)建一個后備存儲忘古,所以無論有多大徘禁,都不會占用太多的內(nèi)存。
- 不會被圖層邊界剪裁掉髓堪。一個CAShapeLayer可以在邊界之外繪制送朱。你的圖層路徑不會像在使用Core Graphics的普通CALayer一樣被剪裁掉。
- 不會出現(xiàn)像素化干旁。當(dāng)你給CAShapeLayer做3D變換時驶沼,它不像一個有寄宿圖的普通圖層一樣變得像素化。
總結(jié)
到此争群,此文中提到的各種優(yōu)化場景和策略已經(jīng)能夠涵蓋絕大多數(shù)情況下的問題回怜,但是這些都是通用對策,并不是萬能鑰匙,只是給我們在遇到問題時提供對策的思路哎迄,至于真正的具體實施方案霞溪,對于每一個具體問題都是不同,我們需要不斷的測試和調(diào)試來找到最佳的方案抹凳。
最后,用Apple WWDC上的兩張圖來簡單總結(jié)一下遇到Graphic Performance問題的對策:
希望此文能幫助到你伦腐!
部分參考:
WWDC2012 Session 211 - Building Concurrent User Interfaces on iOS
WWDC2012 Session 238 - iOS App Performance Graphics and Animation
WWDC2012 Session 506 - Optimizing 2D Graphics and Animation Performance
WWDC2014 Session 419 - Advanced Graphics and Animation Performance
Designing for iOS: Graphics and Performance
Mastering UIKit Performance
iOS 保持界面流暢的技巧
When should I set layer.shouldRasterize to YES
WWDC心得與延伸:iOS圖形性能
UIKit性能調(diào)優(yōu)實戰(zhàn)講解
Layer Trees vs. Flat Drawing – Graphics Performance Across iOS Device Generations
2016.7.20 完稿于南京