簡介
在iOS
中,軟件繪圖通常是由Core Graphics
框架完成來完成。但是,在一些必要的情況下,相比Core Animation
和OpenGL
,Core Graphics
要慢了不少。
軟件繪圖不僅效率低,還會消耗可觀的內存。CALayer
只需要一些與自己相關的內存:它的寄宿圖會消耗一定的內存空間。
但是一旦你實現了CALayerDelegate
協議中的-drawLayer:inContext:
方法或者UIView
中的-drawRect:
方法(其實就是前者的包裝方法),圖層就創建了一個繪制上下文,這個上下文需要的大小的內存可從這個算式得出:圖層寬圖層高4字節,寬高的單位均為像素。對于一個在Retina iPad上的全屏圖層來說,這個內存量就是 204815264字節,相當于12MB內存,圖層每次重繪的時候都需要重新抹掉內存然后重新分配。
軟件繪圖的代價昂貴,除非絕對必要,你應該避免重繪你的視圖。提高繪制性能的秘訣就在于盡量避免去繪制。
繪制方法對比與選擇
Core Graphics繪制 - 如果對視圖實現了-drawRect:方法,或者CALayerDelegate的-drawLayer:inContext:方法,那么在繪制任何東西之前都會產生一個巨大的性能開銷。為了支持對圖層內容的任意繪制,Core Animation必須創建一個內存中等大小的寄宿圖片。然后一旦繪制結束之后,必須把圖片數據通過IPC傳到渲染服務器。在此基礎上,Core Graphics繪制就會變得十分緩慢,所以在一個對性能十分挑剔的場景下這樣做十分不好。
圖層對比與選擇
CAShapeLayer
CAShapeLayer
是一個通過矢量圖形而不是bitmap
來繪制的圖層子類。你指定諸如顏色和線寬等屬性,用CGPath
來定義想要繪制的圖形,最后CAShapeLayer
就自動渲染出來了。當然,你也可以用Core Graphics
直接向原始的CALyer的內容中繪制一個路徑,相比直下,使用CAShapeLayer
有以下一些優點:
- 渲染快速。CAShapeLayer使用了硬件加速,繪制同一圖形會比用Core Graphics快很多。
- 高效使用內存。一個CAShapeLayer不需要像普通CALayer一樣創建一個寄宿圖形,所以無論有多大,都不會占用太多的內存。
- 不會被圖層邊界剪裁掉。一個CAShapeLayer可以在邊界之外繪制。你的圖層路徑不會像在使用Core Graphics的普通CALayer一樣被剪裁掉。
- 不會出現像素化。當你給CAShapeLayer做3D變換時,它不像一個有寄宿圖的普通圖層一樣變得像素化。
CAShapeLayer應用對比
用Core Graphics
做一個簡單的『素描』 這樣實現的問題在于,我們畫得越多,程序就會越慢。因為每次移動手指的時候都會重繪整個貝塞爾路徑(UIBezierPath)
,隨著路徑越來越復雜,每次重繪的工作就會增加,直接導致了幀數的下降。看來我們需要一個更好的方法了。
Core Animation
為這些圖形類型的繪制提供了專門的類,并給他們提供硬件支持。CAShapeLayer
可以繪制多邊形,直線和曲線。CATextLayer
可以繪制文本。CAGradientLayer
用來繪制漸變。
這些總體上都比Core Graphics
更快,同時他們也避免了創造一個寄宿圖。 如果用CAShapeLayer
替代Core Graphics
,性能就會得到提高,雖然隨著路徑復雜性的增加,繪制性能依然會下降,但是只有當非常非常浮躁的繪制時才會感到明顯的幀率差異。
CAShapeLayer存在的缺陷
特殊情況下用CAShapeLayer
或者其他矢量圖形圖層替代Core Graphics
并不是那么切實可行。比如我們的繪圖應用:我們用線條完美地完成了矢量繪制。但是設想一下如果我們能進一步提高應用的性能,讓它就像一個黑板一樣工作,然后用『粉筆』來繪制線條。模擬粉筆最簡單的方法就是用一個『線刷』圖片然后將它粘貼到用戶手指碰觸的地方,但是這個方法用CAShapeLayer
沒辦法實現。 我們可以給每個『線刷』創建一個獨立的圖層,但是實現起來有很大的問題。屏幕上允許同時出現圖層上線數量大約是幾百,那樣我們很快就會超出的。這種情況下我們沒什么辦法,就用Core Graphics吧(除非你想用OpenGL做一些更復雜的事情)。
#import "DrawingView.h"
#import <QuartzCore/QuartzCore.h>
#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 *value in self.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.3),如何提高性能呢?
為了減少不必要的繪制,Mac OS
和iOS
設備將會把屏幕區分為需要重繪的區域
和不需要重繪的區域
。那些需要重繪的部分被稱作『臟區域』
。在實際應用中,鑒于非矩形區域邊界裁剪和混合的復雜性,通常會區分出包含指定視圖的矩形位置,而這個位置就是『臟矩形』。*** 當一個視圖被改動過了,TA可能需要重繪。但是很多情況下,只是這個視圖的一部分被改變了,所以重繪整個寄宿圖就太浪費了。***
但是Core Animation通常并不了解你的自定義繪圖代碼,它也不能自己計算出臟區域的位置。然而,你的確可以提供這些信息。 當你檢測到指定視圖或圖層的指定部分需要被重繪,你直接調用【-setNeedsDisplayInRect:
】來標記它,然后將影響到的矩形作為參數傳入。這樣就會在一次視圖刷新時調用視圖的-drawRect:
(或圖層代理的-drawLayer:inContext:
方法)。 傳入-drawLayer:inContext:
的CGContext
參數會自動被裁切以適應對應的矩形。為了確定矩形的尺寸大小,你可以用CGContextGetClipBoundingBox()
方法來從上下文獲得大小。調用-drawRect()
會更簡單,因為CGRect
會作為參數直接傳入。 你應該將你的繪制工作限制在這個矩形中。任何在此區域之外的繪制都將被自動無視,但是這樣CPU
花在計算和拋棄上的時間就浪費了,實在是太不值得了。 相比依賴于Core Graphics
為你重繪,裁剪出自己的繪制區域可能會讓你避免不必要的操作。那就是說,如果你的裁剪邏輯相當復雜,那還是讓Core Graphics
來代勞吧,記住:當你能高效完成的時候才這樣做。 代碼 展示了一個-addBrushStrokeAtPoint:
方法的升級版,它只重繪當前線刷的附近區域。另外也會刷新之前線刷的附近區域,我們也可以用【CGRectIntersectsRect()
】來避免重繪任何舊的線刷以不至于覆蓋已更新過的區域。這樣做會顯著地提高繪制效率,下面胡代碼 用-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
{
return CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE);
}
- (void)drawRect:(CGRect)rect
{
//redraw strokes
for (NSValue *value in self.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];
}
}
}
異步繪制
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軟件繪制討論了一些性能挑戰,然后探索了一些改進方法:比如提高繪制性能或者減少需要繪制的數量。