iOS之UIKit的布局和绘制

整理对于iOS绘制和布局的知识。

一.iOS的主RunLoop

iOS 的主 RunLoop 负责处理所有的用户输入事件并触发相应的响应。所有的用户交互都会被加入到一个事件队列中。UIApplication对象会从队列中取出事件并将它们分发到应用中的其他对象上。当视图响应之后(视图响应者),控制流回到主 RunLoop 上,然后开始 update cycle(更新周期),Update cycle 负责布局并且重新渲染视图们 views。

main_event_loop.jpg

二.Update Cycle

Update cycle 是当应用完成了你的所有事件处理代码后,控制流回到主 RunLoop 时的那个时间点。正是在这个时间点上系统开始更新布局、显示和设置约束。

如果你在处理事件的代码中请求修改了一个 view,那么系统就会把这个 view 标记为需要重画(redraw)。在接下来的 Update cycle 中,系统就会执行这些 view 上的更改。

用户交互和布局更新间的延迟几乎不会被用户察觉到。iOS 应用一般以 60 fps 的速度展示动画,就是说每个更新周期只需要 1/60 秒。这个更新的过程很快,所以用户在和应用交互时感觉不到 UI 中的更新延迟。但是由于在处理事件和对应 view 重画间存在着一个间隔,RunLoop 中的某时刻的 view 更新可能不是你想要的那样。如果你的代码中的某些计算依赖于当下的 view 内容或者是布局,那么就有在过时的(错误的) view 信息上操作的风险。对此,需要理解UIView中几个重要的布局方法来避免这类问题。

下面的图展示出了 update cycle 发生在 RunLoop 的尾部。

tech-blog-loop.png

三.Layout

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

layoutSubviews()

这个 UIView 方法处理对视图(view)及其所有子视图(subview)的重新定位和大小调整。它负责给出当前 view 和每个子 view 的位置和大小。这个方法很开销很大,因为它会在每个子视图上起作用并且调用它们相应的 layoutSubviews 方法。系统会在任何它需要重新计算视图的 frame 的时候自动调用这个方法,所以你应该在需要更新 frame 来重新定位或更改大小时重载它。

但是你不应该显式调用这个方法。相反,有许多可以在 runloop 的不同时间点触发 layoutSubviews 调用的机制,这些触发机制比直接调用 layoutSubviews 的资源消耗要小得多。

layoutSubviews 完成后,在 view 的所有者 viewController 上,会触发 viewDidLayoutSubviews 调用。因为 viewDidLayoutSubviews 是 view 布局更新后会被唯一可靠调用的方法,所以你应该把所有依赖于布局或者大小的代码放在 viewDidLayoutSubviews 中,而不是放在 viewDidLoad 或者 viewDidAppear 中。这是避免使用过时的布局或者位置变量的唯一方法。

Automatic refresh

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

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

setNeedsLayout()

触发 layoutSubviews 调用的最省资源的方法就是在你的视图上调用 setNeedsLaylout 方法。调用这个方法代表向系统表示视图的布局需要重新计算。setNeedsLayout 方法会立刻执行并返回,但在返回前不会真正更新视图。视图会在下一个 update cycle 中更新.

layoutIfNeeded()

layoutIfNeeded 是另一个会让 UIView 触发 layoutSubviews 的方法。 当视图需要更新的时候,与 setNeedsLayout() 会让视图在下一周期调用 layoutSubviews 更新视图不同,layoutIfNeeded 会立即调用 layoutSubviews 方法。但是如果你调用了 layoutIfNeeded 之后,并且没有任何操作向系统表明需要刷新视图,那么就不会调用 layoutSubview。如果你在同一个 runLoop 内调用两次 layoutIfNeeded,并且两次之间没有更新视图,第二个调用同样不会触发 layoutSubviews 方法。

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

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

四.Display

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

draw:

UIViewdraw 方法是对视图内容显示的操作,类似于视图布局的 layoutSubviews。但是不同于 layoutSubviews``,draw 方法不会触发后续对视图的子视图方法的调用。主要注意的是:你不应该直接调用 draw 方法,而应该通过调用触发方法,让系统在 runLoop 中的不同节点自动调用。

setNeedsDisplay()

这个方法类似于布局中的 setNeedsLayout 。它会给有内容更新的视图设置一个脏标记,但在视图重绘之前就会返回。然后在下一个 update cycle 中,系统会遍历所有已标标记的视图,并调用它们的 draw 方法。

大部分时候,在视图中更新任何 UI 组件都会自动把相应的视图标记为“dirty”,通过设置视图“内部更新标记”,在下一次 update cycle 中就会重绘,而不需要显式的 setNeedsDisplay 调用。

下面的代码例子中,通过设置drawType的值,进行自定义绘制,并在didSet中调用 setNeedsLayout.

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

    override func draw(_ rect: CGRect) {
        switch self.drawType {
            case 0: return
            case 1: drawPoint(rect)
            case 2: drawLine(rect)
            case 3: drawRectangle(rect)
            default: drawEllipse(rect)
        }
    }
}

视图的显示方法里没有类似布局中的 layoutIfNeeded 这样可以触发立即更新的方法。

五.Constraints 约束

自动布局包含三步来布局和重绘视图。第一步是更新约束,系统会计算并给视图设置所有要求的约束。第二步是布局阶段,布局引擎计算视图和子视图的 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() ,而不会等到 runLoop 的末尾。

invalidateIntrinsicContentSize()

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

比如UILableUIImageView等都有intrinsicContentSize 属性,可以不用设置它的大小,而通过intrinsicContentSize自动算出来。

六.总结

布局、显示和约束都遵循着相似的模式,例如他们更新的方式以及如何在 run loop 的不同时间点上强制更新。

任一组件都有一个实际去更新的方法(layoutSubviews, draw, 和 updateConstraints),你可以重写来手动操作视图,但是任何情况下都不要显式调用。如果视图被标记了需要被更新的话,则这个方法会在runLoop的末端被调用。(有一些操作会自动设置这个标志,但是也有一些方法允许您显式地设置它。)

connects.png

下面的流程图总结了 update cycle 和 event loop 之间的交互,并指出了上文提到的方法在 run loop 运行期间的位置。

你可以在 run loop 中的任意一点显式地调用 layoutIfNeeded 或者 updateConstraintsIfNeeded,需要记住,这开销会很大。

在循环的末端是 update cycle 时期,如果视图被设置了特定的 “update constraints”,“update layout” 或者 “needs display” 标记,在这节点会更新约束、布局以及展示。一旦这些更新结束,runloop 会重新启动。

runloop_update.png

七.概括

1.setNeedsDisplay或者setNeedsDisplay(rect:CGRect)

  • 标记相应的视图区域需要重绘
  • 调用之后不会立即重绘,而是在下一个绘制周期里绘制
  • 会调用View的draw(_ rect: CGRect)方法
  • 不会调用layoutSubviews()方法

2.setNeedsLayout方法

  • 不会立即更新界面,会在下一个刷新周期里更新
  • 需要在主线程调用此方法
  • 不管尺寸有没有更改都会会调用layoutSubviews()方法

3.layoutIfNeeded方法

  • 会立即更新视图
  • 使用自动布局的视图会默认更新改变的尺寸
  • 可在动画里使用该属性
  • 有需要刷新的标记会立即调用,没有则不会调用

4.layoutSubviews调用时机

  • 初始化时设置frame不为Zero会触发
  • 直接调用[self setNeedsLayout]
  • addSubview时
  • 当view的size发送改变的时候,前提是frame的值前后发生了变化
  • 滑动UIScrollView的时候
  • 旋转屏幕 可能会触发
  • 更新视图的 constraint

5.如果要立即刷新

  • 先调用[view setNeedsLayout],标记为需要布局,然后调用[view layoutIfNeeded],实现布局

本文是整理学习文章,大部分非原创,参考链接:
1.Demystifying iOS Layout
2.帅气的军大王

END。
我是小侯爷。
在帝都艰苦奋斗,白天是上班族,晚上是知识服务工作者。
如果读完觉得有收获的话,记得关注和点赞哦。
非要打赏的话,我也是不会拒绝的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,547评论 4 374
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,787评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,175评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,752评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,169评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,056评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,195评论 2 321
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,980评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,724评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,872评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,344评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,667评论 3 264
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,379评论 3 245
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,202评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,992评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,189评论 2 286
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,987评论 2 279

推荐阅读更多精彩内容

  • 翻译自:Demystifying iOS Layout 在你刚开始开发 iOS 应用时,最难避免或者是调试的就是和...
    Mr大喵喵阅读 390评论 0 3
  • 在你刚开始开发 iOS 应用时,最难避免或者是调试的就是和布局相关的问题。通常这种问题发生的原因就是对于 view...
    MccReeee阅读 579评论 0 2
  • 理解Update Cycle摘自《[译] 揭秘 iOS 布局》 UPdate Cycle是当应用完成了你所有的事件...
    我是繁星阅读 1,818评论 0 1
  • 1、前言 首先,我认为学习总结,要有所总,所结,就是有归纳后,能用自己的话告诉别人!有所结,就是有所收获输出,一般...
    iHTCboy阅读 623评论 0 1
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 120,811评论 2 7