Farlanki

总结一下让动画保持流畅的几个方法

字数统计: 2.5k阅读时长: 8 min
2017/03/22 Share

前言

动画的流畅性,是让iPhone俘虏广大果粉的心的一个重要的因素.但是我们在开发iOS APP的时候,却会经常遇到各种动画不能流畅表现的情况.下面将会介绍一些能让APP动画重回流畅的方法.

CPU和GPU,哪一块才是短板?

为了更好的实现动画的优化,我们首先需要了解一段动画在iOS中呈现需要经过哪些步骤.


从苹果在WWDC 2012中展示的可以看出,一段动画从创建到展示需要经过三个主要的过程:

  1. 创建动画并且更新动画层级
  2. 准备并且提交动画
  3. 渲染

在这三个过程中,第一第二个过程是在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被滑出屏幕的时候,系统会调用UITableViewDelegatetableView: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

CATALOG
  1. 1. 前言
  2. 2. CPU和GPU,哪一块才是短板?
  3. 3. CPU Bound
    1. 3.1. 一.布局
    2. 3.2. 二.显示
    3. 3.3. 三.准备
    4. 3.4. 四.提交
  4. 4. GPU Bound
    1. 4.1. 减少离屏渲染
    2. 4.2. 光栅化
    3. 4.3. 减少图层混合
  5. 5. Scrolling
    1. 5.1. 异步处理
    2. 5.2. 取消处理
  6. 6. 总结
    1. 6.1. 参考资料