在本系列上一篇《iOS 2D Graphic(1)—— Concept 基本概念和原理》中,我們已經了解了關于iOS 圖形圖像的基本要素。在過去相當長的一段時間里,較之于Android,優秀順暢的UI操作體驗一直是iOS引以為豪的地方。這個不僅和iOS的UI操作線程設計機制有關,也與iOS圖形圖像上對性能這部分的深度優化有關。但是雖然Apple替我們做了很多優化的動作,在實際開發中,如果不注意和圖形圖像相關的性能損失點,仍然會造成App的性能問題。這篇將重點關注如何處理圖形圖像的性能問題。
CPU Bound vs. GPU Bound
首先,我們需要理解兩個不同的性能影響因素:CPU約束型(CPU bound) 和 GPU約束型(GPU bound)。
我們回過頭來看看上一篇中提到的一個完整的Core Animation圖形操作,需要經過哪幾步:
搞清楚了這個步驟,那顧名思義CPU bound和GPU bound的概念,就意味著影響性能的操作可以分為兩種:前者主要集中在CPU上,也就是說對應于上圖中的1~2步;后者主要集中在GPU上,對應于上圖的第3步驟。
這兩種不同的場景,直接決定了使用不同的方式來性能,CPU bound的場合下,需要減輕CPU的壓力,而把一部分工作轉交給GPU更擅長的方式去處理;而在GPU bound的場合下,需要減輕GPU的壓力,把一部分工作提前交給CPU去做預處理。當然了,你要是說那CPU和GPU都很忙怎么辦?這種情況下,就可能需要你重新設計系統的架構,然后不斷的調試,不斷的驗證了。
那么,怎么區分你的App在出現性能問題時,是CPU bound還是GPU bound呢?這個時候,你需要一些工具來幫助你分析問題的根源所在。最主要的工具就是Instrument。
Instrument
“工欲善其事,必先利其器”
-- 《論語》
Apple提供的Instrument是一個很強大的測試平臺工具,打開Instrument,你可以看到很多小工具能針對性的提供不同的功能,這里主要強調和Graphic相關的兩個:Core Animation instrument和OpenGL ES Driver instrument。
1. Core Animation Instrument
選擇Core Animation Instrument后,在面板內你能看到系統提供的默認兩個不同的Profiler,一個是Core Animation(圖中1),一個是Time Profiler(圖中2)。Core Animation Profiler中,你能看到最主要的一個信息就是Frame Rate。這個值是判定你的App有沒有UI性能問題的主要標準,一切的一切優化目標都是要求Frame Rate達到60幀每秒(60 fps)!所以當你看到這個值明顯低于55-60的時候,你就可以確定你的App需要優化。
而Time Profiler則直觀的顯示了你的CPU Utilization,在這里你能看到最耗CPU時間的操作和調用。也就是說,這里是分析CPU Bound問題的切入點,在這里找到最耗時間的操作,然后再去找到應對措施。
Core Animation 一欄在右下方,還有一個非常有用的工具集合:Color Debug Options(圖中3)。這里有一系列的debug選項,這是輔助你找到和GPU Bound操作相關的信息入口。比較常用的選項包括:
- Color Blend Layers
這個選項的意思是,如果該區域有圖層混合的操作,則標記成紅色,混合的圖層越多,顏色越深,否則為綠色。如果你看到你的界面有大量的深紅色區域,則表示你當前的UI可能做顏色混合的操作會比較影響你的性能。圖層混合主要是因為layer的透明度。
屏幕上每一個點都是一個像素,像素有R、G、B三種顏色構成,另外還有一個alpha值。如果某一塊區域上覆蓋了多個layer,最后的顯示效果受到這些layer的共同混合(即Blending)的結果。舉個例子,上層是藍色(RGB=0,0,1),透明度A為50%,下層是紅色(RGB=1,0,0), 不透明。那么最終的顯示效果是紫色(RGB=0.5,0,0.5)。這種顏色的混合需要消耗一定的GPU資源,不透明圖層越多,blending消耗越大。但是如果對于某一層layer的透明度設置為100%(不透明),則GPU會忽略下面所有的layer,從而節約了很多不必要的開銷。
- Color Hits Green and Misses Red
這個選項的意思是,Core Animation是否有使用緩存進行繪制。如果成功使用了緩存,則標記成綠色,如果當前區域有緩存,但是當前緩存失效了,則標記成紅色。這個選項主要和光柵化(Rasterization)相關。光柵化是將一個layer以及它的sub layer預先完成混合和渲染,并生成一個靜態的位圖(bitmap),然后加入緩存中。后續如果這個渲染結果不再變化,則可以復用這個緩存的位圖直接繪制在屏幕上。這對于屏幕上有大量靜態內容時,是很好的優化。后文將詳細介紹Rasterization的部分。
- Color Copied Images
這個選項主要檢查是否有使用不正確圖片格式。iOS推薦的圖片格式是不帶Alpha通道的PNG和JPG格式。若是其它GPU不支持的色彩格式的圖片則會標記為青色,此時只能先由CPU來進行轉換處理,然后將處理完的圖片交給GPU。青色是我們需要避免的,因為CPU實時進行處理圖片可能會阻塞主線程。
- Color Offscreen-Rendered Yellow
這個選項是用來檢查在當前UI中哪些區域的繪制必須使用離屏渲染(Offscreen Rendered)。離屏渲染,顧名思義,是指當前屏幕的繪制操作并不是直接發生在當前的幀緩沖區內,而是會合并/渲染圖層樹的一部分到一個新的緩沖區,然后該緩沖區被渲染到屏幕上。產生離屏渲染的原因有很多,它可以被 Core Animation 自動觸發,也可以被應用程序強制觸發。
一般情況下,你需要避免離屏渲染,因為這是很大的消耗。直接將圖層合成到幀的緩沖區中(在屏幕上)比先創建屏幕外緩沖區,然后渲染到紋理中,最后將結果渲染到幀的緩沖區中要廉價很多。因為這其中涉及兩次昂貴的環境轉換(轉換環境到屏幕外緩沖區,然后轉換環境到幀緩沖區)。
關于離屏渲染,如果展開來說,又是一個相對而言比較復雜的話題,我將在下一篇文章《iOS 2D Graphic(3)—— Offscreen Rendering離屏渲染》中詳細討論和總結這部分的內容。
除了以上常用的選項外,還有一個你可能也會用的到的選項:
- Color Misaligned Images
這個選項檢查了圖片是否被放縮,像素是否對齊。被放縮的圖片會被標記為黃色,像素不對齊則會標注為紫色。
黃色的場景比較多見,比如你將一個200200的圖片塞到了一個100100的UIImageView里,那么圖片自然就會被縮放;紫色的Misaligned Image場景一般很難見到,主要表示要繪制的點無法直接映射到頻幕上的像素點,此時系統需要對相鄰的像素點做anti-aliasing反鋸齒計算。通常這種問題出在對某些View的Frame重新計算和設置時產生的。
2. OpenGL ES Driver Instrument
相對于前面介紹的Core Animation Profiler來說,OpenGL ES Driver的側重點非常集中,它的目標很明確,就是針對GPU的使用進行檢測。你可以在右下角的Panel里選擇感興趣的指標選項,然后在下方的列表里觀察數值。一般而言,Device Utilization,Renderer Utilization和Core Animation Frame Per Second是你應該要關注的首選項,這些指標將直接反應當前GPU的使用率。如果你發現GPU的使用率明顯偏高,那么很明顯你面臨了GPU Bound的情況。然后,你可以再用前文介紹的Core Animation Profiler,使用Color Debug去分析具體可能的原因。
優化策略和方法
那么當我們已經知道了Graphic的性能大致上分為CPU Bound和GPU Bound兩種類型,并且我們也知道可以使用Instrument來分析具體的源頭所在,那么我們能采取哪些措施和策略來解決問題呢?
為了能夠清晰的闡述這些方法,我們需要思考下iOS中Animation的實現步驟,因為即使是非Animation的靜態頁面,我們也可以看作是只有一幀畫面的特殊Animation,所以分析優化策略,可以對照著Animation的核心步驟來進行。
我們來看上一篇中出現過的描述一個完整的Animation的步驟的圖:
1. 創建Animation,并更新視圖;
** 2. 計算Animation必要的數據并提交動畫,具體分為以下四步:**
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 調用在此進行
? String drawing or other expensive drawing
3)準備:【CPU bound】
? 圖像解碼轉換,額外的動畫操作;
4)提交:【CPU bound】
打包Layers及動畫參數,這是遞歸操作,如果layer層級非常復雜,則代價比較昂貴。隨后Layers通過IPC被送到Render Server** 3. Render Server進行最終渲染**
了解這上面這3大步驟,將整個Animation從準備到完成的過程分成了兩個清晰的階段來做性能評估:“動畫的響應速度”(Responsiveness)和“動畫的平滑性”(Smoothness),其中前者對應于1,2步,后者對應于第3步。于是,我們討論優化策略都將在這兩個大方向上做文章。
(1)響應速度優化
減少初始化(Do less set up)
? 盡量避免在layout期間讓CPU做重度運算,或者其它阻塞操作;
? 數據上盡量使用in-memory caches;
? 如果需要做數據庫查詢,盡可能確保數據庫已經對高性能要求的部分提前做了正確的索引;
? 盡可能重用cell和view;
? 在drawRect之外做初始化,并且全局只初始化一次然后復用。例如避免CGColors, CGPaths, clipShapes的重復創建,只初始化一次然后復用減少繪制(Reduce drawing)
-
只重繪變化的部分
? 第一黃金原則:“避免使用drawRect:”:
? 盡可能使用CALayer的屬性,而避免使用DrawRect重繪。經典案例就是設置背景顏色,使用backgroundColor而不是UIColor setFill:// bad
-(void)drawRect:(CGRect)rect {
[[UIColor redColor] setFill];
UIRectFill([self bounds]);
}
// good
[myView setBackgroundColor:[UIColor redColor]];
? 如果你必須重寫drawRect:
,則盡可能調用setNeedsDisplayInRect:
而不是-setNeedsDisplay
。這將讓Core Graphic自動為你的drawRect:代碼創建clipRect:而無需改動任何其它代碼,然后在繪制時自動忽略clipRect之外的部分,它能夠顯著提高性能;
? 如果你無法提前預知更新的視圖范圍,那么只在需要的時候調用-setNeedsDisplay
-
**正確處理圖片(Be smart with images) **
? 使用UIImageView而不是直接繪制image。這是因為此時blending是發生在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強調在iOS中,PNG和JPEG是Apple官方推薦的格式,Xcode能夠針對這兩種格式做額外的優化,不要使用其它格式的圖片比如TIFF等;
? 對UI上的縮略圖使用單獨的Image,而不是將原圖縮放到小圖里。這里對縮略圖有一條規則“如果使用PNG能夠在失真允許的情況下獲得足夠好的壓縮效果,則使用PNG!”
? 關于緩存:
[UIImage imageNamed:]
將自動緩存在可刪除的內存里(purgeable memory ),同時保存索引在 image table中以便復用;而[UIImage imageWithContentsOfFile:]
不會!
? 當你設置layer contents的時候,所有作為layer backstore的CGImages都會被緩存;
? 如果你手動創建一個CGImage,則打開kCGImageSourceShouldCache顯式地建立緩存;
? 通常情況下,不要自己手動建立Image的緩存 - (void)drawRect:(CGRect)rect
關于圖片的這部分,我們可以利用上文里提到的Instrument中的“Color Copied Image”debug選項,來檢測是否有額外的圖片操作是可以避免的。
-
異步繪制(Draw asynchronously)
這是一個比較少見的方法,但是Apple仍然提供了這一選擇。當你使用-drawInContext:
為CALayer提供Content的時候,Core Animation 可以通過兩種方式來進行渲染:
■ 普通繪圖(Normal drawing)會同步的阻塞當前線程直至完成;
■ 異步繪圖(Asynchronous drawing) 會將繪圖命令發送到后臺來完成渲染
異步方式默認是關閉的,因為它并不總是能夠提高性能。通常當你需要在一個單一的很大的view context區域內完成images, rectangles, shadings等等的繪制會比較有效。如果想使用異步繪制,只需要在當前的繪制context layer內調用:
myView.layer.drawsAsynchronously = YES;
同樣的,一定要測試才決定是否使用。
-
預處理數據(Speculative preparation )
提前對將要顯示的內容進行計算查詢,然后將數據放置在緩沖區里以便后面使用。一種經典的場景就是,一個長長的列表顯示網絡的圖片,可以預先把后續需要加載的圖片的下載,解碼和繪制工作放在后臺的線程里進行異步處理,然后等到需要顯示時再在同步到UI上。關于這一點,下文第三部分將會重點強調這種并發的繪圖操作的基本方式。(注意,這里的異步是數據的異步,而不是上文rendering本身的異步)
(2)動畫平滑性優化
Animation的Step3發生在Render Server和GPU上:Render Server是以per layer, per frame的方式工作在CPU上,而Rendering本身是發生在GPU上。所以這一步同樣是CPU Bound和GPU Bound同時存在。而影響動畫的平滑性,更多的是GPU的負擔過重,使用OpenGL ES instrument,查看Device Utilization的使用率,能夠很好的指導你是否發生了GPU bound issue。記住我們一切一切的目標,就是60 fps!
減少Blending
使用Instrument的(Color Blended Layers)選項可以幫助我們定位此項優化操作的必要性。找到圖層復雜的部分:
? 嘗試進行視圖層級精簡;
? 盡可能的使用非透明圖層/圖片。因為Alpha通道的混合blending比繪制完全不透明的圖層要慢很多嘗試扁平化Flattening
注意這里的扁平化不是說iOS 7倡導的UI設計的扁平化,而是說使用一些技術將立體的多層級Layer進行融合和渲染后繪制到單一視圖上。如果你遇到的是GPU Bound的情景,Flattening view hierarchy 能夠顯著的幫助提高性能。
? 使用離屏緩沖區對內容進行flatten(即將當前視圖內容刷到單獨的Image data中)
? 盡可能縮短繪制圖像時的CGPath;當需要畫一個很大很復雜的CGPath時,盡可能只重繪你更新的那一部分,而不是整個path,而將其它的部分Flatten到bitmap中;
? 常見的Flatten方法:
1)Rasterization
對于一個單一的視圖,如果視圖內的Layer內容層級很復雜,但是內容(層級樹和數據等)本身并不變化(或極少變化),則可以對base layer使用Rasterization(光柵化)。Rasterization其實是將當前layer及其所有sub layer的圖像全部混合繪制到一個獨立的bitmap中緩存起來,后續的顯示將直接使用這個緩存的圖片在硬件中進行組合而不再重新渲染。使用光柵化的方法很簡單,在你需要的base layer上調用:
baseView.layer.rasterizationScale = [UIScreen mainScreen].scale
baseView.layer.shouldRasterize = true
但是使用rasterization需要注意:
■ 有限的緩存空間,大約是2.5x 屏幕的大小;
■ 內容一旦變化,緩存立即失效;
■ 如果當前顯示超過100ms沒有使用到當前緩存,則Rasterized images會被清除;
■ 任何你使用shouldRasterize
的地方都必須提前設定rasterizationScale
;
■ Rasterization發生在mask被應用之前,也就是說對于有mask的view,緩存的是被遮罩之前的內容;
你可能會想“Rasterization簡直就是神器啊,那必須得用!” 別急,Rasterization并不是萬能藥!
實際上這是一個 time vs. memory trade-off,用存儲空間交換渲染時間;同時是將部分Render server的工作轉移到了CPU上;又由于在更新緩存內容時,有額外的offsceen操作,這些問題決定了太多的不合適的Rasterization會傷害到“響應性能responsiveness”,因此,對于非GPU-bound的場景,可能會適得其反!所以在做Rasterization之前,請確保在Core Animation instrument中使用 “Color cache hits/misses” 選項來測試你的想法。
2)Seperate Image
對于視圖內的Layer或者內容變化頻繁的場景,rasterization就不再適用了,因為如果做了Rasterization,但是緩存的cache沒有被命中,將會造成比不緩存還要糟糕的消耗!這時可以將整個View的內容使用renderInContext繪制在一個獨立的圖片緩沖區內,然后在后續的繪制過程中,直接使用這張圖片,而不是每次都重復繪制整個View層級。在后面的iPaint Demo里,我們將使用這個技術。
減少離屏渲染Offscreen Rendering
前文已經說過,離屏渲染因為會使得GPU從當前幀緩沖區之外額外的緩沖區進行繪制操作,然后切換環境把內容切換回當前幀緩沖區,會對性能造成非常大的影響。在Scroll/table view中幾乎大部分常見的性能問題都是因為存在過多的離屏渲染造成的。因此在App中要盡可能的避免或者減少離屏渲染。造成離屏渲染的原因有很多,masking是最有可能的一個,陰影也是,而上文提到的rasterization也會至少產生一次offscreen rendering,但是并不能說rasterization就不能用,關鍵還是看使用的方式和場合。除此之外,還有其它一些情況會產生離屏渲染。本系列下一篇文章,將詳細闡述離屏渲染的問題。
? 慎重使用陰影(Drop shadows)
這里單獨強調一下陰影的問題。陰影是非常昂貴的渲染操作,盡可能的減少陰影的設計,如果必須使用陰影,則可以通過以下的方式來提高性能:
■ 使用shadowPath來預先定義陰影的形狀,而不是讓layer自己來判斷陰影的區域:
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];
■ 對于靜態的陰影視圖,在使用上述方式生成陰影之后,再使用rasterization將陰影直接flatten成靜態位圖,這樣后續的顯示不用再重復繪制陰影,而是直接使用緩存顯示。
view.layer.rasterizationScale = [UIScreen mainScreen].scale;
view.layer.shouldRasterize = YES;
在Core Animation Instrument中,可以通過“Color Offscreen-Rendered Yellow”選項來識別當前視圖中發生offscreen rendering的區域。認真對待滑動Scrolling
? 第一黃金原則:“重用cells and views”;
? 盡可能的減少layout和drawing的時間,也就是盡可能的減少視圖層次復雜度和自定義drawRect:的消耗;
? 同樣的,預先加載將要顯示的視圖數據,減少同步加載時間;
? 在視圖結構不可避免的復雜情況時,嘗試Flatten,但是需要測試驗證;
? 及時取消已經滑出屏幕的cell的相關計算和繪制操作(見后文)
(3)并發
除了上面提到的具有針對性的2大部分內容,凡是提到“性能”兩個字,有經驗的人都會自然而然的想到另外兩個字“并發”。并發實際上是充分利用了多核CPU的并行處理能力,讓繁重的處理操作和界面響應操作能夠分別在不同的線程里并行的完成,讓界面的操作能夠及時被響應而不被其它的耗時處理所堵塞。
根據長時高計算的操作的類別不同,可以將并發的方式分為“數據并發”和“繪圖并發”:
-
并發數據處理(Process Data Concurrently)
我們知道,iOS的所有UI事件響應(Touch Event)都是在主線程執行的,而主線程自動維護一個隊列(Main Queue),所有的事件都會在隊列里排隊按照順序執行。簡單情況下,我們可能會將數據處理操作都放在主線程里去做,這樣很直接很簡單。但是在某些情況下,如果數據處理非常耗時,那么主線程會一直被阻塞在數據處理上,而UI的操作事件一直在排隊無法執行,從而造成界面的卡頓現象:
Processing data blocks main thread.png
如上圖所示,Touch Event必須等待數據下載結束并處理完成后才能執行。這個時候,為了提高UI的性能,可以手動創建一個NSOperationQueue(你也可以使用async dispath_queue),將數據處理放到后臺線程的隊列里,當數據處理完成后,再將結果通過主線程更新到UI上:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue setName:@”Data Processing Queue”];
[queue addOperationWithBlock:^{ processStock(someStock); }];
[queue addOperationWithBlock:^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
updateUI(someStock);
}];
}];
concurrent processing.png -
并發繪圖(Draw Concurrently)
同樣的道理可以類推到繪圖操作上。常規情況,我們將所有的繪制都放在drawRect:
里,而drawRect:
是只能執行在主線程上。如果在drawRect:
做了大量的繪制操作,那么主線程會一直被阻塞,導致Touch Event不能被響應。
// Not so good if image drawing is very expensive
- (void)drawRect:(CGRect)rect {
[[UIColor greenColor] set];
UIRectFill([self bounds]);
[anImage drawAtPoint:CGPointZero];
}
Blocking drawing.png
這個時候,我們可以把繪制工作放在獨立的線程隊列里,然后把繪制結果生成一張圖片,在通過主線程,把圖片繪制在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在任何隊列里調用都是安全的,不一定非得是主線程隊列,但是必須確保begin和end在同一個上下文中(同一個operation中)
?最終必須在Main Queue中更新圖片
-
適時取消并發操作(Canceling concurrent operations)
雖然有了并發的隊列操作,也并不是說就不管不問放任Queue中的task去執行了,因為最終你還是需要更新UI的,為了讓UI能夠更快的響應用戶的執行,我們需要讓這個隊列去更快更多去執行操作。因此在任務非常繁重的情況下,適時地取消隊列中已經失效的操作是非常必要的。
舉一個例子,如果你有一個table,每一個cell需要動態的顯示一些圖表(比如股票走勢圖,或者是從網絡上下載更多的小圖片),按照我們之前的建議,你已經把每一個cell的視圖更新和繪制都放在了queue中,然后讓這些queue中的task異步地去更新每一個cell,這很好,table能夠繼續響應用戶的滑動了,但是當用戶已經選擇了一個具體的cell,不再對其它的內容感興趣時,queue中還在繼續執行很多計算或者是下載,當用戶再添加新的操作到queue中,仍然還在等待那些他已經不感興趣的cell內容的更新。
顯然這是不必要的,這個時候,我們就可以取消隊列中的操作來減輕queue的壓力,使得它能夠更快的響應后續其它的操作。
iOS提供了3種相關的取消操作API:
- [NSOperationQueue cancelAllOperations]
- [NSOperationQueue cancel]
- [NSOperation isCancelled]
? 使用[NSOperationQueue cancelAllOperations]
取消隊列中的所有操作;
? 使用[NSOperation cancel]
取消單個操作;
注意上述兩種方式都無法取消正在執行的operation,如果想讓執行中的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,很好的示例了多種不同的優化方法帶來的效果。但是這個Demo并沒有Code可以下載。于是我就模仿演講者在Demo過程中提到的內容和屏幕上能夠看到的一些片段的Code,自己重新寫了一個Demo。你可以在Github上下載 iPaint Demo
因為篇幅的原因,我們只來看這個Demo的一個具體示例,你可以看到不同的開關對最終繪圖的性能效果影響。
首先,在打開“Calculate Dirty Rect”開關計算更新區域并使用- setNeedDisplayInRect
之前,由于打開了Flatten Long Path選項,在繪畫路徑長到一定程度的時候,會將整個視圖扁平化到一個單獨的Image中,在第一個版本的實現上,我們可以看到效果非常差,FPS直接掉到10以下:
用Instrument查看OpenGL ES Driver,發現GPU的壓力并不是很大,但是幀率很低:
使用Time Profiler查看,發現CPU絕大部分的開銷都在DrawImage上面:
那么在不改變當前Drawing策略的前提下,只是更改一個地方,打開Calculate Dirty Rect開關,每次drawInRect只是重新繪制當前更新的一小部分區域(紅框內的區域),可以看到性能明顯提升:
但是,可以看到,幀率仍然不夠理想,離目標60 fps還差得很遠。觀察CPU的使用,還是因為在Flatten Path的時候繪制圖像的時間太長,可以在Time Profiler中,CGContextDrawImage仍然花了大部分時間。如果這個時候,關閉Flatten Path開關,幀率立即就上來了。
可是Flatten Path是有用的,否則隨著Path的不斷增長,幀率還是會下降,所以關鍵問題是在Flatten的時候,如何降低繪圖的開銷。這個問題一直困擾了我很久,因為WWDC視頻的代碼并沒有顯示作者是如何做Flatten的。(這個地方是不能用rasterization的,因為整個Layer的圖層在不斷變化,使用光柵是沒有用的,必須手動將圖層上的物件繪制到一張圖片上)
最后,我終于發現了問題所在:因為在UIKit和Core Graphic的坐標系問題,直接使用UIGraphicsGetImageFromCurrentImageContext()
得到的圖片是顛倒的,所以我原本使用的是一個Flip()函數,將CoreImage又重新繪制一次,這樣顛倒2次就得到了正確的圖片。可是這樣會造成極大的性能損失,前面Time Profiler中檢測到的CPU損耗原因就在此,于是我想到了額外的方法,是在drawRect中,將原先得到的顛倒的flatten image,使用Core Animation的轉換矩陣CTM,直接將圖片反轉。只此改動,幀率立馬到58左右!
所以從這點上,我們也許可以在上文中的優化策略中再增加一條:
- 盡量使用CTM變換矩陣來直接操作圖形,而不要自己使用額外的計算和繪圖方式
2016.8.12 補充優化內容:
關于CAShapeLayer和CALayer:
CAShapeLayer是一個通過矢量圖形而不是bitmap來繪制的圖層子類。你指定諸如顏色和線寬等屬性,用CGPath來定義想要繪制的圖形,最后CAShapeLayer就自動渲染出來了。當然,你也可以用Core Graphics直接向原始的CALyer的內容中繪制一個路徑,相比直下,使用CAShapeLayer有以下一些優點:
- 渲染快速。CAShapeLayer使用了硬件加速,繪制同一圖形會比用Core Graphics快很多。
- 高效使用內存。一個CAShapeLayer不需要像普通CALayer一樣創建一個后備存儲,所以無論有多大,都不會占用太多的內存。
- 不會被圖層邊界剪裁掉。一個CAShapeLayer可以在邊界之外繪制。你的圖層路徑不會像在使用Core Graphics的普通CALayer一樣被剪裁掉。
- 不會出現像素化。當你給CAShapeLayer做3D變換時,它不像一個有寄宿圖的普通圖層一樣變得像素化。
總結
到此,此文中提到的各種優化場景和策略已經能夠涵蓋絕大多數情況下的問題,但是這些都是通用對策,并不是萬能鑰匙,只是給我們在遇到問題時提供對策的思路,至于真正的具體實施方案,對于每一個具體問題都是不同,我們需要不斷的測試和調試來找到最佳的方案。
最后,用Apple WWDC上的兩張圖來簡單總結一下遇到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性能調優實戰講解
Layer Trees vs. Flat Drawing – Graphics Performance Across iOS Device Generations
2016.7.20 完稿于南京