[译] 揭秘 iOS 布局

翻译自:Demystifying iOS Layouthtml

在你刚开始开发 iOS 应用时,最难避免或者是调试的就是和布局相关的问题。一般这种问题发生的缘由就是对于 view 什么时候真正更新的错误理解。想理解 view 在什么时候是如何更新的,须要对 iOS RunLoop 和相关的 UIView 方法有深入的理解。这篇文章会介绍这些关联,但愿能帮你澄清如何用 UIView 的方法来得到正确的行为。ios

一个 iOS 应用的主 RunLoop

一个 iOS 应用的主 RunLoop 负责处理全部的用户输入事件并触发相应的响应。全部的用户交互都会被加入到一个事件队列中。下图中的 Application object 会从队列中取出事件并将它们分发到应用中的其余对象上。本质上它会解释这些来自用户的输入事件,而后调用在应用中的 Core objects 相应的处理代码,而这些代码再调用开发者写的代码。当这些方法调用返回后,控制流回到主 RunLoop 上,而后开始 update cycle(更新周期)。Update cycle 负责布局而且从新渲染视图们(接下来会讲到)。下面的图片展现了应用是如何和设备交互而且处理用户输入的。app

Main Event Loop

developer.apple.com/library/con…ide

Update Cycle

Update cycle 是当应用完成了你的全部事件处理代码后控制流回到主 RunLoop 时的那个时间点。正是在这个时间点上系统开始更新布局、显示和设置约束。若是你在处理事件的代码中请求修改了一个 view,那么系统就会把这个 view 标记为须要重画(redraw)。在接下来的 Update cycle 中,系统就会执行这些 view 上的更改。用户交互和布局更新间的延迟几乎不会被用户察觉到。iOS 应用通常以 60 fps 的速度展现动画,就是说每一个更新周期只须要 1/60 秒。这个更新的过程很快,因此用户在和应用交互时感受不到 UI 中的更新延迟。可是因为在处理事件和对应 view 重画间存在着一个间隔,RunLoop 中的某时刻的 view 更新可能不是你想要的那样。若是你的代码中的某些计算依赖于当下的 view 内容或者是布局,那么就有在过期 view 信息上操做的风险。理解 RunLoop、update cycle 和 UIView 中具体的方法能够帮助避免或者能够调试这类问题。下面的图展现出了 update cycle 发生在 RunLoop 的尾部。函数

Update Cycle

布局

一个视图的布局指的是它在屏幕上的的大小和位置。每一个 view 都有一个 frame 属性,用来表示在父 view 坐标系中的位置和具体的大小。UIView 给你提供了用来通知系统某个 view 布局发生变化的方法,也提供了在 view 布局从新计算后调用的可重写的方法。oop

layoutSubviews()布局

这个 UIView 方法处理对视图(view)及其全部子视图(subview)的从新定位和大小调整。它负责给出当前 view 和每一个子 view 的位置和大小。这个方法很开销很大,由于它会在每一个子视图上起做用而且调用它们相应的 layoutSubviews 方法。系统会在任何它须要从新计算视图的 frame 的时候调用这个方法,因此你应该在须要更新 frame 来从新定位或更改大小时重载它。然而你不该该在代码中显式调用这个方法。相反,有许多能够在 run loop 的不一样时间点触发 layoutSubviews 调用的机制,这些触发机制比直接调用 layoutSubviews 的资源消耗要小得多。动画

layoutSubviews 完成后,在 view 的全部者 view controller 上,会触发 viewDidLayoutSubviews 调用。由于 viewDidLayoutSubviews 是 view 布局更新后会被惟一可靠调用的方法,因此你应该把全部依赖于布局或者大小的代码放在 viewDidLayoutSubviews 中,而不是放在 viewDidLoad 或者 viewDidAppear 中。这是避免使用过期的布局或者位置变量的惟一方法。ui

自动刷新触发器spa

有许多事件会自动给视图打上 “update layout” 标记,所以 layoutSubviews 会在下一个周期中被调用,而不须要开发者手动操做。这些自动通知系统 view 的布局发生变化的方式有:

  • 修改 view 的大小
  • 新增 subview
  • 用户在 UIScrollView 上滚动(layoutSubviews 会在 UIScrollView 和它的父 view 上被调用)
  • 用户旋转设备
  • 更新视图的 constraints

这些方式都会告知系统 view 的位置须要被从新计算,继而会自动转化为一个最终的 layoutSubviews 调用。固然,也有直接触发 layoutSubviews 的方法。

setNeedsLayout()

触发 layoutSubviews 调用的最省资源的方法就是在你的视图上调用 setNeedsLaylout 方法。调用这个方法表明向系统表示视图的布局须要从新计算。setNeedsLayout 方法会马上执行并返回,但在返回前不会真正更新视图。视图会在下一个 update cycle 中更新,就在系统调用视图们的 layoutSubviews 以及他们的全部子视图的 layoutSubviews 方法的时候。即便从 setNeedsLayout 返回后到视图被从新绘制并布局之间有一段任意时间的间隔,可是这个延迟不会对用户形成影响,由于永远不会长到对界面形成卡顿。

layoutIfNeeded()

layoutIfNeeded 是另外一个会让 UIView 触发 layoutSubviews 的方法。 当视图须要更新的时候,与 setNeedsLayout() 会让视图在下一周期调用 layoutSubviews 更新视图不一样,layoutIfNeeded 会当即调用 layoutSubviews 方法。可是若是你调用了 layoutIfNeeded 以后,而且没有任何操做向系统代表须要刷新视图,那么就不会调用 layoutsubview。若是你在同一个 run loop 内调用两次 layoutIfNeeded,而且两次之间没有更新视图,第二个调用一样不会触发 layoutSubviews 方法。

使用 layoutIfNeeded,则布局和重绘会当即发生并在函数返回以前完成(除非有正在运行中的动画)。这个方法在你须要依赖新布局,没法等到下一次 update cycle 的时候会比 setNeedsLayout 有用。除非是这种状况,不然你更应该使用 setNeedsLayout,这样在每次 run loop 中都只会更新一次布局。

当对但愿经过修改 constraint 进行动画时,这个方法特别有用。你须要在 animation block 以前对 self.view 调用 layoutIfNeeded,以确保在动画开始以前传播全部的布局更新。在 animation block 中设置新 constrait 后,须要再次调用 layoutIfNeeded 来动画到新的状态。

显示

一个视图的显示包含了颜色、文本、图片和 Core Graphics 绘制等视图属性,不包括其自己和子视图的大小和位置。和布局的方法相似,显示也有触发更新的方法,它们由系统在检测到更新时被自动调用,或者咱们能够手动调用直接刷新。

draw(_:)

UIViewdraw 方法(本文使用 Swift,对应 Objective-C 的 drawRect)对视图内容显示的操做,相似于视图布局的 layoutSubviews ,可是不一样于 layoutSubviewsdraw 方法不会触发后续对视图的子视图方法的调用。一样,和 layoutSubviews 同样,你不该该直接调用 draw 方法,而应该经过调用触发方法,让系统在 run loop 中的不一样结点自动调用。

setNeedsDisplay()

这个方法相似于布局中的 setNeedsLayout 。它会给有内容更新的视图设置一个内部的标记,但在视图重绘以前就会返回。而后在下一个 update cycle 中,系统会遍历全部已标标记的视图,并调用它们的 draw 方法。若是你只想在下次更新时重绘部分视图,你能够调用 setNeedsDisplay(_:),并把须要重绘的矩形部分传进去(setNeedsDisplayInRect in OC)。大部分时候,在视图中更新任何 UI 组件都会把相应的视图标记为“dirty”,经过设置视图“内部更新标记”,在下一次 update cycle 中就会重绘,而不须要显式的 setNeedsDisplay 调用。然而若是你有一个属性没有绑定到 UI 组件,但须要在每次更新时重绘视图,你能够定义他的 didSet 属性,而且调用 setNeedsDisplay 来触发视图合适的更新。

有时候设置一个属性要求自定义绘制,这种状况下你须要重写 draw 方法。在下面的例子中,设置 numberOfPoints 会触发系统系统根据具体点数绘制视图。在这个例子中,你须要在 draw 方法中实现自定义绘制,并在 numberOfPoints 的 property observer 里调用 setNeedsDisplay

class MyView: UIView {
    var numberOfPoints = 0 {
        didSet {
            setNeedsDisplay()
        }
    }

    override func draw(_ rect: CGRect) {
        switch numberOfPoints {
        case 0:
            return
        case 1:
            drawPoint(rect)
        case 2:
            drawLine(rect)
        case 3:
            drawTriangle(rect)
        case 4:
            drawRectangle(rect)
        case 5:
            drawPentagon(rect)
        default:
            drawEllipse(rect)
        }
    }
}
复制代码

视图的显示方法里没有相似布局中的 layoutIfNeeded 这样能够触发当即更新的方法。一般状况下等到下一个更新周期再从新绘制视图也无所谓。

约束

自动布局包含三步来布局和重绘视图。第一步是更新约束,系统会计算并给视图设置全部要求的约束。第二步是布局阶段,布局引擎计算视图和子视图的 frame 而且将它们布局。最后一步完成这一循环的是显示阶段,重绘视图的内容,如实现了 draw 方法则调用 draw

updateConstraints()

这个方法用来在自动布局中动态改变视图约束。和布局中的 layoutSubviews() 方法或者显示中的 draw 方法相似,updateConstraints() 只应该被重载,毫不要在代码中显式地调用。一般你只应该在 updateConstraints 方法中实现必需要更新的约束。静态的约束应该在 interface builder、视图的初始化方法或者 viewDidLoad() 方法中指定。

一般状况下,设置或者解除约束、更改约束的优先级或者常量值,或者从视图层级中移除一个视图时都会设置一个内部的标记 “update constarints”,这个标记会在下一个更新周期中触发调用 updateConstrains。固然,也有手动给视图打上“update constarints” 标记的方法,以下。

setNeedsUpdateConstraints()

调用 setNeedsUpdateConstraints() 会保证在下一次更新周期中更新约束。它经过标记“update constraints”来触发 updateConstraints()。这个方法和 setNeedsDisplay() 以及 setNeedsLayout() 方法的工做机制相似。

updateConstraintsIfNeeded()

对于使用自动布局的视图来讲,这个方法与 layoutIfNeeded 等价。它会检查 “update constraints”标记(能够被 setNeedsUpdateConstraints 或者 invalidateInstrinsicContentSize方法自动设置)。若是它认为这些约束须要被更新,它会当即触发 updateConstraints() ,而不会等到 run loop 的末尾。

invalidateIntrinsicContentSize()

自动布局中某些视图拥有 intrinsicContentSize 属性,这是视图根据它的内容获得的天然尺寸。一个视图的 intrinsicContentSize 一般由所包含的元素的约束决定,但也能够经过重载提供自定义行为。调用 invalidateIntrinsicContentSize() 会设置一个标记表示这个视图的 intrinsicContentSize 已通过期,须要在下一个布局阶段从新计算。

它们是如何链接起来的

布局、显示和约束都遵循着类似的模式,例如他们更新的方式以及如何在 run loop 的不一样时间点上强制更新。任一组件都有一个实际去更新的方法(layoutSubviews, draw, 和 updateConstraints),你能够重写来手动操做视图,可是任何状况下都不要显式调用。这个方法只在 run loop 的末端会被调用,若是视图被标记了告诉系统该视图须要被更新的标记的话。有一些操做会自动设置这个标志,可是也有一些方法容许您显式地设置它。对于布局和约束相关的更新,若是你等不到在 run loop 末端才更新(例如:其余行为依赖于新布局),有方法可让你当即更新,并保证 “update layout” 标记被正确标记。下面的表格列出了任意组件会怎样更新及其对应方法。

屏幕快照 2017-10-16 上午12.43.38.png

下面的流程图总结了 update cycle 和 event loop 之间的交互,并指出了上文提到的方法在 run loop 运行期间的位置。你能够在 run loop 中的任意一点显式地调用 layoutIfNeeded 或者 updateConstraintsIfNeeded,须要记住,这开销会很大。在循环的末端是 update cycle,若是视图被设置了特定的 “update constraints”,“update layout” 或者 “needs display” 标记,在这节点会更新约束、布局以及展现。一旦这些更新结束,runloop 会从新启动。

Update Cycle
相关文章
相关标签/搜索