WWDC 2018 Session 220: High Performance Auto Layout
做者簡介:@冬瓜爭作全棧瓜,今日頭條 iOS 工程師,Sepicat 做者。git
上世紀 90 年代,名叫 Cassowary 的佈局算法,經過將佈局問題抽象成線性不等式,並分解成多個位置間的約束,解決了用戶界面的佈局問題。github
Apple 自從 iOS 6 引入了 Auto Layout 的佈局概念,其實就是對 Cassowary 佈局算法的一種實現。在使用 Auto Layout 進行佈局時,能夠指定一系列的約束,好比視圖的高度、寬度等等。而每個約束其實都是一個簡單的線性等式或不等式,整個界面上的全部約束在一塊兒就明確地(沒有衝突)定義了整個系統的佈局。算法
對於 Auto Layout 算法部分,本文不作展開。在這裏咱們僅僅須要知道,Auto Layout 的原理,就是在對 Layout 問題抽象的方程組求解,就能夠繼續向下閱讀。緩存
如下就是 WWDC 220 Session - 高性能 Auto Layout 高度脫水版。markdown
下圖是 Ken Ferry 在 Session 現場的演示,能夠比較清晰的看出,左圖自使用佈局的 CollectionView 上下滑動較右圖而言更加流暢,Ken 在描述中也說到 iOS 12 在該例中的全部滑動事件是滿幀狀態。(左 iOS 12,右 iOS 11)app
下圖是官方測試後獲得的 iOS 12 和 iOS 11 在特定場景下時間開銷的對比圖。能夠明顯的看到 iOS 12 具備很大的優點。less
那麼到底是如何作到這個優化的呢?ide
咱們首先來經過一個例子總體的瞭解一下。分析一下這個簡單的 Layout 場景:工具
下面咱們在 updateConstraints()
方法中來描述這個 Layout:oop
// Don’t do this! Removes and re-adds constraints potentially at 120 frames per second override func updateConstraints() { // 首先移除約束 NSLayoutConstraint.deactivate(myConstraints) // 而後對約束從新規則 myConstraints.removeAll() // 構造一個 view 字典便於visual format使用 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: [], metrics: nil, views: views) // 添加約束,與 deactivate 方法對應 NSLayoutConstraint.activate(myConstraints) // 調用父類的 updateConstraints() super.updateConstraints() } 複製代碼
至此咱們就實現了這個簡單的 Layout 方案。爲了繼續探究這個 Topic,在這以前先要了解一些預備知識。
Render Loop 這個過程是用來確保全部的 UI 視圖在每秒的全部幀中都表現出對應表現,正常狀況下每秒會運行 120 次。這個工程分紅三步:
固然,這麼敘述仍是有些抽象。其實這三個過程在咱們平常開發中也是常常接觸的三類方法:
/// Render Loop 過程 /// 過程一:更新約束 func updateConstraints(); func setNeedsUpdateConstraints(); func updateConstraintsIfNeeded(); /// 過程二:Layout 調整 func layoutSubviews(); func setNeedsLayout(); func layoutIfNeeded(); /// 過程三:渲染與展現 func draw(_:); func setNeedsDisplay(); 複製代碼
每一次調整都會運行這麼一個 Render Loop 步驟。這是一套很精確的 API,目的爲了讓各個環節中的工做不重不漏,從而除去了不少重複操做。如上例中,若是一個 UILabel
須要有一個約束來描述其大小,可是其中的不少屬性例如字條、字號等又會影響這個視圖的大小,這套 API 就是這樣,每次修改都會根據不一樣的屬性來肯定其尺寸。開發者能夠在其方法內部來指明在渲染前最後的屬性值,從而排除了屢次設置的重複操做。
瞭解了 Render Loop 咱們再來完善以前的代碼。會發如今每次在 updateConstraints
的時候,都會從新解除和增長一次約束,這顯然會使得性能變差。修改一下代碼:
// This is ok! Doesn’t do anything unless self.myConstraints has been nil’d out override func updateConstraints() { if self.myConstraints == nil { var constraints = [NSLayoutConstraint]() let views = ["text1":text1, "text2":text2] constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[text1]-[text2]", options: [.alignAllFirstBaseline], metrics: nil, views: views) constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[text1]-|", options: [], metrics: nil, views: views) NSLayoutConstraint.activate(constraints) self.myConstraints = constraints } super.updateConstraints() } 複製代碼
這個 nil
的判斷意思是若是咱們增長了約束,那麼就不用對其再次設置。這個錯誤也是開發者在客戶端開發中較常見的錯誤,這種無變化的約束設置咱們稱之爲 規則攪動 (Churning the Constraints) ,這種操做毫無心義且影響性能。
雖然 Render Loop 過程具備明確的目的性,可是這套 API 也是高危的,由於它常常會被調用。
下面咱們來深究一下這個過程的原理。
當咱們爲空間增長一個約束 Constraint 的時候,經過這些約束會組成一個多元一次方程組,這個方程組的解能夠定位那些經過約束可間接計算出的定量。而這個計算過程是 Auto Layout 引擎來完成處理的。求出的解集在 UIView
渲染過程當中,當作其 frame
屬性中的值來使用。下圖就是反應了這麼一個過程。
在計算引擎計算出解集後,計算引擎還有他最後的一個工做,就是發送通知,使得對應的 UIView
調用其父視圖的 setNeedsLayout()
方法。這也就是咱們以前提到的更新約束這個步驟,經過向外層調用 setNeedsLayouts()
方法,咱們能夠驗證這個由內向外的步驟。
在約束更新完成以後,進入了第二個步驟,也就是 Layout 調整階段。每一個視圖會從計算引擎中獲取到其子視圖所需的全部數據,獲取到以後從新爲子視圖賦值。從這點看出,Layout 調整階段是自外向內的。
咱們再來思考一下上文說起到的 規則攪動 問題,若是咱們每次將約束規則刪除、從新添加,則每一次刷新視圖都會重新經歷一遍引擎的解集重計算、由內向外的 setNeedsLayout()
、自外向內的 Layout 調整。而這些實際上是不須要的。
對於一次約束的增長過程至此也就大致講完了。咱們來總結一下這裏我提到的一些主要內容:
在使用 Auto Layout 佈局來實現 UITableView
,咱們常常會發現滑動卡頓的問題。這些問題在開發的時候很難查出緣由所在。爲了方便的解決並排查問題,新版的 Xcode 增長了一個新的工具 - Instrument for Layout。
這個工具的第一行 Layout Time 反應了 CPU 的使用狀況,經過運算時間能夠和後面的異常值進行比對。
第二行用於檢測咱們上文提到的 規則攪動 的問題,當代碼中出現大量的重複添加相同約束的錯誤時,會以直方圖時間複雜度的形式呈現出來,便於咱們作進一步的代碼排查。
第三行來顯示約束的增、刪、改的操做。
最後一行,咱們會對 UILabel
這個控件的 Layout 佔中單獨展現出來。由於咱們的示例 App 中只有 UILabel
,固然若是你的應用中有其餘的視圖,也會按照類型來分行呈現。
其實縱觀這個工具,他可以幫助咱們的僅僅是查看約束的計算耗時以及是否出現了 規則攪動。可是這些都是咱們在代碼中能夠直接避免的。這裏有幾個關於避免 規則攪動 的 Tips 告訴你們:
hide()
方法替代;通常作到這四點,能夠避免絕大多數的 規則攪動 代碼層面的錯誤。
某些控件是十分特殊的,例如 UIImageView
、UILabel
這種,他們都有一個自適應的尺寸,這裏咱們稱之爲固有尺寸(Intrinsic Content Size),當咱們不對其做出特殊化的 height 和 width 限制時,UIView
會直接用他們的固有尺寸(UIImageView
即圖片尺寸,UILabel
即文本尺寸)來當作約束條件。
UILabel
約束性能在不少控件組成的頁面中,UILabel
的 Size 計算會在全部的計算開銷中佔很大的比重。這時候追求極致,咱們能夠 Override UILabel
的 intrinsicContentSize
來告訴計算引擎,如何抉擇 UILabel
的 Size 問題。若是已知一個 UILabel
的展現 Size,直接 Override 其屬性便可,不然對其設置成 UIView.noIntrinsicMetric
。
override var intrinsicContentSize: CGSize { return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) } 複製代碼
systemLayoutSizeFitting()
systemLayoutSizeFitting()
雖然能幫助咱們根據 Layout 來自動計算其約束,可是縱觀整個 Layout 過程,其計算的時間開銷是十分大的。這個方法調用,其目的是從計算引擎中從新得到調用方法對應視圖的 Size。然而這個過程較爲複雜。
也許整個流程並不複雜,可是對於咱們 Render Loop 過程,至關於做出了一次重複步驟。在 iOS 12 中,Apple 再次對自適應 Cell 做出了優化,因此在大多數狀況下,減小 systemLayoutSizeFitting()
的調用可使得時間開銷再次削減。
以上即是筆者對於這個 Session 的全部記錄和脫水敘述。如同 Ken 所說,也許簡單的對於 Auto Layout 中約束的 Tips 並不能知足於你,這裏還有一些資料能夠供你去繼續學習。