Core Animation总结(五)性能

Core Animation总结(一)图层变换(平面 立体)

Core Animation总结(二)专用图层

Core Animation总结(三)动画

Core Animation总结(四)

Core Animation总结(五)性能

Core Animation总结(六)知识点整理

#Core Animation

###性能 关于绘图和动画有两种处理的方式:CPU(中央处理器)和GPU(图形处理器),由于历史原因,我们可以说CPU所做的工作都在软件层面,而GPU在硬件层面。

有一些事情会降低(基于GPU)图层绘制,比如:

  • 太多的几何结构 - 这发生在需要太多的三角板来做变换,以应对处理器的栅格化的时候。现代iOS设备的图形芯片可以处理几百万个三角板,所以在Core Animation中几何结构并不是GPU的瓶颈所在。但由于图层在显示之前通过IPC发送到渲染服务器的时候(图层实际上是由很多小物体组成的特别重量级的对象),太多的图层就会引起CPU的瓶颈。这就限制了一次展示的图层个数(见本章后续“CPU相关操作”)。
  • 重绘 - 主要由重叠的半透明图层引起。GPU的填充比率(用颜色填充像素的比率)是有限的,所以需要避免重绘(每一帧用相同的像素填充多次)的发生。在现代iOS设备上,GPU都会应对重绘;即使是iPhone 3GS都可以处理高达2.5的重绘比率,并任然保持60帧率的渲染(这意味着你可以绘制一个半的整屏的冗余信息,而不影响性能),并且新设备可以处理更多。
  • 离屏绘制 - 这发生在当不能直接在屏幕上绘制,并且必须绘制到离屏图片的上下文中的时候。离屏绘制发生在基于CPU或者是GPU的渲染,或者是为离屏图片分配额外内存,以及切换绘制上下文,这些都会降低GPU性能。对于特定图层效果的使用,比如圆角,图层遮罩,阴影或者是图层光栅化都会强制Core Animation提前渲染图层的离屏绘制。但这不意味着你需要避免使用这些效果,只是要明白这会带来性能的负面影响。
  • 过大的图片 - 如果视图绘制超出GPU支持的2048x2048或者4096x4096尺寸的纹理,就必须要用CPU在图层每次显示之前对图片预处理,同样也会降低性能。

以下CPU的操作都会延迟动画的开始时间:

  • 布局计算 - 如果你的视图层级过于复杂,当视图呈现或者修改的时候,计算图层帧率就会消耗一部分时间。特别是使用iOS6的自动布局机制尤为明显,它应该是比老版的自动调整逻辑加强了CPU的工作。
  • 视图懒加载 - iOS只会当视图控制器的视图显示到屏幕上时才会加载它。这对内存使用和程序启动时间很有好处,但是当呈现到屏幕上之前,按下按钮导致的许多工作都会不能被及时响应。比如控制器从数据库中获取数据,或者视图从一个nib文件中加载,或者涉及IO的图片显示(见后续“IO相关操作”),都会比CPU正常操作慢得多。
  • Core Graphics绘制 - 如果对视图实现了-drawRect:方法,或者CALayerDelegate的-drawLayer:inContext:方法,那么在绘制任何东西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后,必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。
  • 解压图片 - PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多。但是在图片绘制到屏幕上之前,必须把它扩展成完整的未解压的尺寸(通常等同于图片宽 x 长 x 4个字节)。为了节省内存,iOS通常直到真正绘制的时候才去解码图片(14章“图片IO”会更详细讨论)。根据你加载图片的方式,第一次对图层内容赋值的时候(直接或者间接使用UIImageView)或者把它绘制到Core Graphics中,都需要对它解压,这样的话,对于一个较大的图片,都会占用一定的时间。

###绘图 绘图通常在Core Animation的上下文中指代软件绘图(意即:不由GPU协助的绘图)。在iOS中,软件绘图通常是由Core Graphics框架完成来完成。软件绘图不仅效率低,还会消耗可观的内存。

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

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

软件绘图的代价昂贵,除非绝对必要,你应该避免重绘你的视图。

#####矢量图 CAShapeLayer可以绘制多边形,直线和曲线。CATextLayer可以绘制文本。CAGradientLayer用来绘制渐变。这些总体上都比Core Graphics更快,同时他们也避免了创造一个寄宿图。

var path: UIBezierPath!
var layer: CAShapeLayer!
override func viewDidLoad() {
    super.viewDidLoad()
    
    path = UIBezierPath()
    
    layer = CAShapeLayer(layer: self.view.layer)
    layer.strokeColor = UIColor.red.cgColor
    layer.fillColor = UIColor.clear.cgColor
    layer.lineJoin = kCALineJoinRound
    layer.lineCap = kCALineCapRound
    layer.lineWidth = 5
    self.view.layer.addSublayer(layer)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    if let point = touches.first?.location(in: self.view){
        path.move(to: point)
    }
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    if let point = touches.first?.location(in: self.view){
        path.addLine(to: point)
        layer.path = path.cgPath
    }
}

#####脏矩形 为了减少不必要的绘制,Mac OS和iOS设备将会把屏幕区分为需要重绘的区域和不需要重绘的区域。那些需要重绘的部分被称作『脏区域』。在实际应用中,鉴于非矩形区域边界裁剪和混合的复杂性,通常会区分出包含指定视图的矩形位置,而这个位置就是『脏矩形』。

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

drawsAsynchronously属性对传入-drawLayer:inContext:的CGContext进行改动,允许CGContext延缓绘制命令的执行以至于不阻塞用户交互。 它与CATiledLayer使用的异步绘制并不相同。它自己的-drawLayer:inContext:方法只会在主线程调用,但是CGContext并不等待每个绘制命令的结束。相反地,它会将命令加入队列,当方法返回时,在后台线程逐个执行真正的绘制。 根据苹果的说法。这个特性在需要频繁重绘的视图上效果最好(比如我们的绘图应用,或者诸如UITableViewCell之类的),对那些只绘制一次或很少重绘的图层内容来说没什么太大的帮助。

###图片的载入性能 绘图实际消耗的时间通常并不是影响性能的因素。图片消耗很大一部分内存,而且不太可能把需要显示的图片都保留在内存中,所以需要在应用运行的时候周期性地加载和卸载图片。

为了在后台线程加载图片,我们可以使用GCD或者NSOperationQueue创建自定义线程,或者使用CATiledLayer。为了从远程网络加载图片,我们可以使用异步的NSURLConnection,但是对本地存储的图片,并不十分有效。

GCD(Grand Central Dispatch)和NSOperationQueue很类似,都给我们提供了队列闭包块来在线程中按一定顺序来执行。NSOperationQueue有一个Objecive-C接口(而不是使用GCD的全局C函数),同样在操作优先级和依赖关系上提供了很好的粒度控制,但是需要更多地设置代码。

一旦图片文件被加载就必须要进行解码,解码过程是一个相当复杂的任务,需要消耗非常长的时间。解码后的图片将同样使用相当大的内存。+

用于加载的CPU时间相对于解码来说根据图片格式而不同。对于PNG图片来说,加载会比JPEG更长,因为文件可能更大,但是解码会相对较快,而且Xcode会把PNG图片进行解码优化之后引入工程。JPEG图片更小,加载更快,但是解压的步骤要消耗更长的时间,因为JPEG解压算法比基于zip的PNG算法更加复杂。

如果不使用+imageNamed:,那么把整张图片绘制到CGContext可能是最佳的方式了。尽管你可能认为多余的绘制相较别的解压技术而言性能不是很高,但是新创建的图片(在特定的设备上做过优化)可能比原始图片绘制的更快。

let imgView: UIImageView = UIImageView()
DispatchQueue.global().async {
    let imgPath = ""
    let image: UIImage = UIImage(contentsOfFile: imgPath)!
    UIGraphicsBeginImageContextWithOptions(imgView.bounds.size, true, 0)
    image.draw(in: imgView.bounds)
    let img = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    DispatchQueue.main.async(execute: { 
        imgView.image = img
    })
}

CATiledLayer可以用来异步加载和显示大型图片,而不阻塞用户输入

let tiledLayer: CATiledLayer = CATiledLayer()
tiledLayer.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
tiledLayer.contentsScale = UIScreen.main.scale
//单位是像素,而不是点,所以为了保证瓦片和表格尺寸一致,需要乘以屏幕比例因子。
tiledLayer.tileSize = CGSize(width: tiledLayer.frame.size.width*UIScreen.main.scale, height: tiledLayer.frame.size.height*UIScreen.main.scale)
tiledLayer.setValue("path", forKey: "str")
tiledLayer.delegate = self
tiledLayer.needsDisplay()
self.view.layer.addSublayer(tiledLayer)

extension ViewController: CALayerDelegate{
    // MARK: CALayerDelegate
    func draw(_ layer: CALayer, in ctx: CGContext) {
        let v:String = layer.value(forKey: "str") as! String
        let tileImg: UIImage = UIImage(contentsOfFile: v)!
        let aspectRatio: CGFloat = tileImg.size.height / tileImg.size.width
        var imgRect: CGRect = CGRect.zero
        imgRect.size.width = layer.bounds.size.width
        imgRect.size.height = layer.bounds.size.height * aspectRatio
        imgRect.origin.y = (layer.bounds.size.height - imgRect.size.height)/2
        UIGraphicsPushContext(ctx)
        tileImg.draw(in: imgRect)
        UIGraphicsPopContext()
    }    
}

#####缓存 [UIImage imageNamed:]加载图片有个好处在于可以立刻解压图片而不用等到绘制的时候。但是[UIImage imageNamed:]方法有另一个非常显著的好处:它在内存中自动缓存了解压后的图片,即使你自己没有保留对它的任何引用。

  • [UIImage imageNamed:]方法仅仅适用于在应用程序资源束目录下的图片。
  • [UIImage imageNamed:]缓存用来存储应用界面的图片(按钮,背景等等)。如果对照片这种大图也用这种缓存,那么iOS系统就很可能会移除这些图片来节省内存。那么在切换页面时性能就会下降,因为这些图片都需要重新加载。对传送器的图片使用一个单独的缓存机制就可以把它和应用图片的生命周期解耦。
  • [UIImage imageNamed:]缓存机制并不是公开的,所以你不能很好地控制它。

如果要写自己的图片缓存的话,要涉及:

  • 选择一个合适的缓存键 - 缓存键用来做图片的唯一标识。如果实时创建图片,通常不太好生成一个字符串来区分别的图片。在我们的图片传送带例子中就很简单,我们可以用图片的文件名或者表格索引。
  • 提前缓存 - 如果生成和加载数据的代价很大,你可能想当第一次需要用到的时候再去加载和缓存。提前加载的逻辑是应用内在就有的
  • 缓存失效 - 如果图片文件发生了变化,怎样才能通知到缓存更新呢?一个比较好的方式就是当图片缓存的时候打上一个时间戳以便当文件更新的时候作比较。
  • 缓存回收 - 当内存不够的时候,如何判断哪些缓存需要清空呢?苹果提供了一个叫做NSCache通用的解决方案

NSCache和NSDictionary类似。你可以通过-setObject:forKey:和-object:forKey:方法分别来插入,检索。和字典不同的是,NSCache在系统低内存的时候自动丢弃存储的对象。 NSCache用来判断何时丢弃对象的算法并没有在文档中给出,但是你可以使用-setCountLimit:方法设置缓存大小,以及-setObject:forKey:cost:来对每个存储的对象指定消耗的值来提供一些暗示。 指定消耗数值可以用来指定相对的重建成本。如果对大图指定一个大的消耗值,那么缓存就知道这些物体的存储更加昂贵,于是当有大的性能问题的时候才会丢弃这些物体。你也可以用-setTotalCostLimit:方法来指定全体缓存的尺寸。

#####文件格式 图片加载性能取决于加载大图的时间和解压小图时间的权衡 PNG图片使用的无损压缩算法可以比使用JPEG的图片做到更快地解压,但是由于闪存访问的原因,这些加载的时间并没有什么区别。

###图层性能 尽可能地避免改变那些包含文本的视图的frame,因为这样做的话文本就需要重绘。

启用shouldRasterize属性会将图层绘制到一个屏幕之外的图像。然后这个图像将会被缓存起来并绘制到实际图层的contents和子图层。如果有很多的子图层或者有复杂的效果应用,这样做就会比重绘所有事务的所有帧划得来得多。但是光栅化原始图像需要时间,而且还会消耗额外的内存。

如果太多图层在屏幕外渲染(圆角、图层蒙板、阴影)依然会影响到性能。 cornerRadius和maskToBounds独立作用的时候都不会有太大的性能问题,但是当他俩结合在一起,就触发了屏幕外渲染。 可以用现成的UIBezierPath的构造器+bezierPathWithRoundedRect:cornerRadius:(.这样做并不会比直接用cornerRadius更快,但是它避免了性能问题。

//用CAShapeLayer画一个圆角矩形
let shapeLayer: CAShapeLayer = CAShapeLayer()
shapeLayer.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
shapeLayer.fillColor = UIColor.blue.cgColor
shapeLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 100, height: 100), cornerRadius: 20).cgPath
self.view.layer.addSublayer(shapeLayer)

创建圆角矩形的方法就是用一个圆形内容图片并结合,用contensCenter属性去创建一个可伸缩图片 理论上来说,这个应该比用CAShapeLayer要快,因为一个可拉伸图片只需要18个三角形(一个图片是由一个3*3网格渲染而成),然而,许多都需要渲染成一个顺滑的曲线。在实际应用上,二者并没有太大的区别。 使用可伸缩图片的优势在于它可以绘制成任意边框效果而不需要额外的性能消耗。

//用可伸缩图片绘制圆角矩形
let layer: CALayer = CALayer()
layer.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
/*
 contentsCenter 是一个CGRect,它定义了一个固定的边框和一个在图层上可拉伸的区域。 
 改变contentsCenter的值并不会影响到寄宿图的显示,除非这个图层的大小改变了,你才看得到效果。
 默认情况下,contentsCenter是{0, 0, 1, 1},这意味着如果大小(由conttensGravity决定)改变了,那么寄宿图将会均匀地拉伸开。
 效果和UIImage里的-resizableImageWithCapInsets: 方法效果非常类似
 */
layer.contentsCenter = CGRect(x: 0.5, y: 0.5, width: 0, height: 0)
/*
 contentScale属性,用来决定图层内容应该以怎样的分辨率来渲染。
 contentsScale并不关心屏幕的拉伸因素而总是默认为1.0。
 如果我们想以Retina的质量来显示文字,我们就得手动地设置CATextLayer的contentsScale属性
 */
layer.contentsScale = UIScreen.main.scale
layer.contents = UIImage(named: "bg.jpg")?.cgImage
self.view.layer.addSublayer(layer)

合并不同图层的透明重叠像素(即混合)消耗的资源也是相当客观的。所以为了加速处理进程,不到必须时刻不要使用透明图层。

  • 给视图的backgroundColor属性设置一个固定的,不透明的颜色
  • 设置opaque属性为YES

这样做减少了混合行为(因为编译器知道在图层之后的东西都不会对最终的像素颜色产生影响)并且计算得到了加速,避免了过度绘制行为因为Core Animation可以舍弃所有被完全遮盖住的图层,而不用每个像素都去计算一遍。 使用shouldRasterize属性,可以将一个固定的图层体系折叠成单张图片,这样就不需要每一帧重新合成了,也就不会有因为子图层之间的混合和过度绘制的性能问题了

var transform: CATransform3D = CATransform3DIdentity
transform.m34 = -1 / 500
self.view.layer.sublayerTransform = transform

let depth = 10
let height = 10
let width = 10
let space = 100
for i in 0 ..< depth {
    for j in 0 ..< height {
        for k in 0 ..< width{
            let layer: CALayer = CALayer()
            layer.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
            layer.position = CGPoint(x: k*space, y: j*space)
            layer.zPosition = CGFloat(-i) * CGFloat(space)
            layer.backgroundColor = UIColor(white: CGFloat(1-i*(1/depth)), alpha: 1).cgColor
            layer.borderWidth = 1
            self.view.layer.addSublayer(layer)
        }
    }
}

使用CALayer的-renderInContext:方法,你可以将图层及其子图层快照进一个Core Graphics上下文然后得到一个图片,它可以直接显示在UIImageView中,或者作为另一个图层的contents。不同于shouldRasterize 要求图层与图层树相关联 ,这个方法没有持续的性能消耗。 当图层内容改变时,刷新这张图片的机会取决于你(不同于shouldRasterize,它自动地处理缓存和缓存验证),但是一旦图片被生成,相比于让Core Animation处理一个复杂的图层树,你节省了相当客观的性能。


参考资料:https://zsisme.gitbooks.io/ios-/content/index.html

转载于:https://my.oschina.net/asjoker/blog/788957

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值