鍍金池/ 教程/ iOS/ 高效繪圖
圖層的樹狀結(jié)構(gòu)
視覺效果
圖像IO
寄宿圖
緩沖
隱式動(dòng)畫
圖層幾何學(xué)
圖層時(shí)間
顯式動(dòng)畫
變換
性能調(diào)優(yōu)
專用圖層
高效繪圖
圖層性能
基于定時(shí)器的動(dòng)畫

高效繪圖

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

在第12章『速度的曲率』我們學(xué)習(xí)如何用Instruments來診斷Core Animation性能問題。在構(gòu)建一個(gè)iOS app的時(shí)候會(huì)遇到很多潛在的性能陷阱,但是在本章我們將著眼于有關(guān)繪制的性能問題。

軟件繪圖

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

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

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

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

矢量圖形

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

  • 任意多邊形(不僅僅是一個(gè)矩形)
  • 斜線或曲線
  • 文本
  • 漸變

舉個(gè)例子,清單13.1 展示了一個(gè)基本的畫線應(yīng)用。這個(gè)應(yīng)用將用戶的觸摸手勢(shì)轉(zhuǎn)換成一個(gè)UIBezierPath上的點(diǎn),然后繪制成視圖。我們?cè)谝粋€(gè)UIView子類DrawingView中實(shí)現(xiàn)了所有的繪制邏輯,這個(gè)情況下我們沒有用上view controller。但是如果你喜歡你可以在view controller中實(shí)現(xiàn)觸摸事件處理。圖13.1是代碼運(yùn)行結(jié)果。

清單13.1 用Core Graphics實(shí)現(xiàn)一個(gè)簡(jiǎn)單的繪圖應(yīng)用

#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

http://wiki.jikexueyuan.com/project/ios-core-animation/images/13.1.png" alt="圖13.1" />

圖13.1 用Core Graphics做一個(gè)簡(jiǎn)單的『素描』 這樣實(shí)現(xiàn)的問題在于,我們畫得越多,程序就會(huì)越慢。因?yàn)槊看我苿?dòng)手指的時(shí)候都會(huì)重繪整個(gè)貝塞爾路徑(UIBezierPath),隨著路徑越來越復(fù)雜,每次重繪的工作就會(huì)增加,直接導(dǎo)致了幀數(shù)的下降??磥砦覀冃枰粋€(gè)更好的方法了。 Core Animation為這些圖形類型的繪制提供了專門的類,并給他們提供硬件支持(第六章『專有圖層』有詳細(xì)提到)。CAShapeLayer可以繪制多邊形,直線和曲線。CATextLayer可以繪制文本。CAGradientLayer用來繪制漸變。這些總體上都比Core Graphics更快,同時(shí)他們也避免了創(chuàng)造一個(gè)寄宿圖。 如果稍微將之前的代碼變動(dòng)一下,用CAShapeLayer替代Core Graphics,性能就會(huì)得到提高(見清單13.2).雖然隨著路徑復(fù)雜性的增加,繪制性能依然會(huì)下降,但是只有當(dāng)非常非常浮躁的繪制時(shí)才會(huì)感到明顯的幀率差異。 清單13.2 用CAShapeLayer重新實(shí)現(xiàn)繪圖應(yīng)用

#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

臟矩形

有時(shí)候用CAShapeLayer或者其他矢量圖形圖層替代Core Graphics并不是那么切實(shí)可行。比如我們的繪圖應(yīng)用:我們用線條完美地完成了矢量繪制。但是設(shè)想一下如果我們能進(jìn)一步提高應(yīng)用的性能,讓它就像一個(gè)黑板一樣工作,然后用『粉筆』來繪制線條。模擬粉筆最簡(jiǎn)單的方法就是用一個(gè)『線刷』圖片然后將它粘貼到用戶手指碰觸的地方,但是這個(gè)方法用CAShapeLayer沒辦法實(shí)現(xiàn)。 我們可以給每個(gè)『線刷』創(chuàng)建一個(gè)獨(dú)立的圖層,但是實(shí)現(xiàn)起來有很大的問題。屏幕上允許同時(shí)出現(xiàn)圖層上線數(shù)量大約是幾百,那樣我們很快就會(huì)超出的。這種情況下我們沒什么辦法,就用Core Graphics吧(除非你想用OpenGL做一些更復(fù)雜的事情)。 我們的『黑板』應(yīng)用的最初實(shí)現(xiàn)見清單13.3,我們更改了之前版本的DrawingView,用一個(gè)畫刷位置的數(shù)組代替UIBezierPath。圖13.2是運(yùn)行結(jié)果 清單13.3 簡(jiǎn)單的類似黑板的應(yīng)用

#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

http://wiki.jikexueyuan.com/project/ios-core-animation/images/13.2.png" alt="圖13.2" /> 圖13.2 用程序繪制一個(gè)簡(jiǎn)單的『素描』 這個(gè)實(shí)現(xiàn)在模擬器上表現(xiàn)還不錯(cuò),但是在真實(shí)設(shè)備上就沒那么好了。問題在于每次手指移動(dòng)的時(shí)候我們就會(huì)重繪之前的線刷,即使場(chǎng)景的大部分并沒有改變。我們繪制地越多,就會(huì)越慢。隨著時(shí)間的增加每次重繪需要更多的時(shí)間,幀數(shù)也會(huì)下降(見圖13.3),如何提高性能呢? http://wiki.jikexueyuan.com/project/ios-core-animation/images/13.3.png" alt="圖13.3" /> 圖13.3 幀率和線條質(zhì)量會(huì)隨時(shí)間下降。

為了減少不必要的繪制,Mac OS和iOS設(shè)備將會(huì)把屏幕區(qū)分為需要重繪的區(qū)域和不需要重繪的區(qū)域。那些需要重繪的部分被稱作『臟區(qū)域』。在實(shí)際應(yīng)用中,鑒于非矩形區(qū)域邊界裁剪和混合的復(fù)雜性,通常會(huì)區(qū)分出包含指定視圖的矩形位置,而這個(gè)位置就是『臟矩形』。 當(dāng)一個(gè)視圖被改動(dòng)過了,TA可能需要重繪。但是很多情況下,只是這個(gè)視圖的一部分被改變了,所以重繪整個(gè)寄宿圖就太浪費(fèi)了。但是Core Animation通常并不了解你的自定義繪圖代碼,它也不能自己計(jì)算出臟區(qū)域的位置。然而,你的確可以提供這些信息。 當(dāng)你檢測(cè)到指定視圖或圖層的指定部分需要被重繪,你直接調(diào)用-setNeedsDisplayInRect:來標(biāo)記它,然后將影響到的矩形作為參數(shù)傳入。這樣就會(huì)在一次視圖刷新時(shí)調(diào)用視圖的-drawRect:(或圖層代理的-drawLayer:inContext:方法)。 傳入-drawLayer:inContext:CGContext參數(shù)會(huì)自動(dòng)被裁切以適應(yīng)對(duì)應(yīng)的矩形。為了確定矩形的尺寸大小,你可以用CGContextGetClipBoundingBox()方法來從上下文獲得大小。調(diào)用-drawRect()會(huì)更簡(jiǎn)單,因?yàn)?code>CGRect會(huì)作為參數(shù)直接傳入。 你應(yīng)該將你的繪制工作限制在這個(gè)矩形中。任何在此區(qū)域之外的繪制都將被自動(dòng)無視,但是這樣CPU花在計(jì)算和拋棄上的時(shí)間就浪費(fèi)了,實(shí)在是太不值得了。 相比依賴于Core Graphics為你重繪,裁剪出自己的繪制區(qū)域可能會(huì)讓你避免不必要的操作。那就是說,如果你的裁剪邏輯相當(dāng)復(fù)雜,那還是讓Core Graphics來代勞吧,記?。寒?dāng)你能高效完成的時(shí)候才這樣做。 清單13.4 展示了一個(gè)-addBrushStrokeAtPoint:方法的升級(jí)版,它只重繪當(dāng)前線刷的附近區(qū)域。另外也會(huì)刷新之前線刷的附近區(qū)域,我們也可以用CGRectIntersectsRect()來避免重繪任何舊的線刷以不至于覆蓋已更新過的區(qū)域。這樣做會(huì)顯著地提高繪制效率(見圖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];
        }
    }
}

http://wiki.jikexueyuan.com/project/ios-core-animation/images/13.4.png" alt="圖13.4" />

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

異步繪制

UIKit的單線程天性意味著寄宿圖通暢要在主線程上更新,這意味著繪制會(huì)打斷用戶交互,甚至讓整個(gè)app看起來處于無響應(yīng)狀態(tài)。我們對(duì)此無能為力,但是如果能避免用戶等待繪制完成就好多了。 針對(duì)這個(gè)問題,有一些方法可以用到:一些情況下,我們可以推測(cè)性地提前在另外一個(gè)線程上繪制內(nèi)容,然后將由此繪出的圖片直接設(shè)置為圖層的內(nèi)容。這實(shí)現(xiàn)起來可能不是很方便,但是在特定情況下是可行的。Core Animation提供了一些選擇:CATiledLayerdrawsAsynchronously屬性。

CATiledLayer

我們?cè)诘诹潞?jiǎn)單探索了一下CATiledLayer。除了將圖層再次分割成獨(dú)立更新的小塊(類似于臟矩形自動(dòng)更新的概念),CATiledLayer還有一個(gè)有趣的特性:在多個(gè)線程中為每個(gè)小塊同時(shí)調(diào)用-drawLayer:inContext:方法。這就避免了阻塞用戶交互而且能夠利用多核心新片來更快地繪制。只有一個(gè)小塊的CATiledLayer是實(shí)現(xiàn)異步更新圖片視圖的簡(jiǎn)單方法。

drawsAsynchronously

iOS 6中,蘋果為CALayer引入了這個(gè)令人好奇的屬性,drawsAsynchronously屬性對(duì)傳入-drawLayer:inContext:的CGContext進(jìn)行改動(dòng),允許CGContext延緩繪制命令的執(zhí)行以至于不阻塞用戶交互。 它與CATiledLayer使用的異步繪制并不相同。它自己的-drawLayer:inContext:方法只會(huì)在主線程調(diào)用,但是CGContext并不等待每個(gè)繪制命令的結(jié)束。相反地,它會(huì)將命令加入隊(duì)列,當(dāng)方法返回時(shí),在后臺(tái)線程逐個(gè)執(zhí)行真正的繪制。 根據(jù)蘋果的說法。這個(gè)特性在需要頻繁重繪的視圖上效果最好(比如我們的繪圖應(yīng)用,或者諸如UITableViewCell之類的),對(duì)那些只繪制一次或很少重繪的圖層內(nèi)容來說沒什么太大的幫助。

總結(jié)

本章我們主要圍繞用Core Graphics軟件繪制討論了一些性能挑戰(zhàn),然后探索了一些改進(jìn)方法:比如提高繪制性能或者減少需要繪制的數(shù)量。 第14章,『圖像IO』,我們將討論圖片的載入性能。

上一篇:圖層性能下一篇:顯式動(dòng)畫