前言
动画的流畅性,是让iPhone俘虏广大果粉的心的一个重要的因素.但是我们在开发iOS APP的时候,却会经常遇到各种动画不能流畅表现的情况.下面将会介绍一些能让APP动画重回流畅的方法.
CPU和GPU,哪一块才是短板?
为了更好的实现动画的优化,我们首先需要了解一段动画在iOS中呈现需要经过哪些步骤.
从苹果在WWDC 2012中展示的可以看出,一段动画从创建到展示需要经过三个主要的过程:
- 创建动画并且更新动画层级
- 准备并且提交动画
- 渲染
在这三个过程中,第一第二个过程是在CPU
中被处理的,而第三个过程:渲染,则是由GPU
所负责.所以,当我们需要优化动画的流畅性的时候,我们首先需要知道到底是CPU和GPU中的哪一个负担过重.关于如何使用instruments找出性能短板,请参考这篇文章:UIKit性能调优实战讲解.
CPU Bound
如果我们在在Time Profiler中发现一个方法的占用时间到达百分之90以上,那么这个方法很大可能是导致UI卡顿的罪魁祸首.在常见的应用中,我们在显示一些内容之前,CPU需要做的工作有以下几个:
一.布局
布局即确定各个视图所处的位置,各个视图的大小等.另外,视图显示各种数据也在这个时候得到处理.
AutoLayout
的布局在此时会被计算.虽然AutoLayout能让我们在设计应用的时候写更少的代码,但是手工计算各个视图的布局显然能让CPU的负担更少.在一些环境下,我们或许需要作出权衡.图片缓存
也可以减少CPU在布局时的工作.通常情况下,所有被设置为一个layer的content的CGImage
会被系统自动缓存.但是一旦系统发出内存警告,这些缓存会被删除.可以考虑使用SDWebImage
之类的库来制定更多自定义的缓存策略.- 重用view和cell.
二.显示
这里的显示是指系统调用UIView
的-drawRect
方法或者CALayerDelegate
的-displayLayer:
方法.这些方法常用于实现画板或者各种形状各异的控件.无论是调用,-drawRect
还是调用-displayLayer:
,系统都会创建一个Backing Store,将我们想要的图案绘制到Backing Store中.
Backing Store耗费的内存是巨大的,可以看看这篇文章:内存恶鬼drawRect.所以除非是在必要的情况下,我们不应该重载这些方法.针对显示这个步骤我们可以做的有:
- 使用
CAShapeLayer
来绘制线条. - 不使用
[UIColor SetFill]
这类方法而使用[UIView setBackgroundColor]
方法来设置背景颜色. - 如需使用
-drawRect
,使用-setNeedsDisplayInRect:
来指定需要更新的部分而不是使用-setNeedsDisplay
,这会导致整个layer被重新绘制.
三.准备
在这一步中,系统对显示的图片进行转码操作.
当我们创建一个UIImageView时,系统不会立即为我们对该view指向的图片进行解码,而是当该view需要被展示的时候,在这一步对图片进行解码.以下几点可以让这一步的工作量更少:
- 对于UI元素,使用
PNG
格式的图片.对于拖入Xcode的PNG图片,Xcode会对其进行优化. - 对于信息量大的图片例如照片,使用
JPEG
格式. - 使用尺寸恰当的图片
- 尽可能使用不透明的图片
四.提交
这一步CPU向GPU传递layers和参数.减少视图层级可以减少这一步的耗时.
GPU Bound
当动画被提交之后,就到了GPU的工作时间.对于被传进来的动画,GPU需要在一秒之内将其渲染60次.所以如果任务过重,会导致GPU不能在1/60秒内完成该次渲染,导致掉帧.
减少GPU的工作负荷,我们可以从这几点着手.
减少离屏渲染
减少离屏渲染.离屏渲染指的是GPU从当前的帧缓冲区之外的缓冲区进行绘制操作,然后切换环境回到当前缓冲区.离屏渲染对性能影响很大.常见会导致有离屏渲染问题的操作有:
- 圆角+masking
- layer的阴影
为了减少离屏渲染,可以使用一下的方法:
- 把图片预先处理成圆形而不是用mask.
- 使用shadowPath让GPU知道阴影的形状而不是在渲染时再计算阴影区域.
光栅化
光栅化是指对当前layer和其所有sublayer通过bitmap形式缓存起来.使用光栅化的代码如下:1
view.layer.shouldRasterize = YES;
之后如果再次需要展示该layer,则直接使用缓存起来的内容而不是重新渲染.但是,不是所有内容都适合光栅化.光栅化有以下几点需要注意的:
- 可以光栅化的空间大小为屏幕的2.5倍
- 当layer内容变化,缓存失效
- 如果100ms没有使用到缓存,则会被清除
因此,应用光栅化的layer应该具备以下条件:
- 该layer层级复杂
- 该layer实现的是简单的动画,即移动,变形,大小改变等.而sublayer不变.
另外需要注意的是,光栅化由CPU负责处理,使用光栅化会增加CPU的负担,所以需要做出权衡。
减少图层混合
对于isOpaque
为true的layer,系统会在渲染时进行优化,减少GPU的工作量.所以对于不透明的layer,将isOpaque
设置为true.
Scrolling
Scrolling
动画极其常见,当我们使用tableView或者scrollView的时候我们都会和滚动动画打交道.滚动动画的实现形式和前面说的动画有点不同,这里针对Scrolling单独说说.
scrolling的每一帧之前都会需要CPU准备,而不像一般的动画那样一经CPU提交就能高枕无忧了.在每1/60秒的时间内,都需要经过上图所说的三个步骤,所以留给渲染的时间不是1/60秒,而是比1/60秒更短,准确来说,越短越好.针对Scrolling,除了上面所说的方法外,还有几个方法能提高Scrolling的体验.
异步处理
如果我们在主线程中执行很多繁重的工作,那么这些工作就会有可能阻塞主线程进行UI响应.更好的办法是在另一条线程中处理这些繁重的工作,当这些工作完成后,通知主线程.对于tableView,我们可以实现:
- 异步数据处理:把网络请求,数据库请求,数据下载等工作放在其他线程,等取得所需数据后再在主线程中更新view.
- 异步绘图:把view的绘制放到其他线程.
-drawRect
只能在主线程中运行,所以我们不使用-drawRect
.我们可以在其他线程中使用UIGraphicsBeginImageContextWithOptions
新建一个绘图上下文,在这个上下文中绘制,而这些绘制可以在其他线程中执行.在绘制完成后,在主线程设置layer的content.而被设置成content的CGImage会被系统自动缓存,所以这些绘制工作不需要每次显示layer都调用.
注意,因为tableView使用的cell是可重用的,而该cell可能已经被滑出屏幕,但是异步操作使得该cell仍然被设置了,那么当该cell需要展示另一组数据的时候,会有短暂的时间是显示之前的数据的.所以,我们应该在设置cell前使用func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
方法取得cell,而不是通过闭包的自动变量捕获来获得cell.使用func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
方法的好处是:当cell被滑出屏幕.该方法返回nil,那样我们就可以通过判断cell是否为nil来决定是否对cell进行操作.
取消处理
使用异步处理会导致一个问题:当处理被提交到其他线程,而这些处理的结果已经不被需要的时候,就像用户滑动一个tableView,而tableView中的cell会被异步显示出来.但是用户滑过了这些cell,导致这些cell的内容不再需要被显示.这时就需要用到取消处理.如果我们使用NSOperationQueue
提交任务的话,我们就可以取消提交了的但是还没执行的任务.例如:在用户打开一个展示一个tableView的view时,请求tableView展示的cell的数据并且进行异步绘制.在这些操作完成之前用户关闭了这个view,那么就可以调用queue.cancelAllOperations()
.
当一个cell被滑出屏幕的时候,系统会调用UITableViewDelegate
的tableView:didEndDisplayingCell:forRowAtIndexPath:
方法.我们可以在这里该cell对应的operation
被取消的operation的isCancelled为true.当我们要执行一个operation之前,我们可以观察该属性,如果operation被取消了,提早退出.
总结
在我们遇到动画不流畅的问题时,我们一定要先弄懂该问题是CPU负担过重还是GPU负担过重导致的,然后做出相应的改进.如果我们面对的是Scrolling动画,我们除了使用以上的方法之外,我们还应该尝试使用异步处理并且实现取消操作,保持滑动的流畅.
参考资料
WWDC2012 Session 211 - Building Concurrent User Interfaces on iOS
WWDC2012 Session 238 - iOS App Performance Graphics and Animation
WWDC2012 Session 506 - Optimizing 2D Graphics and Animation Performance
WWDC2014 Session 419 - Advanced Graphics and Animation Performance