iOS核心动画高级技术(十三) 高效绘图

More computing sins are committed in the name of efficiency (without necessarily achieving it) than for any other single reason—including blind stupidity. 不必要的效率考虑往往是性能问题的万恶之源。 ——William Allan Wulf

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

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

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

软件绘图的代价昂贵,除非绝对必要,你应该避免重绘你的视图。提高绘制性能的秘诀就在于尽量避免去绘制。 #矢量图形 我们用Core Graphics来绘图的一个通常原因就是只是用图片或是图层效果不能轻易地绘制出矢量图形。矢量绘图包含一下这些:

  • 任意多边形(不仅仅是一个矩形)
  • 斜线或曲线
  • 文本
  • 渐变 举个例子,清单13.1 展示了一个基本的画线应用。这个应用将用户的触摸手势转 换成一个UIBezierPath上的点,然后绘制成视图。我们在一个 UIView 子类DrawingView中实现了所有的绘制逻辑,这个情况下我们没有用上view controller。但是如果你喜欢你可以在view controller中实现触摸事件处理。图13.1 是代码运行结果。
#import "DrawingView.h"
@interface DrawingView ()
@property (nonatomic, strong) UIBezierPath *path; 
@end
@implementation DrawingView
- (void)awakeFromNib {//create a mutable pathself.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 pointCGPoint 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 pointCGPoint 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
复制代码

这样实现的问题在于,我们画得越多,程序就会越慢。因为每次移动手指的时候都会重绘整个贝塞尔路径( 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 pathself.path = [[UIBezierPath alloc] init];//configure the layerCAShapeLayer *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 pointCGPoint 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 pointCGPoint 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 arrayself.strokes = [NSMutableArray array];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {//get the starting pointCGPoint point = [[touches anyObject] locationInView:self];//add brush stroke[self addBrushStrokeAtPoint:point]; 
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {//get the touch pointCGPoint 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 strokesfor (NSValue *value in self.strokes) {//get pointCGPoint point = [value CGPointValue];//get brush rectCGRect brushRect = CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE); [[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];}}
@end
复制代码

这个实现在模拟器上表现还不错,但是在真实设备上就没那么好了。问题在于每次手指移动的时候我们就会重绘之前的线刷,即使场景的大部分并没有改变。我们 绘制地越多,就会越慢。随着时间的增加每次重绘需要更多的时间,帧数也会下降 (见图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 strokesfor (NSValue *value in self.strokes) {//get pointCGPoint point = [value CGPointValue];//get brush rectCGRect brushRect = [self brushRectForPoint:point];//only draw brush stroke if it intersects dirty rectif (CGRectIntersectsRect(rect, brushRect)) {//draw brush stroke[[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];}} 
}
复制代码

#异步绘制 UIKit的单线程天性意味着寄宿图通畅要在主线程上更新,这意味着绘制会打断用户交互,甚至让整个app看起来处于无响应状态。我们对此无能为力,但是如果能避免用户等待绘制完成就好多了。

针对这个问题,有一些方法可以用到:一些情况下,我们可以推测性地提前在另外一个线程上绘制内容,然后将由此绘出的图片直接设置为图层的内容。这实现起 来可能不是很方便,但是在特定情况下是可行的。Core Animation提供了一些选 择: CATiledLayerdrawsAsynchronously 属性。 #CATiledLayer 我们在第六章简单探索了一下CATiledLayer。除了将图层再次分割成独立更新的小块(类似于脏矩形自动更新的概念), CATiledLayer还有一个有趣的特性:在多个线程中为每个小块同时调用-drawLayer:inContext:方法。这就避免了阻塞用户交互而且能够利用多核心新片来更快地绘制。只有一个小块的 CATiledLayer 是实现异步更新图片视图的简单方法。 #drawsAsynchronously iOS 6中,苹果为 引入了这个令人好奇的属性,drawsAsynchronously属性对传入 -drawLayer:inContext: 的 CGContext进行改动,允许CGContext延缓绘制命令的执行以至于不阻塞用户交互。

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

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

#总结 本章我们主要围绕用Core Graphics软件绘制讨论了一些性能挑战,然后探索了一些改进方法:比如提高绘制性能或者减少需要绘制的数量。 第14章,『图像IO』,我们将讨论图片的载入性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/278901.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

dropbox链接过期_询问操作方法:“开始”菜单中的Dropbox,了解符号链接和翻录TV系列DVD...

dropbox链接过期This week we take a look at how to incorporate Dropbox into your Windows Start Menu, understanding and using symbolic links, and how to rip your TV series DVDs right to unique and high-quality episode files. 本周&#xff0c;我们来看看如何将D…

android listpreference 自定义,Android – 我的ListPreference中的自定义行布局

在我的Android应用程序中,我实现了从ListPreference扩展的类SubtitleColorListPreference.我需要这个,因为我需要为列表中的每个项目设置自己的布局.一切正常,它看起来像这样&#xff1a;重要的代码是onPrepareDialogBu​​ilder(AlertDialog.Builder builder)中的方法,我在其中…

springMVC3学习(十一)--文件上传CommonsMultipartFile

版权声明&#xff1a;本文为博主原创文章&#xff0c;未经博主同意不得转载。 https://blog.csdn.net/itmyhome/article/details/27976873 使用springMVC提供的CommonsMultipartFile类进行读取文件须要用到上传文件的两个jar包 commons-logging.jar、commons-io-xxx.jar1、在sp…

基于React和SpringBoot的快速开发模板QuickAdmin

经过一段时间的总结和完善&#xff0c;我的管理系统快速开发模板已经基本成型&#xff0c;现在GitHub上开源啦&#xff1a; QuickAdmin QuickAdmin是基于Spring Boot和React.js实现的管理系统开发框架。用于开发网站的后台管理系统。 本框架提供了如下功能&#xff1a; 完整的基…

android sim iso,android – 意外的telephonyManager.getSimCountryIso()行为

您可以使用MCC MNC获取SIM卡国家/地区,它是SIM配置的,与您所在的网络无关.Configuration config getResources().getConfiguration();int countryCode config.mcc;您可以在此处找到MCC列表MccTable.java例如,西班牙是214,法国是208MCC should work on all GSM devices with S…

火狐 增强查找工具栏_在“提示”框中:简单的IE至Firefox同步,轻松的Windows工具栏和识别USB电缆...

火狐 增强查找工具栏() Every week we tip into our mail bag and share great tips from your fellow readers. This week we’re looking at an easy way to sync your bookmarks between IE and Firefox, using simple Windows toolbars, and a clever way to ID USB cables…

day22 模块-collections,time,random,pickle,shelve等

一、引入模块的方式: 1. 认识模块 模块可以认为是一个py文件. 模块实际上是我们的py文件运行后的名称空间 导入模块: 1. 判断sys.modules中是否已经导入过该模块 2. 开辟一个内存 3. 在这个内存中执行该py文件 4. 给这个内存起个名字&#xff0c; 一般用的是py文件的名字。返回…

基于Redis实现分布式锁,避免重复执行定时任务

Spring提供了定时任务的功能&#xff0c;但是在多个实例的集群中&#xff0c;会出现定时任务重复执行多次的情况。 使用Qutaz框架自带的分布式定时任务可以很好的解决这个问题&#xff0c;但是讲道理功能有些过于强大&#xff0c;对于需求不高&#xff0c;乃至可以一定程度上允…

Input Director使用一个键盘和鼠标即可控制多台Windows计算机

The problem is having two or more PC’s and having to go back and forth between workstation. Input Director solves the problem by allowing you to control multiple Windows systems with only one keyboard and mouse on the Master PC. 问题是拥有两台或更多台PC…

viper4android 生效,另一种让V4a音效在Poweramp上生效的方法

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼本人按照网上的方法进Poweramp设置—音频—高级选项—直接音量控制—不打勾后 V4a音效没有生效 我又把V4a音效兼容模式里的正常模式切换成为兼容模式 结果还是不行......后来我郁闷了三天三夜有一天我去了v4a官网论坛看到了admin帖…

[学习笔记]状压dp

状压 \(dp\) 1、[SDOI2009]Bill的挑战 \(f[i][j]\) 表示匹配到字符串的第 \(i\) 位状态为 \(j\) 的方案数 那么方程就很明显了&#xff0c;每次枚举第 \(i\) 位的字母 \(alpha\) 然后 \(O(n)\) 判断就好了 时间复杂度 \(O(26Tlen2^nn)\) \(Code\ Below:\) #include <bits/st…

excel导入csv文件_如何将包含以0开头的列的CSV文件导入Excel

excel导入csv文件Microsoft Excel will automatically convert data columns into the format that it thinks is best when opening comma-separated data files. For those of us that don’t want our data changed, we can change that behavior. Microsoft Excel将在打开…

MySQL之进化篇

MySQL之实用篇 MySQL之牛刀小试 子查询是指出现在其他SQL语句内的SELECT子句. 例如: SELECT * FROM t1 WHERE column1 (SELECT column2 FROM t2) 其中 SELECT * FRIN t1 称为outerQuery SELECT column2 FROM t2 称为subQuery 注意:子查询指嵌套在查询内部,且必须始终出现在圆括…

android 9.0新ui,SystemUI分析(Android9.0)

8种机械键盘轴体对比本人程序员&#xff0c;要买一个写代码的键盘&#xff0c;请问红轴和茶轴怎么选&#xff1f;一、SystemUI组成SystemUI是Android的系统界面&#xff0c;包括状态栏statusbar、锁屏keyboard、任务列表recents等等&#xff0c;都继承于SystemUI这个类&#xf…

WMI技术介绍和应用——WMI概述

https://blog.csdn.net/breaksoftware/article/details/8424317转载于:https://www.cnblogs.com/diyunpeng/p/9982885.html

解决App启动时白屏的问题

第一次 03-25 11:02:34.431 6908-6908/com.newenergyjinfu.jytz D/App: before_onCreate: 239 03-25 11:02:34.513 6908-6908/com.newenergyjinfu.jytz D/App: after_initOkGo( initPicasso): 316 03-25 11:02:34.570 6908-6908/com.newenergyjinfu.jytz D/App: after_ J…

chromebook刷机_如何为不支持Chrome操作系统的网站欺骗Chromebook用户代理

chromebook刷机Not all browsers handle websites the same, and if they don’t support your operating system or browser, you could be denied access. Luckily, you can spoof the user agent on Chrome OS to make it look like you use a completely different system.…

什么时候可以升级HarmonyOS,华为鸿蒙OS即将迎来升级 手机版本或仍需时间

原标题&#xff1a;华为鸿蒙OS即将迎来升级 手机版本或仍需时间在2019年的华为开发者大会上&#xff0c;华为消费者业务CEO余承东正式对外发布了HarmonyOS。时隔一年后&#xff0c;华为开发者大会2020即将拉开帷幕。此次大会&#xff0c;HarmonyOS无疑仍会是重头戏之一&#xf…

Shell_mysql命令以及将数据导入Mysql数据库

连接MYSQL数据库 mysql -h${db_ip} -u${db_user} -p${db_pawd} -P${db_port} -D${db_name} -s -e "${sql}" db_ip&#xff1a;主机地址 db_user &#xff1a;数据库用户名 db_pwd&#xff1a;密码 db_port&#xff1a;端口号 db_name&#xff1a;数据库名称 sql&…

cocos android-1,cocos2dx在windows下开发,编译到android上(1)

转自&#xff1a;http://www.2cto.com/kf/201205/130697.html下面我给大家介绍下&#xff0c;用vs2010开发cocos2dx&#xff0c;然后如何使其编译到android上。步骤如下&#xff1a;1、必要条件&#xff0c;你的eclipse能把代码编译到安卓手机或虚拟机上&#xff0c;如果这一步…