[譯] 揭祕 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
相關文章
相關標籤/搜索