iOS一次显示性能优化中学到的

最近做的一个性能优化上的问题,对iOS的一个视图是如何显示到屏幕上的过程产生疑问。

先来基础知识回顾。

UIView 与 CALayer

继承关系

  1. UIView 继承结构
    UIView:UIResponder:NSObejet
    我们都知道UIResponder是用来响应事件的,也就是说UIView可以响应事件。

  2. CALayer 继承结构为NSObject
    直接从NSObject继承,而不是继承于UIResponder,所以CALayer不能响应事件。

所属框架

  1. UIView是在 /System/Library/Frameworks/UIKit.framework中定义的。

    The UIView class defines a rectangular area on the screen and the interfaces for managing the content in that area.
    UIView类定义了一个矩形区域在屏幕上和管理内容的接口。

  2. CALayer是在/System/Library/Frameworks/QuartzCore.framework定义的。并且:

    The CALayer class manages image-based content and allows you to perform animations on that content.
    CALayer类管理基于图像内容,并允许你执行动画内容。

UIView 与 CALayer 的关系

官方解释The Relationship Between Layers and Views

看下第一段,大概就知道什么意思了。

Layers are not a replacement for your app’s views—that is, you cannot create a visual interface based solely on layer objects. Layers provide infrastructure for your views. Specifically, layers make it easier and more efficient to draw and animate the contents of views and maintain high frame rates while doing so. However, there are many things that layers do not do. Layers do not handle events, draw content, participate in the responder chain, or do many other things. For this reason, every app must still have one or more views to handle those kinds of interactions.

  1. 首先layer不能取代view的。不可以创建一个完全基于layer对象的可视化界面

  2. layer为view 提供了一些基础设施。具体来说,layer使view的绘制和动画更加简单和高效

  3. layer不处理事件,layer不绘画内容,layer不参与响应者链(2015年12月29日更新)
    (2015年12月31日补充:CALayer并不关心任何响应链事件,所以不能直接处理触摸事件或者手势。但是它有一系列的方法帮你处理事件:-containsPoint:-hitTest:-containsPoint:接受一个在本图层坐标系下的CGPoint,如果这个点在图层frame范围内就返回YES。-hitTest:方法同样接受一个CGPoint类型参数,而不是BOOL类型,它返回图层本身,或者包含这个坐标点的叶子节点图层。这意味着不再需要像使用-containsPoint:那样,人工地在每个子图层变换或者测试点击的坐标。如果这个点在最外面图层的范围之外,则返回nil。)

UIView 中提供了

1
@property(class, nonatomic, readonly) Class layerClass;


1
@property(nonatomic, readonly, strong) CALayer *layer;

这两个属性

总结一下:

  1. UIView 是什么,做什么
    UIView是用来显示内容的,可以处理用户事件

  2. CALayer是什么,做什么
    CALayer是用来绘制内容的,对内容进行动画处理依赖与UIView来进行显示,不能处理用户事件

  3. 两者什么关系
    UIView和CALayer是相互依赖的关系。UIView依赖与CALayer提供的内容,CALayer依赖UIView提供 的容器来显示绘制的内容。归根到底CALayer是这一切的基础,如果没有CALayer,UIView自身也不会 存在,UIView是一个特殊的CALayer实现,添加了响应事件的能力。
    那么你给我解析清楚,都有了CALayer了,为什么还要UIView这文章篇确实分析的不错。

    • 机制与策略分离

      Unix内核设计的一个主要思想是——提供机制(Mechanism)而不是策略(Policy)。编程问题都可以抽离 出机制和策略部分。机制一旦实现,就会很少更改,但策略会经常得到优化。例如原子可以看做是机制, 而各种原子的组成就是一种策略。CALayer也可以看做是一种机制,提供图层绘制,你们可以翻开 CALayer的头文件看看,基本上是没怎么变过的,而UIView可以看做是策略,变动很多。越是底层,越 是机制,越是机制就越是稳定。机制与策略分离,可以使得需要修改的代码更少,特别是底层代码,这样 可以提高系统的稳定性

    • 更多的不可变

      稳定给你的是什么感觉?坚固?不可形变?稳定其实就是不可变。一个系统不可变的东西越多,越是稳 定。所以机制恰是满足这个不可变的因素的。构建一个系统有一个指导思想就是尽量抽取不可变的东西和 可变的东西分离。水是成不了万丈高楼的,坚固的混凝土才可以。更少的修改,意味着更少的bug的几 率

    • 各司其职

      即使能力再大也不能把说有事情都干了,万一哪一天不行了呢,那就是突然什么都不能干了。所以仅仅是 基于分散风险原则也不应该出现全能类。各司其职,相互合作,把可控粒度降到最低,这样也可以是系统 更稳定,更易修改

    • 漏的更少

      接口应该面向大众的,按照八二原则,其实20%的接口就可以满足80%的需求,剩下的80%应该隐藏在背 后。因为漏的少总是安全的,不是吗。剩下的80%专家接口可以隐藏与深层次。比如UIView遮蔽了大部分 的CALayer接口,抽取构造出更易用的frame和动画实现,这样上手更容易。

iOS图形绘制框架

图形绘制框架图

我们知道了,UIView并不负责图形的绘制和渲染,从框架的结构上也能看得出,图形的绘制是由UIkit的底层 Core Animation 还有更底层的 OpenGL ES/ OpenGL ,Core Graphics 负责。

Core Animation Pipeline

46804CoreAnimationPipeline.png

从这张图上我们能够看出,一个视图从开始到显示到屏幕上的一个过程。

纵向:iOS中,最理想的动画是1秒60帧,那么1帧就是16.76毫秒。

横向:第一根横线上方标示的是CPU的操作,第一根横线下方是GPU的操作。最后是显示器的操作。

  1. 系统接受到事件,创建出UIView后,就会向Render Server 打包提交事物 Commit Transaction,把所有的CALayer打包发给Render Server
  2. Render Server 接收到后首先做的是等待,等待下1帧,下一个循环。然后下一帧开始网格化,再拆分成三角形,然后生成相应OpenGL绘制的代码,调用绘制的代码,但不是真正的绘制,只是把绘制的命令发了出去
  3. GUP开始渲染。

  4. 显示在显示器上。

    从接受到事件开始,到显示到显示器上,经过了3帧的时间。这其实是一个流水线的过程,iOS在这每一部分的每一帧都会做相应的工作。

GPU屏幕渲染

在OpenGL中,GPU屏幕渲染有两种方式:

  • On-Screen Renderin
    当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行
  • Off-Screen Renderin
    离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

离屏渲染

相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:

  • 创建新缓冲
    要想进行离屏渲染,首先要创建一个新的缓冲区
  • 上下文切
    离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off- Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切 换到当前屏幕。而上下文环境的切换是要付出很大代价的。

离屏渲染触发方式

设置了以下属性时,都会触发离屏绘制:

  • Any layer with a mask (layer.mask)
  • Any layer with layer.masksToBounds / view.clipsToBounds being true
  • Any layer with layer.allowsGroupOpacity set to YES and layer.opacity is less than 1.0
  • Any layer with a drop shadow (layer.shadow*).
  • Any layer with layer.shouldRasterize being true
  • Any layer with layer.cornerRadius, layer.edgeAntialiasingMask, layer.allowsEdgeAntialiasing
  • Text (any kind, including UILabel, CATextLayer, Core Text, etc).
  • Most of the drawing you do with CGContext in drawRect:. Even an empty implementation will be rendered offscreen.

需要注意的是,如果shouldRasterize被设置成YES,在触发离屏绘制的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用。这将在很大程度上提升渲染性能。但是:

  • 避免在经常变动的图层上应用光栅
  • 尺寸不超过2.5倍屏
  • 100ms 之内不使用不会在绘存(0.1秒的缓存)
    而其它属性如果是开启的,就不会有缓存,离屏绘制会在每一帧都发生。

遇到的问题:在列表中大量设置了圆角视图

视图实现圆角效果的四种方法及比较

  1. layer.cornerRadiu
    第一种方法最简单,通过层对象的cornerRadius属性实现圆角效果,代码如下

    1
    2
    view.layer.cornerRadius = 8.0
    view.layer.masksToBounds = YES

    缺点是会有2次rending passes。首先off-screen画出带圆角的图,然后在视图上画第二次

    我们知道了相比于当前屏幕渲染,离屏渲染的代价是很高的。鄙人就是图方便,之前知道这个方法会导致 列表滑动卡顿,但是还是试了一试,结果卡成狗了

    虽然也设置了光栅化,这个视图也不是经常变化的,认为光栅化后这个视图会缓存起来,但是不知这个缓 存也只有0.1秒的时间啊

  2. 这种方法的好处是只有一次rending pass,是三种方法中效率最高的。缺点是需要override视图。代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    - (void)drawRect:(CGRect)rect
    {
    CGRect bounds = self.bounds;
    [[UIColor whiteColor] set];
    UIRectFill(bounds);
    [[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:8.0] addClip];
    [self.image drawInRect:bounds];
    }
  3. 通过另一张mask图创建新图
    首先需要一张mask图,然后将这张mask图和原图合成,得到带圆角的新图。效率和方法一类似,合成新图等同于在off-screen作图。该方法的优点是可以不局限于圆角,全屏mask图控制。

  4. UI妹纸辛苦下,图片直接给切成圆角
    这中方法的效率有待调研。- -

总结

如果要效率(例如要提高table view的滚动帧数),就多用方法二。要方便,自然是方法一。如果需要的特殊形状UIBezierPath对象无法构成,则考虑方法三。实在不行,上杀手锏,麻烦UI妹纸吧~

李剑飞 wechat
欢迎订阅我的微信公众号"剑飞说"!
坚持原创技术分享,您的支持将鼓励我继续创作!