沒必要要的效率考慮每每是性能問題的萬惡之源。 ——William Allan Wulfgit
在第12章『速度的曲率』咱們學習如何用Instruments來診斷Core Animation性能問題。在構建一個iOS app的時候會遇到不少潛在的性能陷阱,可是在本章咱們將着眼於有關繪製的性能問題。github
術語繪圖一般在Core Animation的上下文中指代軟件繪圖(意即:不禁GPU協助的繪圖)。在iOS中,軟件繪圖一般是由Core Graphics框架完成來完成。可是,在一些必要的狀況下,相比Core Animation和OpenGL,Core Graphics要慢了很多。objective-c
軟件繪圖不只效率低,還會消耗可觀的內存。CALayer
只須要一些與本身相關的內存:只有它的寄宿圖會消耗必定的內存空間。即便直接賦給contents
屬性一張圖片,也不須要增長額外的照片存儲大小。若是相同的一張圖片被多個圖層做爲contents
屬性,那麼他們將會共用同一塊內存,而不是複製內存塊。數組
可是一旦你實現了CALayerDelegate
協議中的-drawLayer:inContext:
方法或者UIView
中的-drawRect:
方法(其實就是前者的包裝方法),圖層就建立了一個繪製上下文,這個上下文須要的大小的內存可從這個算式得出:圖層寬*圖層高*4字節,寬高的單位均爲像素。對於一個在Retina iPad上的全屏圖層來講,這個內存量就是 2048*1526*4字節,至關於12MB內存,圖層每次重繪的時候都須要從新抹掉內存而後從新分配。app
軟件繪圖的代價昂貴,除非絕對必要,你應該避免重繪你的視圖。提升繪製性能的祕訣就在於儘可能避免去繪製。框架
咱們用Core Graphics來繪圖的一個一般緣由就是隻是用圖片或是圖層效果不能輕易地繪製出矢量圖形。矢量繪圖包含一下這些:異步
舉個例子,清單13.1 展現了一個基本的畫線應用。這個應用將用戶的觸摸手勢轉換成一個UIBezierPath
上的點,而後繪製成視圖。咱們在一個UIView
子類DrawingView
中實現了全部的繪製邏輯,這個狀況下咱們沒有用上view controller。可是若是你喜歡你能夠在view controller中實現觸摸事件處理。圖13.1是代碼運行結果。async
清單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 <QuartzCore/QuartzCore.h> @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 <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.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 { 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]; } } }
圖13.4 更好的幀率和順滑線條
UIKit的單線程天性意味着寄宿圖通暢要在主線程上更新,這意味着繪製會打斷用戶交互,甚至讓整個app看起來處於無響應狀態。咱們對此無能爲力,可是若是能避免用戶等待繪製完成就好多了。
針對這個問題,有一些方法能夠用到:一些狀況下,咱們能夠推測性地提早在另一個線程上繪製內容,而後將由此繪出的圖片直接設置爲圖層的內容。這實現起來可能不是很方便,可是在特定狀況下是可行的。Core Animation提供了一些選擇:CATiledLayer
和 drawsAsynchronously 屬性。
咱們在第六章簡單探索了一下CATiledLayer
。除了將圖層再次分割成獨立更新的小塊(相似於髒矩形自動更新的概念),CATiledLayer
還有一個有趣的特性:在多個線程中爲每一個小塊同時調用-drawLayer:inContext:
方法。這就避免了阻塞用戶交互並且可以利用多核心新片來更快地繪製。只有一個小塊的CATiledLayer
是實現異步更新圖片視圖的簡單方法。
iOS 6中,蘋果爲CALayer
引入了這個使人好奇的屬性,drawsAsynchronously
屬性對傳入 -drawLayer:inContext: 的CGContext進行改動,容許CGContext延緩繪製命令的執行以致於不阻塞用戶交互。
它與CATiledLayer
使用的異步繪製並不相同。它本身的 -drawLayer:inContext: 方法只會在主線程調用,可是CGContext並不等待每一個繪製命令的結束。相反地,它會將命令加入隊列,當方法返回時,在後臺線程逐個執行真正的繪製。
根據蘋果的說法。這個特性在須要頻繁重繪的視圖上效果最好(好比咱們的繪圖應用,或者諸如UITableViewCell
之類的),對那些只繪製一次或不多重繪的圖層內容來講沒什麼太大的幫助。
本章咱們主要圍繞用Core Graphics軟件繪製討論了一些性能挑戰,而後探索了一些改進方法:好比提升繪製性能或者減小須要繪製的數量。
第14章,『圖像IO』,咱們將討論圖片的載入性能。