[iOS Animation]-CALayer 绘图效率

绘图

没必要要的效率考虑每每是性能问题的万恶之源。 ——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

图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.2 用程序绘制一个简单的『素描』

这个实如今模拟器上表现还不错,可是在真实设备上就没那么好了。问题在于每次手指移动的时候咱们就会重绘以前的线刷,即便场景的大部分并无改变。咱们绘制地越多,就会越慢。随着时间的增长每次重绘须要更多的时间,帧数也会降低(见图13.3),如何提升性能呢?

图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

图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』,咱们将讨论图片的载入性能。

相关文章
相关标签/搜索