·
·
文章目录
  1. 离屏渲染(offscreen rendering)
    1. 圆角 cornerRadius
      1. UIImageView
      2. UIButton 设置背景图或 Image
      3. UILabel
      4. 总结
    2. 阴影 shadow
    3. 遮罩 masks
    4. 其他
  2. 列表
    1. CPU
      1. 预加载
      2. 多线程
      3. 其他
    2. 参考

如何让界面保持流畅

首先建议大家先读一读这篇文章 iOS 保持界面流畅的技巧 | Garan no dou ,从原理层面讲的很清楚。

离屏渲染(offscreen rendering)

关于离屏渲染的概念推荐这篇文章 离屏渲染优化详解:实例示范+性能测试 - 简书,总结下造成离屏渲染消耗性能的原因。

  • 需要创建新的缓冲区;
  • 离屏渲染的整个过程,需要多次切换上下文环境,不停的在当前屏幕(On-Screen)和离屏(Off-Screen)之间切换;

下面我们主要说说具体的离屏渲染优化方案。

圆角 cornerRadius

首先了解下 cornerRadius 概念,参考 cornerRadius - CALayer | Apple Developer Documentation

Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.

声明一点,单纯的设置 layer.cornerRadius 是不会造成任何性能问题的,只有配合 layer.masksToBounds 使用时才会造成离屏渲染(根据试验在 iOS 10 and later 的系统这样设置大部分 UIKit 控件也不会再触发离屏渲染)。在默认情况下,这个属性只会影响 layer 的 background color 和 border,对于 layer 中的 content 的内容是无法奏效。所以我们对以下 UIKit 控件做分析(下面的方法是针对 iOS 10 and later 不触发离屏渲染的方法):

UIKit iOS 9 & Before iOS 10 & Later
UIView 只设置 cornerRadius 只设置 cornerRadius
UIImageView 下方原理解释 只设置 cornerRadius 和 masksToBounds
UIButton (不设置背景图或 Image) 只设置 cornerRadius 只设置 cornerRadius
UILabel 下方原理解释 设置 cornerRadius 和 masksToBounds

如果是 iOS 10 & Later,可以放心的使用系统方法,除了带图片的 UIButton 其他的控件直接使用不会造成离屏渲染。
如果是 iOS 9 & Before,分为以下几种情况:

UIImageView

低版本系统,上述提到的混合图层方法是一种思路,另外可以直接对图片进行剪裁,方法如下:

WithRoundedCorners

因为是贝塞尔曲线的路径,你可以扩展成选择哪几个角有圆角效果,这边不做详细论述。

UIButton 设置背景图或 Image

不论系统版本高低,分如下几种情况:

  • 只有文字的 UIButton 只设置 cornerRadius 即可,不要设置 masksToBounds
  • 如果是有图片或者背景图片的 UIButton,方法一使用混合图层,方法二用上述图片剪裁后再设置;

UILabel

文本类视图 layer 的 contents 默认是透明的(字符就在这个透明的环境里绘制、显示),只需要设置 layer 的backgroundColor,再加上 cornerRadius 就可以实现圆角。但是 UILabel 的 backgroundColor 不是对 layer 而是对 content 的背景进行设置,所以不使用 backgroundColor 以及不 IB 里面设置,用如下方法实现即可:

1
2
label.layer.backgroundColor = color
label.layer.cornerRadius = 5

UITextField 自带圆角风格,UITextView 可以用类似的方法进行设置。

疑问:发现 UITextView 设置圆角在输入文本后,出现离屏渲染;文本为空时,不会出现离屏渲染。原理暂时未搞清楚。

总结

  1. 如果能只用 cornerRadius 解决问题,不会造成离屏渲染;
  2. 如果必须设置 masksToBounds,若圆角视图的数量较少(一页只有几个)可以考虑不用优化;若数量多参考上述方案优化,尤其在滚动列表中;
  3. 任何时候优先考虑避免触发离屏渲染,无法避免时使用 Rasterization(适用于静态内容的视图,也就是内部结构和内容不发生变化的视图);

阴影 shadow

使用阴影的前提条件是 layer.masksToBounds = false,因此阴影与系统圆角不兼容。

根据 Layer Style Property Animations 文档说明,阴影是和视图合成的,位于视图的下方。并且根据 shadowPath - CALayer | Apple Developer Documentation ,默认的阴影路径是 nil。未指定的情况下,阴影是沿着视图非透明部分扩展,如下图所示:

通过设置 shadowPath 来设置阴影来避免离屏渲染,代码如下:

1
2
3
4
5
6
7
imageView.layer.masksToBounds = false
imageView.layer.shadowColor = UIColor.red.cgColor
imageView.layer.shadowOpacity = 1.0 //此参数默认为0,即阴影不显示
imageView.layer.shadowRadius = 2.0 //给阴影加上圆角,对性能无明显影响
imageView.layer.shadowOffset = CGSize(width: 5, height: 5)
let path = UIBezierPath(rect: imageView.bounds) //设定路径:与视图的边界相同
imageView.layer.shadowPath = path.cgPath//路径默认为 nil

遮罩 masks

利用遮罩是可以画圆角的,大多数的使用情况是在特殊性质的 view,需要使用 layer mask 来渲染。
注意一下两点:

  • 一旦使用 mask,必定会造成离屏渲染;
  • 不常变动的打开 shouldRasterize 对渲染结果进行缓存,可以很大程度的提升 FPS;

其他

其他的 GroupOpacity、EdgeAntialiasing 在 iOS 10 & Later 后做了优化,日常使用场景较少,不做过多说明。

列表

类似于 UITableView、UICollectionView 等滚动视图最能反映界面是否流畅,且也最容易出现肉眼可见的不流畅现象。系统对于该视图也有优化,比如常见的在滑动时将 RunLoop 的模式切换到 UITrackingRunLoopMode。

根据开篇的文章,我们知道视图在显示的过程中,CPU 和 GPU 分别承担了部分工作,所以我们做列表层面的优化也要从这两个方面分别入手,我们着重补充下 CPU 部分,GPU 可以直接参考开篇文章。

在屏幕成像的过程中,CPU和GPU起着至关重要的作用

  • CPU(Central Processing Unit,中央处理器)
    • 对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)
  • GPU(Graphics Processing Unit,图形处理器)
    • 纹理的渲染

QxtJd2

CPU

在滑动过程中CPU占用特点是:

  • 滑动时CPU占用率高、空闲时占用率低
  • 主线程CPU占用率高、子线程占率低

我们根据上述两个特别分别做优化。

预加载

将滑动时的高占用率平摊到空闲时,在空闲时进行预加载。

  • iOS 10 & Later,UITableView 和 UICollectionView 提供了预加载机制
  • iOS12 开始 prefeatching 做了优化,不再与 cell 加载同时并发进行,而是 cell 加载完成之后串行开始prefeatch,从而优化了流畅度
  • 加载内容:
    • cell 高度、布局计算
    • 网络数据和图片

多线程

涉及到 UIKit 的操作一定是在主线程的,我们只能从以下几个层面去考虑子线程处理:

  • 图片解码
    • 当你用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。如果想要绕开这个机制,常见的做法是在后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片。目前常见的网络图片库都自带这个功能。
  • 文本计算
    • 如果一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。如果你对文本显示没有特殊要求,可以参考下 UILabel 内部的实现方式:用 [NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高,用 -[NSAttributedString drawWithRect:options:context:] 来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。如果你用 CoreText 绘制文本,那就可以先生成 CoreText 排版对象,然后自己计算了,并且 CoreText 对象还能保留以供稍后绘制使用。
  • 文本渲染
    • 屏幕上能看到的所有文本内容控件,包括 UIWebView,在底层都是通过 CoreText 排版、绘制为 Bitmap 显示的。常见的文本控件 (UILabel、UITextView 等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。对此解决方案只有一个,那就是自定义文本控件,用 TextKit 或最底层的 CoreText 对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少,可以缓存下来以备稍后多次渲染。

其他

  • 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用 CALayer 取代 UIView
  • 不要频繁地调用UIView的相关属性,比如 frame、bounds 等属性

参考

**版权声明**

Ivan’s Blog by Ivan Ye is licensed under a Creative Commons BY-NC-ND 4.0 International License.
叶帆创作并维护的叶帆的博客博客采用创作共用保留署名-非商业-禁止演绎4.0国际许可证

本文首发于Ivan’s Blog | 叶帆的博客博客( http://yeziahehe.com ),版权所有,侵权必究。

本文链接:http://yeziahehe.com/2020/02/05/smooth_user_interfaces_for_ios/

支持一下
扫一扫,支持yeziahehe
  • 微信扫一扫
  • 支付宝扫一扫