整理對(duì)于iOS繪制和布局的知識(shí)赢赊。
一.iOS的主RunLoop
iOS 的主 RunLoop 負(fù)責(zé)處理所有的用戶輸入事件并觸發(fā)相應(yīng)的響應(yīng)辽狈。所有的用戶交互都會(huì)被加入到一個(gè)事件隊(duì)列中着茸。UIApplication
對(duì)象會(huì)從隊(duì)列中取出事件并將它們分發(fā)到應(yīng)用中的其他對(duì)象上敬特。當(dāng)視圖響應(yīng)之后(視圖響應(yīng)者)掰伸,控制流回到主 RunLoop 上歧蕉,然后開(kāi)始 update cycle(更新周期),Update cycle 負(fù)責(zé)布局并且重新渲染視圖們 views。
二.Update Cycle
Update cycle 是當(dāng)應(yīng)用完成了你的所有事件處理代碼后逃呼,控制流回到主 RunLoop 時(shí)的那個(gè)時(shí)間點(diǎn)推姻。正是在這個(gè)時(shí)間點(diǎn)上系統(tǒng)開(kāi)始更新布局拧晕、顯示和設(shè)置約束饲嗽。
如果你在處理事件的代碼中請(qǐng)求修改了一個(gè) view,那么系統(tǒng)就會(huì)把這個(gè) view 標(biāo)記為需要重畫(redraw)。在接下來(lái)的 Update cycle 中,系統(tǒng)就會(huì)執(zhí)行這些 view 上的更改揖盘。
用戶交互和布局更新間的延遲幾乎不會(huì)被用戶察覺(jué)到。iOS 應(yīng)用一般以 60 fps 的速度展示動(dòng)畫,就是說(shuō)每個(gè)更新周期只需要 1/60 秒鹿蜀。這個(gè)更新的過(guò)程很快婉商,所以用戶在和應(yīng)用交互時(shí)感覺(jué)不到 UI 中的更新延遲。但是由于在處理事件和對(duì)應(yīng) view 重畫間存在著一個(gè)間隔箫攀,RunLoop 中的某時(shí)刻的 view 更新可能不是你想要的那樣缀雳。如果你的代碼中的某些計(jì)算依賴于當(dāng)下的 view 內(nèi)容或者是布局,那么就有在過(guò)時(shí)的(錯(cuò)誤的) view 信息上操作的風(fēng)險(xiǎn)梢睛。對(duì)此肥印,需要理解UIView
中幾個(gè)重要的布局方法來(lái)避免這類問(wèn)題。
下面的圖展示出了 update cycle 發(fā)生在 RunLoop 的尾部绝葡。
三.Layout
一個(gè)視圖的布局指的是它在屏幕上的的大小和位置深碱。每個(gè) view 都有一個(gè) frame 屬性,用來(lái)表示在父 view 坐標(biāo)系中的位置和具體的大小藏畅。UIView
給你提供了用來(lái)通知系統(tǒng)某個(gè) view 布局發(fā)生變化的方法敷硅,也提供了在 view 布局重新計(jì)算后調(diào)用的回調(diào)方法。
layoutSubviews()
這個(gè) UIView
方法處理對(duì)視圖(view)及其所有子視圖(subview)的重新定位和大小調(diào)整。它負(fù)責(zé)給出當(dāng)前 view 和每個(gè)子 view 的位置和大小竞膳。這個(gè)方法很開(kāi)銷很大航瞭,因?yàn)樗鼤?huì)在每個(gè)子視圖上起作用并且調(diào)用它們相應(yīng)的 layoutSubviews
方法。系統(tǒng)會(huì)在任何它需要重新計(jì)算視圖的 frame 的時(shí)候自動(dòng)調(diào)用這個(gè)方法坦辟,所以你應(yīng)該在需要更新 frame 來(lái)重新定位或更改大小時(shí)重載它刊侯。
但是你不應(yīng)該顯式調(diào)用這個(gè)方法。相反锉走,有許多可以在 runloop 的不同時(shí)間點(diǎn)觸發(fā) layoutSubviews
調(diào)用的機(jī)制滨彻,這些觸發(fā)機(jī)制比直接調(diào)用 layoutSubviews
的資源消耗要小得多。
當(dāng) layoutSubviews
完成后挪蹭,在 view 的所有者 viewController 上亭饵,會(huì)觸發(fā) viewDidLayoutSubviews
調(diào)用。因?yàn)?viewDidLayoutSubviews
是 view 布局更新后會(huì)被唯一可靠調(diào)用的方法梁厉,所以你應(yīng)該把所有依賴于布局或者大小的代碼放在 viewDidLayoutSubviews
中辜羊,而不是放在 viewDidLoad
或者 viewDidAppear
中。這是避免使用過(guò)時(shí)的布局或者位置變量的唯一方法词顾。
Automatic refresh
有許多操作會(huì)自動(dòng)給視圖打上 “update layout” 標(biāo)記八秃,因此 layoutSubviews
會(huì)在下一個(gè)周期中被調(diào)用,而不需要開(kāi)發(fā)者手動(dòng)操作肉盹。這些自動(dòng)通知系統(tǒng) view 的布局發(fā)生變化的方式有:
- 修改 view 的大小
- 新增 subview
- 用戶在
UIScrollView
上滾動(dòng)(layoutSubviews
會(huì)在UIScrollView
和它的父 view 上被調(diào)用) - 用戶旋轉(zhuǎn)設(shè)備
- 更新視圖的 constraints
setNeedsLayout()
觸發(fā) layoutSubviews
調(diào)用的最省資源的方法就是在你的視圖上調(diào)用 setNeedsLaylout
方法昔驱。調(diào)用這個(gè)方法代表向系統(tǒng)表示視圖的布局需要重新計(jì)算。setNeedsLayout
方法會(huì)立刻執(zhí)行并返回上忍,但在返回前不會(huì)真正更新視圖骤肛。視圖會(huì)在下一個(gè) update cycle 中更新.
layoutIfNeeded()
layoutIfNeeded
是另一個(gè)會(huì)讓 UIView
觸發(fā) layoutSubviews
的方法。 當(dāng)視圖需要更新的時(shí)候窍蓝,與 setNeedsLayout()
會(huì)讓視圖在下一周期調(diào)用 layoutSubviews
更新視圖不同腋颠,layoutIfNeeded
會(huì)立即調(diào)用 layoutSubviews
方法。但是如果你調(diào)用了 layoutIfNeeded
之后吓笙,并且沒(méi)有任何操作向系統(tǒng)表明需要刷新視圖淑玫,那么就不會(huì)調(diào)用 layoutSubview
。如果你在同一個(gè) runLoop 內(nèi)調(diào)用兩次 layoutIfNeeded
观蓄,并且兩次之間沒(méi)有更新視圖混移,第二個(gè)調(diào)用同樣不會(huì)觸發(fā) layoutSubviews
方法。
使用 layoutIfNeeded
侮穿,則布局和重繪會(huì)立即發(fā)生并在函數(shù)返回之前完成(除非有正在運(yùn)行中的動(dòng)畫)歌径。這個(gè)方法在你需要依賴新布局,無(wú)法等到下一次 update cycle 的時(shí)候會(huì)比 setNeedsLayout
有用亲茅。除非是這種情況回铛,否則你更應(yīng)該使用 setNeedsLayout
狗准,這樣在每次 runLoop 中都只會(huì)更新一次布局。
當(dāng)對(duì)希望通過(guò)修改 constraint 進(jìn)行動(dòng)畫時(shí)茵肃,這個(gè)方法特別有用腔长。你需要在 animation block 之前對(duì) self.view
調(diào)用 layoutIfNeeded
,以確保在動(dòng)畫開(kāi)始之前傳播所有的布局更新验残。在 animation block 中設(shè)置新 constraint 后捞附,需要再次調(diào)用 layoutIfNeeded
來(lái)動(dòng)畫到新的狀態(tài)。
四.Display
一個(gè)視圖的顯示包含了顏色您没、文本鸟召、圖片和 Core Graphics 繪制等視圖屬性,不包括其本身和子視圖的大小和位置氨鹏。和布局的方法類似欧募,顯示也有觸發(fā)更新的方法,它們由系統(tǒng)在檢測(cè)到更新時(shí)被自動(dòng)調(diào)用仆抵,或者我們可以手動(dòng)調(diào)用直接刷新跟继。
draw:
UIView
的 draw
方法是對(duì)視圖內(nèi)容顯示的操作,類似于視圖布局的 layoutSubviews
镣丑。但是不同于 layoutSubviews``舔糖,draw
方法不會(huì)觸發(fā)后續(xù)對(duì)視圖的子視圖方法的調(diào)用。主要注意的是:你不應(yīng)該直接調(diào)用 draw
方法传轰,而應(yīng)該通過(guò)調(diào)用觸發(fā)方法剩盒,讓系統(tǒng)在 runLoop 中的不同節(jié)點(diǎn)自動(dòng)調(diào)用谷婆。
setNeedsDisplay()
這個(gè)方法類似于布局中的 setNeedsLayout
慨蛙。它會(huì)給有內(nèi)容更新的視圖設(shè)置一個(gè)臟標(biāo)記,但在視圖重繪之前就會(huì)返回纪挎。然后在下一個(gè) update cycle 中期贫,系統(tǒng)會(huì)遍歷所有已標(biāo)標(biāo)記的視圖,并調(diào)用它們的 draw
方法异袄。
大部分時(shí)候通砍,在視圖中更新任何 UI 組件都會(huì)自動(dòng)把相應(yīng)的視圖標(biāo)記為“dirty”,通過(guò)設(shè)置視圖“內(nèi)部更新標(biāo)記”烤蜕,在下一次 update cycle 中就會(huì)重繪封孙,而不需要顯式的 setNeedsDisplay
調(diào)用。
下面的代碼例子中讽营,通過(guò)設(shè)置drawType
的值虎忌,進(jìn)行自定義繪制,并在didSet中調(diào)用 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)
}
}
}
視圖的顯示方法里沒(méi)有類似布局中的
layoutIfNeeded
這樣可以觸發(fā)立即更新的方法橱鹏。
五.Constraints 約束
自動(dòng)布局包含三步來(lái)布局和重繪視圖膜蠢。第一步是更新約束堪藐,系統(tǒng)會(huì)計(jì)算并給視圖設(shè)置所有要求的約束。第二步是布局階段挑围,布局引擎計(jì)算視圖和子視圖的 frame 并且將它們布局礁竞。第三步是顯示階段,重繪視圖的內(nèi)容杉辙,如實(shí)現(xiàn)了 draw
方法則調(diào)用 draw
模捂。
updateConstraints()
這個(gè)方法用來(lái)在自動(dòng)布局中動(dòng)態(tài)改變視圖約束。和布局中的 layoutSubviews()
方法或者顯示中的 draw
方法類似蜘矢,updateConstraints()
只應(yīng)該被重載枫绅,絕不要在代碼中顯式地調(diào)用。
通常你只應(yīng)該在 updateConstraints
方法中實(shí)現(xiàn)必須要更新的約束硼端。靜態(tài)的約束應(yīng)該在 interface builder并淋、視圖的初始化方法或者 viewDidLoad()
方法中指定。
通常情況下珍昨,設(shè)置或者解除約束县耽、更改約束的優(yōu)先級(jí)或者常量值,或者從視圖層級(jí)中移除一個(gè)視圖時(shí)都會(huì)設(shè)置一個(gè)內(nèi)部的標(biāo)記 “update constarints”镣典,這個(gè)標(biāo)記會(huì)在下一個(gè)更新周期中觸發(fā)調(diào)用 updateConstrains()
兔毙。當(dāng)然,也有手動(dòng)給視圖打上“update constarints” 標(biāo)記的方法兄春,如下澎剥。
setNeedsUpdateConstraints()
調(diào)用 setNeedsUpdateConstraints()
會(huì)保證在下一次更新周期中更新約束。它通過(guò)標(biāo)記“update constraints”來(lái)觸發(fā) updateConstraints()
赶舆。這個(gè)方法和 setNeedsDisplay()
以及 setNeedsLayout()
方法的工作機(jī)制類似哑姚。
updateConstraintsIfNeeded()
對(duì)于使用自動(dòng)布局的視圖來(lái)說(shuō),這個(gè)方法與 layoutIfNeeded
等價(jià)芜茵。它會(huì)檢查 “update constraints”標(biāo)記(可以被 setNeedsUpdateConstraints
或者 invalidateInstrinsicContentSize
方法自動(dòng)設(shè)置)叙量。如果它認(rèn)為這些約束需要被更新,它會(huì)立即觸發(fā) updateConstraints()
九串,而不會(huì)等到 runLoop 的末尾绞佩。
invalidateIntrinsicContentSize()
自動(dòng)布局中某些視圖擁有 intrinsicContentSize
屬性,這是視圖根據(jù)它的內(nèi)容得到的自然尺寸猪钮。一個(gè)視圖的 intrinsicContentSize
通常由所包含的元素的約束決定品山,但也可以通過(guò)重載提供自定義行為。調(diào)用 invalidateIntrinsicContentSize()
會(huì)設(shè)置一個(gè)標(biāo)記表示這個(gè)視圖的 intrinsicContentSize
已經(jīng)過(guò)期烤低,需要在下一個(gè)布局階段重新計(jì)算肘交。
比如
UILable
、UIImageView
等都有intrinsicContentSize
屬性拂玻,可以不用設(shè)置它的大小酸些,而通過(guò)intrinsicContentSize
自動(dòng)算出來(lái)宰译。
六.總結(jié)
布局、顯示和約束都遵循著相似的模式魄懂,例如他們更新的方式以及如何在 run loop 的不同時(shí)間點(diǎn)上強(qiáng)制更新沿侈。
任一組件都有一個(gè)實(shí)際去更新的方法(layoutSubviews
, draw
, 和 updateConstraints
),你可以重寫來(lái)手動(dòng)操作視圖市栗,但是任何情況下都不要顯式調(diào)用缀拭。如果視圖被標(biāo)記了需要被更新的話,則這個(gè)方法會(huì)在runLoop的末端被調(diào)用填帽。(有一些操作會(huì)自動(dòng)設(shè)置這個(gè)標(biāo)志蛛淋,但是也有一些方法允許您顯式地設(shè)置它。)
下面的流程圖總結(jié)了 update cycle 和 event loop 之間的交互篡腌,并指出了上文提到的方法在 run loop 運(yùn)行期間的位置褐荷。
你可以在 run loop 中的任意一點(diǎn)顯式地調(diào)用 layoutIfNeeded
或者 updateConstraintsIfNeeded
,需要記住嘹悼,這開(kāi)銷會(huì)很大叛甫。
在循環(huán)的末端是 update cycle 時(shí)期,如果視圖被設(shè)置了特定的 “update constraints”杨伙,“update layout” 或者 “needs display” 標(biāo)記其监,在這節(jié)點(diǎn)會(huì)更新約束、布局以及展示限匣。一旦這些更新結(jié)束抖苦,runloop 會(huì)重新啟動(dòng)。
七.概括
1.setNeedsDisplay
或者setNeedsDisplay(rect:CGRect)
- 標(biāo)記相應(yīng)的視圖區(qū)域需要重繪
- 調(diào)用之后不會(huì)立即重繪米死,而是在下一個(gè)繪制周期里繪制
- 會(huì)調(diào)用View的
draw(_ rect: CGRect)
方法 - 不會(huì)調(diào)用
layoutSubviews()
方法
2.setNeedsLayout
方法
- 不會(huì)立即更新界面锌历,會(huì)在下一個(gè)刷新周期里更新
- 需要在主線程調(diào)用此方法
- 不管尺寸有沒(méi)有更改都會(huì)會(huì)調(diào)用
layoutSubviews()
方法
3.layoutIfNeeded
方法
- 會(huì)立即更新視圖
- 使用自動(dòng)布局的視圖會(huì)默認(rèn)更新改變的尺寸
- 可在動(dòng)畫里使用該屬性
- 有需要刷新的標(biāo)記會(huì)立即調(diào)用,沒(méi)有則不會(huì)調(diào)用
4.layoutSubviews
調(diào)用時(shí)機(jī)
- 初始化時(shí)設(shè)置frame不為Zero會(huì)觸發(fā)
- 直接調(diào)用
[self setNeedsLayout]
- addSubview時(shí)
- 當(dāng)view的size發(fā)送改變的時(shí)候哲身,前提是frame的值前后發(fā)生了變化
- 滑動(dòng)
UIScrollView
的時(shí)候 - 旋轉(zhuǎn)屏幕 可能會(huì)觸發(fā)
- 更新視圖的 constraint
5.如果要立即刷新
- 先調(diào)用
[view setNeedsLayout]
辩涝,標(biāo)記為需要布局贸伐,然后調(diào)用[view layoutIfNeeded]
勘天,實(shí)現(xiàn)布局
本文是整理學(xué)習(xí)文章,大部分非原創(chuàng)捉邢,參考鏈接:
1.Demystifying iOS Layout
2.帥氣的軍大王
END脯丝。
我是小侯爺。
在帝都艱苦奮斗伏伐,白天是上班族宠进,晚上是知識(shí)服務(wù)工作者。
如果讀完覺(jué)得有收獲的話藐翎,記得關(guān)注和點(diǎn)贊哦材蹬。
非要打賞的話实幕,我也是不會(huì)拒絕的。