高效绘图

如果实现了CALayerDelegate协议中的- srawLayer:inContext:方法或者UIView- drawRect方法(其实就是前者的包装),图层创建了一个绘制上下文,需要的内存大小就是图层宽*图层高*4字节(宽高单位是像素),图层每次绘制都需要抹掉内存重新分配

关于绘制的性能
提高绘制性能或者减少需要绘制的数量

软件绘图

Core Animation中的绘图通常是指软件绘图
在iOS中,软件绘图通常是由Core Graphics框架完成来完成。但是,在一些必要的情况下,相比Core AnimationOpenGLCore Graphics要慢了不少。

软件绘图不仅效率很低 而且很耗内存.CALayer只需要和自己相关的内存,只有他的寄宿图会消耗一点内存控件,即使直接赋给contents属性一张图片,也不需要增加额外的照片存储大小,如果相同的一张图片被多个图层作为contents属性,那么他们将会共用同一块内存,而不是复制内存块。

但是如果实现了CALayerDelegate协议中的- srawLayer:inContext:方法或者UIView- drawRect方法(其实就是前者的包装),图层创建了一个绘制上下文,需要的内存大小就是图层宽*图层高*4字节(宽高单位是像素),图层每次绘制都需要抹掉内存重新分配

绘图代价很大,尽量避免重绘你的视图

矢量图形

我们用的矢量绘图包含:

  • 任意多边形
  • 斜线或曲线
  • 文本
  • 渐变

实现一个划线应用:将用户的触摸手势转换成一个 UIBezierPath 上的点,然后绘制成视图。

1.用Core Graphic基于drawRect绘制

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    CGPoint point = [[touches anyObject] locationInView:self];
    [self.path addLineToPoint:point];
    [self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
    [[UIColor clearColor] setFill];
    [[UIColor redColor] setStroke];
    [self.path stroke];
}

[self setNeedsDisplay];可以重绘视图,再次走drawRect方法,但是如果一直重绘而且绘制工作量增大就会导致帧数下降.

2.用CAShaperLayer绘制

+ (Class)layerClass
{
    return [CAShapeLayer class];
}

Core Animation为这些图层类型提供了专门的类,并提供了硬件支持,比如CAShapeLayer可以绘制多边形,直线和曲线。 CATextLayer可以绘制文本。CAGradientLayer用来绘制渐变。这些总体上都比Core Graphics更快,同时他们也避免了创造一个寄宿图。(这些图层是CA提供的,而我们的绘图用的是CG)

因此对于绘制矢量图层我们可以重写UIViewlayer然后只需要将绘制的path赋值CAShapeLayerpath即可绘制,虽然性能依然下降但不明显几乎感觉不到帧率差异

脏矩形

有时候我们没办法用CAShapeLayer等图层来代替Core Graphics,对于上面的例子,我们为了进一步提高性能,我们用一个“线刷”图片粘贴到用户手指触碰的地方

我们如果在用户手指移动就重绘视图将图片粘贴到用户手指移动过的所有点,这样绘制越多就越慢,仍会引起性能问题

为了减少不必要的绘制,Mac OS和iOS设备将会把屏幕区分为需要重绘的区域和不需要重绘的区域。那些需要重绘的部分被称作『脏区域』。为了方便我们通常指出包含需要重绘的视图的矩形位置,就是脏矩形

只改变了视图的部分区域的,重绘整个整个寄宿图就太浪费了,我们可以通过提供重绘区域.

当检测到需要重绘的区域时通过setNeedsDisplayInRect:标记,这样就会在一次试图刷新时调用调用视图的- drawRect:(或图层代理的- drawLayer:inContext:方法)

传入- drawLayer:inContext:CGContext参数会自动被裁切以适应对应的矩形。为了确定矩形的尺寸大小,你可以用CGContextGetClipBoundingBox()方法来从上下文获得大小。调用- drawRect()会更简单,因为CGRect会作为参数直接传入。

当然如果你的裁剪逻辑相当复杂,那还是是让Core Graphics来为你重绘吧,

- (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看起来处于无响应状态。

为了可以避免用户等待绘制:

CATiledLayer

CATiledLayer除了将图层再次分割成独立更新的小块外
CATiledLayer还有一个有趣的特性:在多个线程中为每个小块同时调用- drawLayer:inContext:方法。
这就避免了阻塞用户交互而且能够利用多核心新片来更快地绘制。只有一个小块的CATiledLayer是实现异步更新图片视图的简单方法。

drawsAsynchronously

苹果为CALayer引入了这个令人好奇的属性,drawsAsynchronously属性对传入-drawLayer:inContext:CGContext进行改动,允许CGContext延缓绘制命令的执行以至于不阻塞用户交互。

它与CATiledLayer使用的异步绘制并不相同。它自己的 - drawLayer:inContext:方法只会在主线程调用,但是CGContext并不等待每个绘制命令的结束。相反地,它会将命令加入队列,当方法返回时,在后台线程逐个执行真正的绘制。

根据苹果的说法。这个特性在需要频繁重绘的视图上效果最好(比如我们的绘图应用,或者诸如UITableViewCell 之类的),对那些只绘制一次或很少重绘的图层内容来说没什么太大的帮助。