iOS-Core-Animation-Advanced-Techniques(十三:高效繪圖)

本文轉載自:http://www.cocoachina.com/ios/20150106/10840.html? 為了防止cocochina以后刪除該文章,故轉載至此;

高效繪圖

不必要的效率考慮往往是性能問題的萬惡之源。 ——William Allan Wulf

在第12章『速度的曲率』我們學習如何用Instruments來診斷Core Animation性能問題。在構建一個iOS app的時候會遇到很多潛在的性能陷阱,但是在本章我們將著眼于有關繪制的性能問題。

軟件繪圖

術語繪圖通常在Core Animation的上下文中指代軟件繪圖(意即:不由GPU協助的繪圖)。在iOS中,軟件繪圖通常是由Core Graphics框架完成來完成。但是,在一些必要的情況下,相比Core Animation和OpenGL,Core Graphics要慢了不少。

軟件繪圖不僅效率低,還會消耗可觀的內存。CALayer只需要一些與自己相關的內存:只有它的寄宿圖會消耗一定的內存空間。即使直接賦給contents屬性一張圖片,也不需要增加額外的照片存儲大小。如果相同的一張圖片被多個圖層作為contents屬性,那么他們將會共用同一塊內存,而不是復制內存塊。

但是一旦你實現了CALayerDelegate協議中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(其實就是前者的包裝方法),圖層就創建了一個繪制上下文,這個上下文需要的大小的內存可從這個算式得出:圖層寬*圖層高*4字節,寬高的單位均為像素。對于一個在Retina iPad上的全屏圖層來說,這個內存量就是 2048*1526*4字節,相當于12MB內存,圖層每次重繪的時候都需要重新抹掉內存然后重新分配。

軟件繪圖的代價昂貴,除非絕對必要,你應該避免重繪你的視圖。提高繪制性能的秘訣就在于盡量避免去繪制。

矢量圖形

我們用Core Graphics來繪圖的一個通常原因就是只是用圖片或是圖層效果不能輕易地繪制出矢量圖形。矢量繪圖包含一下這些:

任意多邊形(不僅僅是一個矩形)

斜線或曲線

文本

漸變

舉個例子,清單13.1 展示了一個基本的畫線應用。這個應用將用戶的觸摸手勢轉換成一個UIBezierPath上的點,然后繪制成視圖。我們在一個UIView子類DrawingView中實現了所有的繪制邏輯,這個情況下我們沒有用上view controller。但是如果你喜歡你可以在view controller中實現觸摸事件處理。圖13.1是代碼運行結果。

清單13.1 用Core Graphics實現一個簡單的繪圖應用

#import?"DrawingView.h"

@interface?DrawingView?()

@property?(nonatomic,?strong)?UIBezierPath?*path;

@end

@implementation?DrawingView

-?(void)awakeFromNib

{

//create?a?mutable?path

self.path?=?[[UIBezierPath?alloc]?init];

self.path.lineJoinStyle?=?kCGLineJoinRound;

self.path.lineCapStyle?=?kCGLineCapRound;

self.path.lineWidth?=?5;

}

-?(void)touchesBegan:(NSSet?*)touches?withEvent:(UIEvent?*)event

{

//get?the?starting?point

CGPoint?point?=?[[touches?anyObject]?locationInView:self];

//move?the?path?drawing?cursor?to?the?starting?point

[self.path?moveToPoint:point];

}

-?(void)touchesMoved:(NSSet?*)touches?withEvent:(UIEvent?*)event

{

//get?the?current?point

CGPoint?point?=?[[touches?anyObject]?locationInView:self];

//add?a?new?line?segment?to?our?path

[self.path?addLineToPoint:point];

//redraw?the?view

[self?setNeedsDisplay];

}

-?(void)drawRect:(CGRect)rect

{

//draw?path

[[UIColor?clearColor]?setFill];

[[UIColor?redColor]?setStroke];

[self.path?stroke];

}

@end

圖13.1 用Core Graphics做一個簡單的『素描』

這樣實現的問題在于,我們畫得越多,程序就會越慢。因為每次移動手指的時候都會重繪整個貝塞爾路徑(UIBezierPath),隨著路徑越來越復雜,每次重繪的工作就會增加,直接導致了幀數的下降。看來我們需要一個更好的方法了。

Core Animation為這些圖形類型的繪制提供了專門的類,并給他們提供硬件支持(第六章『專有圖層』有詳細提到)。CAShapeLayer可以繪制多邊形,直線和曲線。CATextLayer可以繪制文本。CAGradientLayer用來繪制漸變。這些總體上都比Core Graphics更快,同時他們也避免了創造一個寄宿圖。

如果稍微將之前的代碼變動一下,用CAShapeLayer替代Core Graphics,性能就會得到提高(見清單13.2).雖然隨著路徑復雜性的增加,繪制性能依然會下降,但是只有當非常非常浮躁的繪制時才會感到明顯的幀率差異。

清單13.2 用CAShapeLayer重新實現繪圖應用

#import?"DrawingView.h"

#import?@interface?DrawingView?()

@property?(nonatomic,?strong)?UIBezierPath?*path;

@end


@implementation?DrawingView

+?(Class)layerClass

{

//this?makes?our?view?create?a?CAShapeLayer

//instead?of?a?CALayer?for?its?backing?layer

return[CAShapeLayer?class];

}

-?(void)awakeFromNib

{

//create?a?mutable?path

self.path?=?[[UIBezierPath?alloc]?init];

//configure?the?layer

CAShapeLayer?*shapeLayer?=?(CAShapeLayer?*)self.layer;

shapeLayer.strokeColor?=?[UIColor?redColor].CGColor;

shapeLayer.fillColor?=?[UIColor?clearColor].CGColor;

shapeLayer.lineJoin?=?kCALineJoinRound;

shapeLayer.lineCap?=?kCALineCapRound;

shapeLayer.lineWidth?=?5;

}

-?(void)touchesBegan:(NSSet?*)touches?withEvent:(UIEvent?*)event

{

//get?the?starting?point

CGPoint?point?=?[[touches?anyObject]?locationInView:self];

//move?the?path?drawing?cursor?to?the?starting?point

[self.path?moveToPoint:point];

}

-?(void)touchesMoved:(NSSet?*)touches?withEvent:(UIEvent?*)event

{

//get?the?current?point

CGPoint?point?=?[[touches?anyObject]?locationInView:self];

//add?a?new?line?segment?to?our?path

[self.path?addLineToPoint:point];

//update?the?layer?with?a?copy?of?the?path

((CAShapeLayer?*)self.layer).path?=?self.path.CGPath;

}

@end

臟矩形

有時候用CAShapeLayer或者其他矢量圖形圖層替代Core Graphics并不是那么切實可行。比如我們的繪圖應用:我們用線條完美地完成了矢量繪制。但是設想一下如果我們能進一步提高應用的性能,讓它就像一個黑板一樣工作,然后用『粉筆』來繪制線條。模擬粉筆最簡單的方法就是用一個『線刷』圖片然后將它粘貼到用戶手指碰觸的地方,但是這個方法用CAShapeLayer沒辦法實現。

我們可以給每個『線刷』創建一個獨立的圖層,但是實現起來有很大的問題。屏幕上允許同時出現圖層上線數量大約是幾百,那樣我們很快就會超出的。這種情況下我們沒什么辦法,就用Core Graphics吧(除非你想用OpenGL做一些更復雜的事情)。

我們的『黑板』應用的最初實現見清單13.3,我們更改了之前版本的DrawingView,用一個畫刷位置的數組代替UIBezierPath。圖13.2是運行結果

清單13.3 簡單的類似黑板的應用

#import?"DrawingView.h"

#import?#define?BRUSH_SIZE?32

@interface?DrawingView?()

@property?(nonatomic,?strong)?NSMutableArray?*strokes;

@end

@implementation?DrawingView

-?(void)awakeFromNib

{

//create?array

self.strokes?=?[NSMutableArray?array];

}

-?(void)touchesBegan:(NSSet?*)touches?withEvent:(UIEvent?*)event

{

//get?the?starting?point

CGPoint?point?=?[[touches?anyObject]?locationInView:self];

//add?brush?stroke

[self?addBrushStrokeAtPoint:point];

}

-?(void)touchesMoved:(NSSet?*)touches?withEvent:(UIEvent?*)event

{

//get?the?touch?point

CGPoint?point?=?[[touches?anyObject]?locationInView:self];

//add?brush?stroke

[self?addBrushStrokeAtPoint:point];

}

-?(void)addBrushStrokeAtPoint:(CGPoint)point

{

//add?brush?stroke?to?array

[self.strokes?addObject:[NSValue?valueWithCGPoint:point]];

//needs?redraw

[self?setNeedsDisplay];

}

-?(void)drawRect:(CGRect)rect

{

//redraw?strokes

for(NSValue?*valueinself.strokes)?{

//get?point

CGPoint?point?=?[value?CGPointValue];

//get?brush?rect

CGRect?brushRect?=?CGRectMake(point.x?-?BRUSH_SIZE/2,?point.y?-?BRUSH_SIZE/2,?BRUSH_SIZE,?BRUSH_SIZE);

//draw?brush?stroke?????

[[UIImage?imageNamed:@"Chalk.png"]?drawInRect:brushRect];

}

}

@end

圖13.2 用程序繪制一個簡單的『素描』

這個實現在模擬器上表現還不錯,但是在真實設備上就沒那么好了。問題在于每次手指移動的時候我們就會重繪之前的線刷,即使場景的大部分并沒有改變。我們繪制地越多,就會越慢。隨著時間的增加每次重繪需要更多的時間,幀數也會下降(見圖13.3),如何提高性能呢?

圖13.3 幀率和線條質量會隨時間下降。

為了減少不必要的繪制,Mac OS和iOS設備將會把屏幕區分為需要重繪的區域和不需要重繪的區域。那些需要重繪的部分被稱作『臟區域』。在實際應用中,鑒于非矩形區域邊界裁剪和混合的復雜性,通常會區分出包含指定視圖的矩形位置,而這個位置就是『臟矩形』。

當一個視圖被改動過了,TA可能需要重繪。但是很多情況下,只是這個視圖的一部分被改變了,所以重繪整個寄宿圖就太浪費了。但是Core Animation通常并不了解你的自定義繪圖代碼,它也不能自己計算出臟區域的位置。然而,你的確可以提供這些信息。

當你檢測到指定視圖或圖層的指定部分需要被重繪,你直接調用-setNeedsDisplayInRect:來標記它,然后將影響到的矩形作為參數傳入。這樣就會在一次視圖刷新時調用視圖的-drawRect:(或圖層代理的-drawLayer:inContext:方法)。

傳入-drawLayer:inContext:的CGContext參數會自動被裁切以適應對應的矩形。為了確定矩形的尺寸大小,你可以用CGContextGetClipBoundingBox()方法來從上下文獲得大小。調用-drawRect()會更簡單,因為CGRect會作為參數直接傳入。

你應該將你的繪制工作限制在這個矩形中。任何在此區域之外的繪制都將被自動無視,但是這樣CPU花在計算和拋棄上的時間就浪費了,實在是太不值得了。

相比依賴于Core Graphics為你重繪,裁剪出自己的繪制區域可能會讓你避免不必要的操作。那就是說,如果你的裁剪邏輯相當復雜,那還是讓Core Graphics來代勞吧,記住:當你能高效完成的時候才這樣做。

清單13.4 展示了一個-addBrushStrokeAtPoint:方法的升級版,它只重繪當前線刷的附近區域。另外也會刷新之前線刷的附近區域,我們也可以用CGRectIntersectsRect()來避免重繪任何舊的線刷以不至于覆蓋已更新過的區域。這樣做會顯著地提高繪制效率(見圖13.4)

清單13.4 用-setNeedsDisplayInRect:來減少不必要的繪制

-?(void)addBrushStrokeAtPoint:(CGPoint)point

{

//add?brush?stroke?to?array

[self.strokes?addObject:[NSValue?valueWithCGPoint:point]];

//set?dirty?rect

[self?setNeedsDisplayInRect:[self?brushRectForPoint:point]];

}

-?(CGRect)brushRectForPoint:(CGPoint)point

{

returnCGRectMake(point.x?-?BRUSH_SIZE/2,?point.y?-?BRUSH_SIZE/2,?BRUSH_SIZE,?BRUSH_SIZE);

}

-?(void)drawRect:(CGRect)rect

{

//redraw?strokes

for(NSValue?*valueinself.strokes)?{

//get?point

CGPoint?point?=?[value?CGPointValue];

//get?brush?rect

CGRect?brushRect?=?[self?brushRectForPoint:point];

?

//only?draw?brush?stroke?if?it?intersects?dirty?rect

if(CGRectIntersectsRect(rect,?brushRect))?{

//draw?brush?stroke

[[UIImage?imageNamed:@"Chalk.png"]?drawInRect:brushRect];

}

}

}

圖13.4 更好的幀率和順滑線條

異步繪制

UIKit的單線程天性意味著寄宿圖通暢要在主線程上更新,這意味著繪制會打斷用戶交互,甚至讓整個app看起來處于無響應狀態。我們對此無能為力,但是如果能避免用戶等待繪制完成就好多了。

針對這個問題,有一些方法可以用到:一些情況下,我們可以推測性地提前在另外一個線程上繪制內容,然后將由此繪出的圖片直接設置為圖層的內容。這實現起來可能不是很方便,但是在特定情況下是可行的。Core Animation提供了一些選擇:CATiledLayer和drawsAsynchronously屬性。

CATiledLayer

我們在第六章簡單探索了一下CATiledLayer。除了將圖層再次分割成獨立更新的小塊(類似于臟矩形自動更新的概念),CATiledLayer還有一個有趣的特性:在多個線程中為每個小塊同時調用-drawLayer:inContext:方法。這就避免了阻塞用戶交互而且能夠利用多核心新片來更快地繪制。只有一個小塊的CATiledLayer是實現異步更新圖片視圖的簡單方法。

drawsAsynchronously

iOS 6中,蘋果為CALayer引入了這個令人好奇的屬性,drawsAsynchronously屬性對傳入-drawLayer:inContext:的CGContext進行改動,允許CGContext延緩繪制命令的執行以至于不阻塞用戶交互。

它與CATiledLayer使用的異步繪制并不相同。它自己的-drawLayer:inContext:方法只會在主線程調用,但是CGContext并不等待每個繪制命令的結束。相反地,它會將命令加入隊列,當方法返回時,在后臺線程逐個執行真正的繪制。

根據蘋果的說法。這個特性在需要頻繁重繪的視圖上效果最好(比如我們的繪圖應用,或者諸如UITableViewCell之類的),對那些只繪制一次或很少重繪的圖層內容來說沒什么太大的幫助。

總結

本章我們主要圍繞用Core Graphics軟件繪制討論了一些性能挑戰,然后探索了一些改進方法:比如提高繪制性能或者減少需要繪制的數量。

第14章,『圖像IO』,我們將討論圖片的載入性能。

--------------------------------------------------------------------------------------------------------------------------------------------------------

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

推薦閱讀更多精彩內容