WWDC2018 之 高性能 Auto Layout

1. 关于 Auto Layout 的历史渊源

上世纪 90 年代,名叫 Cassowary的布局算法,通过将布局问题抽象成线性不等式,并分解成多个位置间的约束,解决了用户界面的布局问题。

Apple 自从 iOS 6 引入了 Auto Layout 的布局概念,其实就是对 Cassowary布局算法的一种实现。在使用 Auto Layout 进行布局时,可以指定一系列的约束,比如视图的高度、宽度等等。而每一个约束其实都是一个简单的线性等式或不等式,整个界面上的所有约束在一起就明确地(没有冲突)定义了整个系统的布局。

对于 Auto Layout 算法部分,本文不做展开。在这里我们仅仅需要知道,Auto Layout 的原理,就是在对 Layout 问题抽象的方程组求解,就可以继续向下阅读。

以下就是 WWDC 220 Session - 高性能 Auto Layout 高度脱水版。

2. iOS 上的性能表现

下图是 Ken Ferry 在 Session 现场的演示,可以比较清晰的看出,左图自使用布局的 CollectionView 上下滑动较右图而言更加流畅,Ken 在描述中也说到 iOS 12 在该例中的所有滑动事件是满帧状态。(左 iOS 12,右 iOS 11)

下图是官方测试后得到的 iOS 12 和 iOS 11 在特定场景下时间开销的对比图。可以明显的看到 iOS 12 具有很大的优势。

那么究竟是如何做到这个优化的呢?

3. 内部实现和感观体验

我们首先来通过一个例子整体的了解一下。分析一下这个简单的 Layout 场景:

下面我们在 updateConstraints()方法中来描述这个 Layout:

// Don’t do this! Removes and re-adds constraints potentially at 120 frames per second
override func updateConstraints() {
    // 首先移除约束
    NSLayoutConstraint.deactivate(myConstraints)
    // 然后对约束重新规则
    myConstraints.removeAll()
    // 构造一个 view 字典便于visual format使用
    let views = ["text1":text1, "text2":text2]
    // 为约束增加规则
    myConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[text1]-[text2]",
                                                    options: [.alignAllFirstBaseline],
                                                    metrics: nil,
                                                    views: views)
    myConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[text1]-|",
                                                    options: [],
                                                    metrics: nil,
                                                    views: views)
    // 添加约束,与 deactivate 方法对应
    NSLayoutConstraint.activate(myConstraints)
    // 调用父类的 updateConstraints()
    super.updateConstraints()
}
复制代码

至此我们就实现了这个简单的 Layout 方案。为了继续探究这个 Topic,在这之前先要了解一些预备知识。

3.1 updateConstraints 原理 - Render Loop

Render Loop 这个过程是用来确保所有的 UI 视图在每秒的所有帧中都表现出对应表现,正常情况下每秒会运行 120 次。这个工程分成三步:

  1. 更新约束:从子视图向外层逐级更新约束;
  2. Layout 调整:从外部向内,逐级视图获得自身的 Layout;
  3. 渲染与展示:与 Layout 相同,呈现顺序从外向内,使得视图呈现出来;

当然,这么叙述还是有些抽象。其实这三个过程在我们日常开发中也是经常接触的三类方法:

/// Render Loop 过程
/// 过程一:更新约束
func updateConstraints();
func setNeedsUpdateConstraints();
func updateConstraintsIfNeeded();

/// 过程二:Layout 调整
func layoutSubviews();
func setNeedsLayout();
func layoutIfNeeded();

/// 过程三:渲染与展示
func draw(_:);
func setNeedsDisplay();
复制代码

每一次调整都会运行这么一个 Render Loop 步骤。这是一套很精确的 API,目的为了让各个环节中的工作不重不漏,从而除去了很多重复操作。如上例中,如果一个 UILabel需要有一个约束来描述其大小,但是其中的很多属性例如字条、字号等又会影响这个视图的大小,这套 API 就是这样,每次修改都会根据不同的属性来确定其尺寸。开发者可以在其方法内部来指明在渲染前最后的属性值,从而排除了多次设置的重复操作。

了解了 Render Loop 我们再来完善之前的代码。会发现在每次在 updateConstraints的时候,都会重新解除和增加一次约束,这显然会使得性能变差。修改一下代码:

// This is ok! Doesn’t do anything unless self.myConstraints has been nil’d out
override func updateConstraints() {
    if self.myConstraints == nil {
        var constraints = [NSLayoutConstraint]()
        let views = ["text1":text1, "text2":text2]
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[text1]-[text2]",
                                                      options: [.alignAllFirstBaseline],
                                                      metrics: nil,
                                                      views: views)
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[text1]-|",
                                                      options: [],
                                                      metrics: nil,
                                                      views: views)
        NSLayoutConstraint.activate(constraints)
        self.myConstraints = constraints
    }
    super.updateConstraints()
}
复制代码

这个 nil的判断意思是如果我们增加了约束,那么就不用对其再次设置。这个错误也是开发者在客户端开发中较常见的错误,这种无变化的约束设置我们称之为 规则搅动 (Churning the Constraints),这种操作毫无意义且影响性能。

虽然 Render Loop 过程具有明确的目的性,但是这套 API 也是高危的,因为它经常会被调用。

下面我们来深究一下这个过程的原理。

3.2 增加约束的内部实现

当我们为空间增加一个约束 Constraint的时候,通过这些约束会组成一个多元一次方程组,这个方程组的解可以定位那些通过约束可间接计算出的定量。而这个计算过程是 Auto Layout 引擎来完成处理的。求出的解集在 UIView渲染过程中,当做其 frame属性中的值来使用。下图就是反应了这么一个过程。

在计算引擎计算出解集后,计算引擎还有他最后的一个工作,就是发送通知,使得对应的 UIView调用其父视图的 setNeedsLayout()方法。这也就是我们之前提到的更新约束这个步骤,通过向外层调用 setNeedsLayouts()方法,我们可以验证这个由内向外的步骤。

在约束更新完成之后,进入了第二个步骤,也就是 Layout 调整阶段。每个视图会从计算引擎中获取到其子视图所需的所有数据,获取到之后重新为子视图赋值。从这点看出,Layout 调整阶段是自外向内的。

我们再来思考一下上文提及到的 规则搅动问题,如果我们每次将约束规则删除、重新添加,则每一次刷新视图都会从新经历一遍引擎的解集重计算、由内向外的 setNeedsLayout()、自外向内的 Layout 调整。而这些其实是不需要的。

对于一次约束的增加过程至此也就大体讲完了。我们来总结一下这里我提到的一些主要内容:

  1. 不要由于自身的问题从而带来 规则搅动的错误;
  2. Auto Layout 的数学原理,就是基本的代数运算。
  3. Auto Layout 计算引擎是一个布局缓存和关系依赖的跟踪器;
  4. 需要什么就对什么做出约束,不要增加额外的约束,避免造成不必要的开销;

4. 建立一个有效的 Layout

4.1 使用 Instrument 来捕捉规则搅动

在使用 Auto Layout 布局来实现 UITableView,我们经常会发现滑动卡顿的问题。这些问题在开发的时候很难查出原因所在。为了方便的解决并排查问题,新版的 Xcode 增加了一个新的工具 - Instrument for Layout

这个工具的第一行 Layout Time 反应了 CPU 的使用情况,通过运算时间可以和后面的异常值进行比对。

第二行用于检测我们上文提到的 规则搅动的问题,当代码中出现大量的重复添加相同约束的错误时,会以直方图时间复杂度的形式呈现出来,便于我们做进一步的代码排查。

第三行来显示约束的增、删、改的操作。

最后一行,我们会对 UILabel这个控件的 Layout 占中单独展示出来。因为我们的示例 App 中只有 UILabel,当然如果你的应用中有其他的视图,也会按照类型来分行呈现。

其实纵观这个工具,他能够帮助我们的仅仅是查看约束的计算耗时以及是否出现了 规则搅动。但是这些都是我们在代码中可以直接避免的。这里有几个关于避免 规则搅动的 Tips 告诉大家:

  1. 尽量不要删除所有的约束(Avoid removing all constraints);
  2. 若是一个静态约束,仅做一次添加操作即可;
  3. 仅改变需要改变的约束;
  4. 尽量不要做删除视图的操作,反之用 hide()方法替代;

一般做到这四点,可以避免绝大多数的 规则搅动代码层面的错误。

某些控件是十分特殊的,例如 UIImageViewUILabel这种,他们都有一个自适应的尺寸,这里我们称之为固有尺寸(Intrinsic Content Size),当我们不对其作出特殊化的 height 和 width 限制时,UIView会直接用他们的固有尺寸(UIImageView即图片尺寸,UILabel即文本尺寸)来当做约束条件。

4.2 Override intrinsicContentSize 来调整 UILabel约束性能

在很多控件组成的页面中,UILabel的 Size 计算会在所有的计算开销中占很大的比重。这时候追求极致,我们可以 Override UILabel的 intrinsicContentSize来告诉计算引擎,如何抉择 UILabel的 Size 问题。如果已知一个 UILabel的展示 Size,直接 Override 其属性即可,否则对其设置成 UIView.noIntrinsicMetric

override var intrinsicContentSize: CGSize {
    return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
}
复制代码

4.3 不要过度使用 systemLayoutSizeFitting()

systemLayoutSizeFitting()虽然能帮助我们根据 Layout 来自动计算其约束,但是纵观整个 Layout 过程,其计算的时间开销是十分大的。这个方法调用,其目的是从计算引擎中重新获得调用方法对应视图的 Size。然而这个过程较为复杂。

也许整个流程并不复杂,但是对于我们 Render Loop 过程,相当于作出了一次重复步骤。在 iOS 12 中,Apple 再次对自适应 Cell 作出了优化,所以在大多数情况下,减少 systemLayoutSizeFitting()的调用可以使得时间开销再次削减。

5 总述

以上便是笔者对于这个 Session 的所有记录和脱水叙述。如同 Ken 所说,也许简单的对于 Auto Layout 中约束的 Tips 并不能满足于你,这里还有一些资料可以供你去继续学习。

  • 可以前往 WWDC 2015 查看 Session 219 - Mysteries of Auto Layout, Part 2,为你带来 Auto Layout 实现及原理。
  • https://techblog.toutiao.com/2018/06/19/untitled-46/

原文地址:https://www.cnblogs.com/miaomiaocat/p/9715466.html

时间: 2024-11-08 21:31:23

WWDC2018 之 高性能 Auto Layout的相关文章

Auto Layout Guide----(三)-----Anatomy of a Constraint

Anatomy of a Constraint 剖析约束 The layout of your view hierarchy is defined as a series of linear equations. Each constraint represents a single equation. Your goal is to declare a series of equations that has one and only one possible solution. A samp

Auto Layout 使用心得

此系列文章代码仓库在 https://github.com/johnlui/AutoLayout ,有不明白的地方可以参考我的 Auto Layout 设置哦,下载到本地打开就可以了. 简介 Auto Layout 是苹果在 Xcode 5 (iOS 6) 中新引入的布局方式,旨在解决 3.5 寸和 4 寸屏幕的适配问题.屏幕适配工作在 iPhone 6 及 plus 发布以后变得更加重要,而且以往的“笨办法”的工作量大幅增加,所以很多人开始学习使用 Auto Layout 技术. 初体验 0.

Day3 : Auto layout 和 JVFloatLabeledTextfield框架 学习笔记

为了可以优化项目的UI,为了可以使用JVFloatLabeledTextfield框架来构建文本输入框(动画效果超赞),今天重点学习了Auto Layout(以下简称AL)技术,主要是了解AL的工作原理,并且要掌握用代码添加constraints. 1.JVFloatLabeledTextfield JVFloatLabeledTextfield框架可以让文本框呈现一个漂浮的PlaceHolder,简洁.明确.生动.而这个框架另一个让我大开眼界的是他利用AL技术画直线,做出一个简单的表单页面.画

iOS开发之Auto Layout入门

随着iPhone6与iOS8的临近,适配的问题讲更加复杂,最近学习了一下Auto Layout的使用,与大家分享.  什么是Auto Layout? Auto Layout是iOS6发布后引入的一个全新的布局特性,其目的是弥补以往Autoresizing在布局方面的不足之处,以及未来面对更多尺寸适配时界面布局可以更好的适应. 为什么要用Auto Layout? Autolayout能解决不同屏幕(iPhone4,iPhone5,iPad...)之间的适配问题. 在iPhone4时代开发者只需要适

Auto Layout Guide----(二)-----Auto Layout Without Constraints

Auto Layout Without Constraints 没有约束的自动布局 Stack views provide an easy way to leverage the power of Auto Layout without introducing the complexity of constraints. A single stack view defines a row or column of user interface elements. The stack view a

【转】有了Auto Layout,为什么你还是害怕写UITabelView的自适应布局?

Apple 算是最重视应用开发体验的公司了.从Xib到StoryBoard,从Auto Layout到Size Class,每一次的更新,都会给iOS应用的开发带来不小的便利.但是,对于绝对多数iOS攻城狮来说,我们依然还是很害怕写UITabelVIew的自适应布局.当然,害怕不是因为我们不会写,或者本身有什么特殊的技术点,而是因为太麻烦.当然,文章的后半部分,会给出相应的解决方案,毕竟本文不是为了吐槽而吐槽. UITabelView的自适应布局有多麻烦? 数据类型的不确定性:种类越多,页面越复

【Auto Layout】Xcode6创建Auto Layout 约束时产生的一些变化【iOS开发教程】

[#Auto Layout#]Xcode6创建Auto Layout 约束时产生的一些变化 ? ? ? 运行效果: 没有从顶部开始,似乎是从statusbar的20高度以外开始计算的 ? ? ? ? ? 另外在设置顶部约束和底部约束时也尽量不要选择默认的,尽量点击右侧的小箭头,在弹框中选择父视图,如下图所示: ? ? Created: 05/24/2015Link:?http://www.cnblogs.com/ChenYilong/p/4526893.html

iOS 使用LayoutGuide 来限制控件的位置,配合Auto Layout constraints

UIButton *button = [UIButton buttonWithType:UIButtonTypeDetailDisclosure]; [self.view addSubview:button]; [button setTranslatesAutoresizingMaskIntoConstraints: NO]; // 得到当前视图的最低基准限制,这个是对于Auto Layout constraint来说的. id bottomGuide = self.bottomLayoutGu

iOS屏幕适配方案-Auto Layout

市场上的android手机五花八门.各种尺寸的屏幕让android程序员们比較头疼. 也有一些大神写了一些博客提出了自己的观点.iOS貌似也迎来了大屏6+,因此屏幕适配的问题也是有滴,因此苹果也有自己的方法-auto Layout . 本人初学iOS.今天学了自己主动布局.在学习的过程中,毕竟还是有些知识点没有接触到的,因此写这篇博客来深入的了解一下Auto Layout. 官方解释: Auto Layout 是一个系统,能够让你通过创建元素之间关系的数学描写叙述来布局应用程序的用户界面.--<