如何编写高性能的 Auto Layout

1,793 阅读3分钟

对于经常写 UI 页面的 iOS 开发者来说,Auto Layout 是提高开发效率的一大利器。但如果使用不当的话,也会对项目性能造成损害。所以,知道如何正确的使用 Auto Layout 还是很重要的。

本文首先会通过一个例子来说明下什么是约束流失,了解约束流失后,会带大家看一下 Render Loop 的工作流程。然后会说下 Auto Layout 的背后实现原理。最后,了解下 Auto Layout 特定情况下的最佳做法。让我们开始吧。

约束流失

什么是约束流失?

答:对于同一视图,进行不必要的删除和重新添加约束。

通过下面的例子来解释一下:

var myConstraints = [NSLayoutConstraint]()
let text1 = UILabel()
let text2 = UILabel()

override func updateViewConstraints() {
    // step1
    NSLayoutConstraint.deactivate(myConstraints)
    myConstraints.removeAll()
    // step2
    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: [.alignAllFirstBaseline],
                                                    metrics: nil,
                                                    views: views)
    // step3
    NSLayoutConstraint.activate(myConstraints)
    super.updateViewConstraints()
}

上面的代码做了三件事:

  • step1:将之前的约束失效并移除。
  • step2:重新设置约束。
  • step3:生效重新设置的约束。

移除,重新创建。直观上看这不是高效的代码,事实上也确实不是。它会给性能带来压力,因为这段代码会每秒执行很多次。

我们可以通过一个简单的 if 判断来避免无用的执行。

override func updateViewConstraints() {
    if myConstraints.isEmpty {
        var constrains = [NSLayoutConstraint]()
        
        let views = ["text1": text1, "text2": text2]
        constrains += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[text1]-[text2]",
                                                        options: [.alignAllFirstBaseline],
                                                        metrics: nil,
                                                        views: views)
        
        constrains += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[text1]-|",
                                                        options: [.alignAllFirstBaseline],
                                                        metrics: nil,
                                                        views: views)
        NSLayoutConstraint.activate(constrains)
        myConstraints = constrains
    }
    super.updateViewConstraints()
}

在更新约束的时候,首先判断当前约束是否有值,若有值则直接跳过;无值再创建约束赋值。这样就避免了多次执行移除,重新创建的流程。

Render Loop 概览

Render Loop 涉及更新约束、布局、显示三个阶段。下面是这三个阶段的流向: 截屏2021-05-11 下午3.23.31.png

更新布局是从底层视图一步步流向 window ,而布局恰巧相反,从 window 流向底层视图,显示则和布局流向一致。

Render Loop 的优点:可以避免无用的工作;注意事项:会运行很多次。所以我们应该谨慎使用。

Auto Layout 的背后实现

当我们给控件添加约束时,下面的四个值必须能计算出来,否则视图会显示不正常。

控件需计算的四个值:minX、minY、width、height。

截屏2021-05-11 下午3.32.38.png

比如上面的约束会替换成下面的公式:

  • text1
    • text1.minX = 20
    • text1.minY = 30
    • text1.width = 100
    • text1.height = 20
  • text2
    • text2.minX = text1.minX + text1.width + 10 最终得出 130
    • text2.minY = 30
    • text2.width = 100
    • text2.height = 20

也就是说 Auto Layout 就是用二元一次方程式来求出各个参数的值。

当我们写下上面的约束时,系统会创建一个 Engine,Engine 去负责约束的计算,最终 Engine 会把 minX、minY、width、height 的具体值返回给 View,View 则根据返回值调用 setNeedsLayout() 来更新视图。

截屏2021-05-11 下午3.49.31.png

最佳做法

  • 特定情况下需要隐藏控件
    • 直接使用 hidden 属性就好,不要移除控件或者约束。
  • 刷新视图
    • 避免移除所有约束,最好在当前约束的基础上修改。
    • 对于不变的约束确保只添加一次。
    • 只修改需要改变的约束。

总结

  • 不要让约束流失。
  • 约束底层计算只是简单的方程式计算。
  • 只为你的功能耗费性能,不做无谓的消耗。
  • 避免添加有歧义的约束,比如要求 view 的 width 即是 50 又是 200。