绘图

不必要的效率考虑往往是性能问题的万恶之源。 ——William Allan Wulf

在第12章『速度的曲率』我们学习如何用Instruments来诊断Core Animation性能问题。在构建一个iOS app的时候会遇到很多潜在的性能陷阱,但是在本章我们将着眼于有关绘制的性能问题。

软件绘图

术语绘图通常在Core Animation的上下文中指代软件绘图(意即:不由GPU协助的绘图)。在iOS中,软件绘图通常是由Core Graphics框架完成来完成。但是,在一些必要的情况下,相比Core Animation和OpenGL,Core Graphics要慢了不少。

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

但是一旦你实现了CALayerDelegate协议中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(其实就是前者的包装方法),图层就创建了一个绘制上下文,这个上下文需要的大小的内存可从这个算式得出:图层宽*图层高*4字节,宽高的单位均为像素。对于一个在Retina iPad上的全屏图层来说,这个内存量就是 2048*1526*4字节,相当于12MB内存,图层每次重绘的时候都需要重新抹掉内存然后重新分配。

软件绘图的代价昂贵,除非绝对必要,你应该避免重绘你的视图。提高绘制性能的秘诀就在于尽量避免去绘制。

矢量图形

我们用Core Graphics来绘图的一个通常原因就是只是用图片或是图层效果不能轻易地绘制出矢量图形。矢量绘图包含一下这些:

  • 任意多边形(不仅仅是一个矩形)
  • 斜线或曲线
  • 文本
  • 渐变

举个例子,清单13.1 展示了一个基本的画线应用。这个应用将用户的触摸手势转换成一个UIBezierPath上的点,然后绘制成视图。我们在一个UIView子类DrawingView中实现了所有的绘制逻辑,这个情况下我们没有用上view controller。但是如果你喜欢你可以在view controller中实现触摸事件处理。图13.1是代码运行结果。

清单13.1 用Core Graphics实现一个简单的绘图应用

  1. #import "DrawingView.h"
  2.  
  3. @interface DrawingView ()
  4.  
  5. @property (nonatomic, strong) UIBezierPath *path;
  6.  
  7. @end
  8.  
  9. @implementation DrawingView
  10.  
  11. - (void)awakeFromNib
  12. {
  13. //create a mutable path
  14. self.path = [[UIBezierPath alloc] init];
  15. self.path.lineJoinStyle = kCGLineJoinRound;
  16. self.path.lineCapStyle = kCGLineCapRound;

  17. self.path.lineWidth = ;
  18. }
  19.  
  20. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  21. {
  22. //get the starting point
  23. CGPoint point = [[touches anyObject] locationInView:self];
  24.  
  25. //move the path drawing cursor to the starting point
  26. [self.path moveToPoint:point];
  27. }
  28.  
  29. - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
  30. {
  31. //get the current point
  32. CGPoint point = [[touches anyObject] locationInView:self];
  33.  
  34. //add a new line segment to our path
  35. [self.path addLineToPoint:point];
  36.  
  37. //redraw the view
  38. [self setNeedsDisplay];
  39. }
  40.  
  41. - (void)drawRect:(CGRect)rect
  42. {
  43. //draw path
  44. [[UIColor clearColor] setFill];
  45. [[UIColor redColor] setStroke];
  46. [self.path stroke];
  47. }
  48. @end

图13.1 用Core Graphics做一个简单的『素描』

这样实现的问题在于,我们画得越多,程序就会越慢。因为每次移动手指的时候都会重绘整个贝塞尔路径(UIBezierPath),随着路径越来越复杂,每次重绘的工作就会增加,直接导致了帧数的下降。看来我们需要一个更好的方法了。

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

如果稍微将之前的代码变动一下,用CAShapeLayer替代Core Graphics,性能就会得到提高(见清单13.2).虽然随着路径复杂性的增加,绘制性能依然会下降,但是只有当非常非常浮躁的绘制时才会感到明显的帧率差异。

清单13.2 用CAShapeLayer重新实现绘图应用

  1. #import "DrawingView.h"
  2. #import <QuartzCore/QuartzCore.h>
  3.  
  4. @interface DrawingView ()
  5.  
  6. @property (nonatomic, strong) UIBezierPath *path;
  7.  
  8. @end

  9. @implementation DrawingView
  10.  
  11. + (Class)layerClass
  12. {
  13. //this makes our view create a CAShapeLayer
  14. //instead of a CALayer for its backing layer
  15. return [CAShapeLayer class];
  16. }
  17.  
  18. - (void)awakeFromNib
  19. {
  20. //create a mutable path
  21. self.path = [[UIBezierPath alloc] init];
  22.  
  23. //configure the layer
  24. CAShapeLayer *shapeLayer = (CAShapeLayer *)self.layer;
  25. shapeLayer.strokeColor = [UIColor redColor].CGColor;
  26. shapeLayer.fillColor = [UIColor clearColor].CGColor;
  27. shapeLayer.lineJoin = kCALineJoinRound;
  28. shapeLayer.lineCap = kCALineCapRound;
  29. shapeLayer.lineWidth = ;
  30. }
  31.  
  32. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  33. {
  34. //get the starting point
  35. CGPoint point = [[touches anyObject] locationInView:self];
  36.  
  37. //move the path drawing cursor to the starting point
  38. [self.path moveToPoint:point];
  39. }
  40.  
  41. - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
  42. {
  43. //get the current point
  44. CGPoint point = [[touches anyObject] locationInView:self];
  45.  
  46. //add a new line segment to our path
  47. [self.path addLineToPoint:point];
  48.  
  49. //update the layer with a copy of the path
  50. ((CAShapeLayer *)self.layer).path = self.path.CGPath;
  51. }
  52. @end

脏矩形

有时候用CAShapeLayer或者其他矢量图形图层替代Core Graphics并不是那么切实可行。比如我们的绘图应用:我们用线条完美地完成了矢量绘制。但是设想一下如果我们能进一步提高应用的性能,让它就像一个黑板一样工作,然后用『粉笔』来绘制线条。模拟粉笔最简单的方法就是用一个『线刷』图片然后将它粘贴到用户手指碰触的地方,但是这个方法用CAShapeLayer没办法实现。

我们可以给每个『线刷』创建一个独立的图层,但是实现起来有很大的问题。屏幕上允许同时出现图层上线数量大约是几百,那样我们很快就会超出的。这种情况下我们没什么办法,就用Core Graphics吧(除非你想用OpenGL做一些更复杂的事情)。

我们的『黑板』应用的最初实现见清单13.3,我们更改了之前版本的DrawingView,用一个画刷位置的数组代替UIBezierPath。图13.2是运行结果

清单13.3 简单的类似黑板的应用

  1. #import "DrawingView.h"
  2. #import <QuartzCore/QuartzCore.h>
  3. #define BRUSH_SIZE 32
  4.  
  5. @interface DrawingView ()
  6.  
  7. @property (nonatomic, strong) NSMutableArray *strokes;
  8.  
  9. @end
  10.  
  11. @implementation DrawingView
  12.  
  13. - (void)awakeFromNib
  14. {
  15. //create array
  16. self.strokes = [NSMutableArray array];
  17. }
  18.  
  19. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  20. {
  21. //get the starting point
  22. CGPoint point = [[touches anyObject] locationInView:self];
  23.  
  24. //add brush stroke
  25. [self addBrushStrokeAtPoint:point];
  26. }
  27.  
  28. - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
  29. {
  30. //get the touch point
  31. CGPoint point = [[touches anyObject] locationInView:self];
  32.  
  33. //add brush stroke
  34. [self addBrushStrokeAtPoint:point];
  35. }
  36.  
  37. - (void)addBrushStrokeAtPoint:(CGPoint)point
  38. {
  39. //add brush stroke to array
  40. [self.strokes addObject:[NSValue valueWithCGPoint:point]];
  41.  
  42. //needs redraw
  43. [self setNeedsDisplay];
  44. }
  45.  
  46. - (void)drawRect:(CGRect)rect
  47. {
  48. //redraw strokes
  49. for (NSValue *value in self.strokes) {
  50. //get point
  51. CGPoint point = [value CGPointValue];
  52.  
  53. //get brush rect
  54. CGRect brushRect = CGRectMake(point.x - BRUSH_SIZE/, point.y - BRUSH_SIZE/, BRUSH_SIZE, BRUSH_SIZE);
  55.  
  56. //draw brush stroke 
  57. [[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];
  58. }
  59. }
  60. @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:来减少不必要的绘制

  1. - (void)addBrushStrokeAtPoint:(CGPoint)point
  2. {
  3. //add brush stroke to array
  4. [self.strokes addObject:[NSValue valueWithCGPoint:point]];
  5.  
  6. //set dirty rect
  7. [self setNeedsDisplayInRect:[self brushRectForPoint:point]];
  8. }
  9.  
  10. - (CGRect)brushRectForPoint:(CGPoint)point
  11. {
  12. return CGRectMake(point.x - BRUSH_SIZE/, point.y - BRUSH_SIZE/, BRUSH_SIZE, BRUSH_SIZE);
  13. }
  14.  
  15. - (void)drawRect:(CGRect)rect
  16. {
  17. //redraw strokes
  18. for (NSValue *value in self.strokes) {
  19. //get point
  20. CGPoint point = [value CGPointValue];
  21.  
  22. //get brush rect
  23. CGRect brushRect = [self brushRectForPoint:point];

  24. //only draw brush stroke if it intersects dirty rect
  25. if (CGRectIntersectsRect(rect, brushRect)) {
  26. //draw brush stroke
  27. [[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];
  28. }
  29. }
  30. }

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

[iOS Animation]-CALayer 绘图效率的更多相关文章

  1. [iOS Animation]-CALayer 性能优化

    性能优化 代码应该运行的尽量快,而不是更快 - 理查德 在第一和第二部分,我们了解了Core Animation提供的关于绘制和动画的一些特性.Core Animation功能和性能都非常强大,但如果 ...

  2. [iOS Animation]-CALayer 图层性能

    图层性能 要更快性能,也要做对正确的事情. ——Stephen R. Covey 在第14章『图像IO』讨论如何高效地载入和显示图像,通过视图来避免可能引起动画帧率下降的性能问题.在最后一章,我们将着 ...

  3. [iOS Animation]-CALayer 显示动画

    显式动画 如果想让事情变得顺利,只有靠自己 -- 夏尔·纪尧姆 上一章介绍了隐式动画的概念.隐式动画是在iOS平台创建动态用户界面的一种直接方式,也是UIKit动画机制的基础,不过它并不能涵盖所有的动 ...

  4. [iOS Animation]-CALayer 专用图层

    专用图层 复杂的组织都是专门化的 Catharine R. Stimpson 到目前为止,我们已经探讨过CALayer类了,同时我们也了解到了一些非常有用的绘图和动画功能.但是Core Animati ...

  5. [iOS Animation]-CALayer 变换

    变换 很不幸,没人能告诉你母体是什么,你只能自己体会 -- 骇客帝国 在第四章“可视效果”中,我们研究了一些增强图层和它的内容显示效果的一些技术,在这一章中,我们将要研究可以用来对图层旋转,摆放或者扭 ...

  6. [iOS Animation]-CALayer 图层几何学

    图层几何学 不熟悉几何学的人就不要来这里了 --柏拉图学院入口的签名 在第二章里面,我们介绍了图层背后的图片,和一些控制图层坐标和旋转的属性.在这一章中,我们将要看一看图层内部是如何根据父图层和兄弟图 ...

  7. [iOS Animation]-CALayer 显示方式

    寄宿图 图片胜过千言万语,界面抵得上千图片 ——Ben Shneiderman 我们在第一章『图层树』中介绍了CALayer类并创建了一个简单的有蓝色背景的图层.背景颜色还好啦,但是如果它仅仅是展现了 ...

  8. [iOS Animation]-CALayer 图层树

    图层的树状结构 巨妖有图层,洋葱也有图层,你有吗?我们都有图层 -- 史莱克 Core Animation其实是一个令人误解的命名.你可能认为它只是用来做动画的,但实际上它是从一个叫做Layer Ki ...

  9. [iOS Animation]-CALayer 图像IO

    图像IO 潜伏期值得思考 - 凯文 帕萨特 在第13章“高效绘图”中,我们研究了和Core Graphics绘图相关的性能问题,以及如何修复.和绘图性能相关紧密相关的是图像性能.在这一章中,我们将研究 ...

随机推荐

  1. 观光公交noip<贪心>

    题目链接:https://www.oj.swust.edu.cn/problem/show/1190 思路: 每在一段路上使用一次加速器,就会对某些人或者说某些路段上的人产生影响,目的是使产生的影响最 ...

  2. python 第三方库下载

    C:\Python27\Scripts 路径下: easy_install.exe: C:\Python27\Scripts>easy_install.exe pycrypto pip.exe: ...

  3. [转]startActivityForResult的用法和demo

    有时候我们需要把A activity提交数据给B  activity处理,然后把结果返回给A 这种方式在很多种情况需要用到,比如我应用的程序需要有拍照上传的功能. 一种解决方案是  我的应用程序 〉调 ...

  4. 用python计算md5,sha1,crc32

    Linux下计算md5sum,sha1sum,crc: 命令 输出 $md5sum hello f19dd746bc6ab0f0155808c388be8ff0  hello $sha1sum hel ...

  5. Django - 通用视图

    urls.py from . import views ... url(r'^$', views.IndexView.as_view, name="index"), url(r'^ ...

  6. Json.net对数据的解析

    在官网下载Json.net文件后,解压完将Net20下面的DLL复制到Assets目录下. using UnityEngine; using System.Collections; using New ...

  7. 使用Java的BlockingQueue实现生产者-消费者

    http://tonl.iteye.com/blog/1936391 使用Java的BlockingQueue实现生产者-消费者 博客分类: Java JavaBlockingQueue阻塞队列  B ...

  8. 解决 Oracle 11g 不能导出空表的问题

    --解决 Oracle 11g 不能导出空表的问题 --执行下面语句,查询数据库中的空表,同时产生分配空间.把生成的结果复制出来并执行. select 'alter table '||table_na ...

  9. PHP字节格式化

    /** * 容量转换 * @param string $value 字节数值 * @return string */function GBKB($value) { $size = ($value &g ...

  10. HDU 5215 BestCoder"杯中国大学生程序设计冠军赛” 边双连通分量取出子图+二分染色判图内奇偶环

    Cycle Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 131072/131072 K (Java/Others)Total Sub ...