本系列博客是本人的开发笔记。为了方便讨论,本人新建了一个微信群(iOS技术讨论群),想要加入的,请添加本人微信:zhujinhui207407,【加我前请备注:iOS 】,本人博客http://www.kyson.cn 也在不停的更新中,欢迎一起讨论
引言
在WWDC2018中,展示了iOS12对AutoLayout优化,UICollectionView性能对比,item自动适配大小,iOS 11看上去有掉帧卡顿的现象,iOS 12表现完美,没有掉帧。
初识 Auto Layout
Auto Layout 的原理其实非常简单,在这里通过一个例子先简单的解释一下:
iOS 中视图所需要的布局信息只有两个,分别是 origin/center 和 size,在这里我们以 origin & size 为例,也就是 frame 时代下布局的需要的两个信息;这两个信息由四部分组成:
x & y
width & height
以左上角的 (0, 0) 为坐标的原点,找到坐标 (x, y),然后绘制一个大小为 (width, height) 的矩形,这样就完成了一个最简单的布局。而 Auto Layout 的布局方式与上面所说的 frame 有些不同,frame 表示与父视图之间的绝对距离,但是 Auto Layout 中大部分的约束都是描述性的,表示视图间相对距离,以上图为例:
A.left = Superview.left + 50
A.top = Superview.top + 30
A.width = 100
A.height = 100
B.left = (A.left + A.width)/(A.right) + 30
B.top = A.top
B.width = A.width
B.height = A.height
虽然上面的约束很好的表示了各个视图之间的关系,但是 Auto Layout 实际上并没有改变原有的 Hard-Coded 形式的布局方式,只是将原有没有太多意义的 (x, y) 值,变成了描述性的代码。
我们仍然需要知道布局信息所需要的四部分 x、y、width 以及 height。换句话说,我们要求解上述的八元一次方程组,将每个视图所需要的信息解出来;Cocoa 会在运行时求解上述的方程组,最终使用 frame 来绘制视图。
Auto Layout 的原理就是对线性方程组或者不等式的求解。
- Auto Layout 的性能
在使用 Auto Layout 进行布局时,可以指定一系列的约束,比如视图的高度、宽度等等。而每一个约束其实都是一个简单的线性等式或不等式,整个界面上的所有约束在一起就明确地(没有冲突)定义了整个系统的布局。
在涉及冲突发生时,Auto Layout 会尝试 break 一些优先级低的约束,尽量满足最多并且优先级最高的约束。
因为布局系统在最后仍然需要通过 frame 来进行,所以 Auto Layout 虽然为开发者在描述布局时带来了一些好处,不过它相比原有的布局系统加入了从约束计算 frame 的过程,而在这里,我们需要了解 Auto Layout 的布局性能如何。
因为使用算法解决约束问题就是对线性等式或不等式求解,所以其时间复杂度就是多项式时间的,不难推测出,在处理极其复杂的 UI 界面时,会造成性能上的巨大损失。
在这里我们会对 Auto Layout 的性能进行测试,为了更明显的展示 Auto Layout 的性能,我们通过 frame 的性能建立一条基准线以消除对象的创建和销毁、视图的渲染、视图层级的改变带来的影响。
Layout 性能测量使用的代码可以在笔者附带的Demo中进行演示获取。
代码分别使用 Auto Layout 和 frame 对 N 个视图进行布局,测算其运行时间。
使用 AutoLayout 时,每个视图会随机选择两个视图对它的 top 和 left 进行约束,随机生成一个数字作为 offset;同时,还会用几个优先级高的约束保证视图的布局不会超出整个 keyWindow。
而下图就是对 100~1000 个视图布局所需要的时间的折线图。
这里的数据是在 OS X EL Captain,Macbook Air (13-inch Mid 2013)上的 iPhone 6s Plus 模拟器上采集的, Xcode 版本为 7.3.1。在其他设备上可能不会获得一致的信息,由于笔者的 iPhone 升级到了 iOS 10,所以没有办法真机测试,最后的结果可能会有一定的偏差。
从图中可以看到,使用 Auto Layout 进行布局的时间会是只使用 frame 的 16 倍左右,虽然这里的测试结果可能受外界条件影响差异比较大,不过 Auto Layout 的性能相比 frame 确实差很多,如果去掉设置 frame 的过程消耗的时间,Auto Layout 过程进行的计算量也是非常巨大的。
在上一篇文章中,我们曾经提到,想要让 iOS 应用的视图保持 60 FPS 的刷新频率,我们必须在 1/60 = 16.67 ms 之内完成包括布局、绘制以及渲染等操作。
也就是说如果当前界面上的视图大于 100 的话,使用 Auto Layout 是很难达到绝对流畅的要求的;而在使用 frame 时,同一个界面下哪怕有 500 个视图,也是可以在 16.67 ms 之内完成布局的。不过在一般情况下,在 iOS 的整个 UIWindow 中也不会一次性出现如此多的视图。
我们更关心的是,在日常开发中难免会使用 Auto Layout 进行布局,既然有 16.67 ms 这个限制,那么在界面上出现了多少个视图时,我才需要考虑其它的布局方式呢?在这里,我们将需要布局的视图数量减少一个量级,重新绘制一个图表:
从图中可以看出,当对 30 个左右视图使用 Auto Layout 进行布局时,所需要的时间就会在 16.67 ms 左右,当然这里不排除一些其它因素的影响;到目前为止,会得出一个大致的结论,使用 Auto Layout 对复杂的 UI 界面进行布局时(大于 30 个视图)就会对性能有严重的影响(同时与设备有关,文章中不会考虑设备性能的差异性)。
上述对 Auto Layout 的使用还是比较简单的,而在日常使用中,使用嵌套的视图层级又非常正常。
在笔者对嵌套视图层级中使用 Auto Layout 进行布局时,当视图的数量超过了 500 时,模拟器直接就 crash 了,所以这里没有超过 500 个视图的数据。
我们对嵌套视图数量在 100~500 之间布局时间进行测量,并与 Auto Layout 进行比较:
在视图数量大于 200 之后,随着视图数量的增加,使用 Auto Layout 对嵌套视图进行布局的时间相比非嵌套的布局成倍增长。
虽然说 Auto Layout 为开发者在多尺寸布局上提供了遍历,而且支持跨越视图层级的约束,但是由于其实现原理导致其时间复杂度为多项式时间,其性能损耗是仅使用 frame 的十几倍,所以在处理庞大的 UI 界面时表现差强人意。
iOS优化实现
- Render Loop
Render Loop 是一个每秒钟跑120次的一个进程,是为了确保所有的内容都能为每一个frame做好准备。Render Loop 一共包括三个步骤来更新约束,布局和渲染。
首先,每一个需要接收到更新约束的view会从子view向上传递,直到window;
然后,每一个接收到的view开始layoutsubviews,和更新约束是从相反的方向开始,layout从window开始到每一个子view进行layout。
最后,每一个需要渲染的view,和layout相同,从父view向子view开始渲染。
Render Loop目的是为了避免重复的工作。
举一个例子:一个UILable 需要一个约束来描述它的大小,但是有很多属性会影响他的大小,设置它的font,text size等等都会受到影响。当一个属性改变的时候,可能text其他属性也会被重新赋值
,很有可能调用一堆属性的setter方法,这样效率会很低。
只需要调用updateConstraints 并指定好要更新的属性,Render Loop会帮助你计算好它的frame并完成渲染,从而避免多次设置的重复工作。
Auto Layout 以外的优化
iOS 12 比 iOS 11 更快、更平滑了,升级了的朋友肯定都同意这一点。但苹果为了让 iOS 12 变快究竟做了哪些升级呢,苹果为我们介绍了针对 iOS 12 做出的一系列改变。
改进预加载功能
在 iOS 12 中,苹果工程师攻克了困扰用户数年的卡顿问题,他们在检查官方 App 时发现了过去丢帧的原因。在 iOS 中,用户滑动、点击屏幕的时候就需要即时渲染生成内容,比如说菜单列表或者是文档中的图形,这个生成的过程花费的时间就会导致延迟。为了降低这一延迟,苹果在 iOS 10 中引入了一个名为“Cell Pre-fetch”的功能,简单来说就是在后台预测用户的动作,然后预先加载好相应的内容,当用户确定了这个点击或滑动马上把预加载好的内容呈现上来,这样一来就不会延迟了,而且不会出现处理器负载突然加大的问题。虽然“Cell Pre-fetch”确实让 iOS 10 变快了,但是仍然没有完全解决丢帧问题,工程师们发现,当系统在预加载内容的时候,处理器也同时在渲染确定会在屏幕上呈现的那一组内容,二者同时进行就会导致 CPU 遇到瓶颈,然后两个任务就都变慢了。为了解决这一问题,苹果在 Cocoa Touch (开发者创建 iOS App 时使用的接口)中重新对任务进行了排序,在屏幕渲染任务完成之后,预加载才启动,因为前者需要马上被用户看到,更加重要。CPU响应机制升级
除了这一点之外,CPU 也有一定的升级。此前的 iOS 设备中,在 CPU 负载很小的时候也会发生丢帧,因为在任务很少的时候 CPU 会降低频率进入怠速模式。但是如果用户突然触发了一个比较麻烦的任务,CPU 就需要一点时间反应过来然后加快频率,这一过程中就会发生延迟。而在 iOS 12 中,苹果优化了 CPU 的响应机制,能更快地从低频提升到高频,并且 iOS 12 还很聪明,不会把 CPU 的频率拉得太高,只会提供完成这个任务所需要的那个频率上,所以在完成任务之后,CPU 也能够很快将频率降到合适的怠速水平,减少电池的消耗。Auto-layout (自动布局)功能升级
紧接着还有对 Auto-layout (自动布局)功能的升级,Auto-layout 让开发者能对 App 内的图标、文字等元素更方便地定位(比如据边框多少像素),以适应不同的屏幕尺寸。但是苹果工程师发现这个从 iOS 6 就有的功能现在看来有些过于耗费资源了,在不同的情况下, Auto-layout 使用的资源可能会呈指数级增长,比如元素在嵌套和依赖于其他元素进行放置两种状态下,耗费的资源大不相同。在 iOS 12 中,苹果大大减少了会导致指数缩放的元素,减轻了对 CPU 和 GPU 的负担。总体来看,让渲染的元素彼此独立放置、互不干扰能够减少资源消耗。自动备份存储技术
最后,苹果还介绍了 iOS 12 的自动备份存储技术,它可以减少应用程序运行的时候消耗的内存量。在苹果的演示中,当旧版系统渲染相同大小的彩色照片和灰度图像(只存在灰度信息的黑白照片)时,耗费的内存竟然是完全一样的,但其实这两张照片包含的信息存在显著差异,彩色照片每个像素都有色值等信息,黑白照片只有灰度信息。而在使用自动备份存储技术之后,系统会对不同文件包含的数值信息进行一个初步的判断,然后再分配合适的内存给它,不会像以前那样一视同仁地处理所有内容,这样做显然可以避免浪费内存资源。
除了以上四点之外,苹果还介绍了不少加快系统运作的方法。不过在演讲的最后,工程师还是表示现在他们正在积极地对这些功能展开工作,希望在 iOS 12 正式版上线之前(今年九月)完成优化,如果无法完成,可能会推迟几个版本。
AutoLayout 基础知识
Autolayout是IOS6以后才出现的新技术,比frame使用更加地方便,比Autoresizing更加地强大。下面,就介绍一下在项目开发中使用Autolayout几种途径。
纯代码实现Autolayout
- NSLayoutConstraint
[NSLayoutConstraint constraintWithItem:(id)item attribute:(NSLayoutAttribute)attribute relatedBy:(NSLayoutRelation)relation toItem:(id)otherItem attribute:(NSLayoutAttribute)otherAttribute multiplier:(CGFloat)multiplier constant:(CGFloat)constant]
举个简单的例子,我们在所在controller默认view上添加一个子视图subview,subview的宽高为view宽高的1/3,距离view左边100,距离view上边150,就可以用下面代码这样实现:
UIView *subView = [[UIView alloc]init];
subView.backgroundColor = [UIColor greenColor];
[self.view addSubview:subView];
subView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1. constant:150]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeHeight multiplier:0.3 constant:0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeWidth multiplier:0.3 constant:0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1. constant:100]];
上面方法参数说明:
第一个参数:指定约束左边的视图view1
第二个参数:指定view1的属性attr1
第三个参数:指定左右两边的视图的关系relation
第四个参数:指定约束右边的视图view2
第五个参数:指定view2的属性attr2
第六个参数:指定一个与view2属性相乘的乘数multiplier
第七个参数:指定一个与view2属性相加的浮点数constant
依据的公式是:view1.attr1 = view2.attr2*multiplier +constant
NSLayoutAttribute的类型:
NSLayoutAttributeLeft 视图的左边
NSLayoutAttributeRight 视图的右边
NSLayoutAttributeTop 视图的上边
NSLayoutAttributeBottom 视图的下边
NSLayoutAttributeLeading 视图的前边
NSLayoutAttributeTrailing 视图的后边
NSLayoutAttributeWidth 视图的宽度
NSLayoutAttributeHeight 视图的高度
NSLayoutAttributeCenterX 视图的中点的X值
NSLayoutAttributeCenterY 视图中点的Y值
NSLayoutAttributeBaseline 视图的基准线
NSLayoutAttributeNotAnAttribute 无属性
NSLayoutRelation的类型:
NSLayoutRelationLessThanOrEqual 关系小于或等于
NSLayoutRelationEqual 视图关系等于
NSLayoutRelationGreaterThanOrEqual 视图关系大于或等于
这里要说明一下,设置约束之前必须要求确保子视图添加到了父视图上了(如:[self.view addSubview:subView]),并且被约束的视图的translatesAutoresizingMaskIntoConstraints = NO,不然就会发生程序crash。
- VFL(Visual format language)
VFL是NSLayoutConstraint使用特定的NSString类型字符串来创建Constraint,通过下面方法就可以创建
[NSLayoutConstraint
constraintsWithVisualFormat:(NSString *)format options:(NSLayoutFormatOptions)opts metrics:(nullable NSDictionary *)metrics views:(NSDictionary *)views]
关于VFL格式使用规则,可以参考这篇博客http://blog.csdn.net/mmoaay/article/details/46707103
- Masonry
通过上面的NSLayoutConstraint使用可以发现,每次添加一条约束就要写一长串的代码,如果视图约束很多的话,那么就要写很多的代码,所以就有人基于NSLayoutConstraints进行了封装。Masonry就是基于NSLayoutConstraints封装的轻量级的第三方开源框架之一。所以实例可以通过Masonry实现:
UIView *subView = [[UIView alloc]init];
subView.backgroundColor = [UIColor greenColor];
[self.view addSubview:subView];
__weak typeof(self) weakSelf = self;
[subView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(@100);
make.top.mas_equalTo(@150);
make.width.mas_equalTo(weakSelf.view.mas_width).multipliedBy(0.3);
make.height.mas_equalTo(weakSelf.view.mas_height).multipliedBy(0.3);
}];
xib 中Autolayout设置
在xib中使用Autolayout,是一种非常直观,非常方便的一种方式。我们可以在不用代码就可以实现各种布局。
按照图中所示,可以很快的设置子视图Top,Leading,Bottom,Trailing的约束,切换到导航栏Show the size inspect按钮可以看到已经设置好的各个约束,并且可以对设置好约束进行修改,如修改每个约束中constant,priority,multiplier值等。
当然,不管是纯代码实现Autolayout还是xib实现Autolayout,都可能遇到约束冲突的情况。比如,一个subview子视图基于父视图设置了Top,Leading,Bottom,Trailing四个约束,并且又设置了subview的高度height,这样运行就是出现Probably at least one of the constraints in the following list is one you don't want. 的警告,因为subview子视图高度height约束和Top或Bottom产生了冲突。
约束冲突可以通过修改Autolayout约束的优先级不同的方式进行解决,在Autolayout中每个约束都有一个优先级priority,priority的范围是1 ~ 1000,默认创建的约束priority最高是1000,并且系统默认提供了Required(1000)、High(750)、Low(250)三种priority。所以对于上述subview子视图产生的约束冲突问题,可以通过修改subview 子视图Top或Bottom约束的priority小于1000就可以解决冲突问题。
创建高性能的布局
通过instrument调试工具,可以看出一些布局上的耗时问题。一下是需要注意的几点:
- 避免删除所有的约束的情况
- 对于静态约束,只需要添加一次
- 只改变需要改变的约束
- 尽量用hide() 方法隐藏view,而不是remove然后在add
- 有些控件比较特殊,比如 UIImageView,它的大小是根据他的image计算确定他的content size。UILabel是根据他的text确定的。这些都会返回它们的固有尺寸,UIView 会直接通过他们的固有尺寸来当做约束条件。
- 重写 intrinsicContentSize text的计算是成本很高的,所以UIlabel的size通过text去控制计算开销成本会很高。这个时候我们可以 通过重写 UILabel 的 intrinsicContentSize 来直接控制它的固有尺寸。如果已知一个UILabel的展示size,直接重写其属性,其他情况使用UIView.noIntrinsicMetric。
override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
}
Auto Layout 原理
一般大家都会认为Auto Layout这个东西是苹果自己搞出来的,其实不然,早在1997年Alan Borning, Kim Marriott, Peter Stuckey等人就发布了《Solving Linear Arithmetic Constraints for User Interface Applications》论文
(论文地址:http://constraints.cs.washington.edu/solvers/uist97.html)
提出了在解决布局问题的Cassowary constraint-solving算法实现,并且将代码发布在他们搭建的Cassowary网站上
http://constraints.cs.washington.edu/cassowary/
后来更多开发者用各种语言来写Cassowary,比如说pybee用python写的
https://github.com/pybee/cassowary
自从它发布以来JavaScript,.NET,JAVA,Smalltall和C++都有相应的库。2011年苹果将这个算法运用到了自家的布局引擎中,美其名曰Auto Layout。
Cassowary 算法
在上世纪 90 年代,一个名叫 Cassowary 的布局算法解决了用户界面的布局问题,它通过将布局问题抽象成线性等式和不等式约束来进行求解。
Auto Layout 其实就是对 Cassowary 算法的一种实现,但是这里并不会对它展开介绍,有兴趣的读者可以在文章最后的 Reference 中了解一下 Cassowary 算法相关的文章。
引用
- 深入剖析Auto Layout,分析iOS各版本新增特性
- iOS 12 比 iOS 11 更快、更平滑了
- High Performance Auto Layout
- 从 Auto Layout 的布局算法谈性能
- [ WWDC2018 ] - 高性能 AutoLayout High Performance Auto Layout