WWDC 2018:高性能 Auto Layout

WWDC 2018 Session 220: High Performance Auto Layout
做者簡介:@冬瓜爭作全棧瓜,今日頭條 iOS 工程師,Sepicat 做者。git

1. 關於 Auto Layout 的歷史淵源

上世紀 90 年代,名叫 Cassowary 的佈局算法,經過將佈局問題抽象成線性不等式,並分解成多個位置間的約束,解決了用戶界面的佈局問題。github

Apple 自從 iOS 6 引入了 Auto Layout 的佈局概念,其實就是對 Cassowary 佈局算法的一種實現。在使用 Auto Layout 進行佈局時,能夠指定一系列的約束,好比視圖的高度、寬度等等。而每個約束其實都是一個簡單的線性等式或不等式,整個界面上的全部約束在一塊兒就明確地(沒有衝突)定義了整個系統的佈局。算法

對於 Auto Layout 算法部分,本文不作展開。在這裏咱們僅僅須要知道,Auto Layout 的原理,就是在對 Layout 問題抽象的方程組求解,就能夠繼續向下閱讀。緩存

如下就是 WWDC 220 Session - 高性能 Auto Layout 高度脫水版。markdown

2. iOS 上的性能表現

下圖是 Ken Ferry 在 Session 現場的演示,能夠比較清晰的看出,左圖自使用佈局的 CollectionView 上下滑動較右圖而言更加流暢,Ken 在描述中也說到 iOS 12 在該例中的全部滑動事件是滿幀狀態。(左 iOS 12,右 iOS 11)app

下圖是官方測試後獲得的 iOS 12 和 iOS 11 在特定場景下時間開銷的對比圖。能夠明顯的看到 iOS 12 具備很大的優點。less

那麼到底是如何作到這個優化的呢?ide

3. 內部實現和感觀體驗

咱們首先來經過一個例子總體的瞭解一下。分析一下這個簡單的 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,在這以前先要了解一些預備知識。

3.1 updateConstraints 原理 - Render Loop

Render Loop 這個過程是用來確保全部的 UI 視圖在每秒的全部幀中都表現出對應表現,正常狀況下每秒會運行 120 次。這個工程分紅三步:

  1. 更新約束:從子視圖向外層逐級更新約束;
  2. Layout 調整:從外部向內,逐級視圖得到自身的 Layout;
  3. 渲染與展現:與 Layout 相同,呈現順序從外向內,使得視圖呈現出來;

固然,這麼敘述仍是有些抽象。其實這三個過程在咱們平常開發中也是常常接觸的三類方法:

/// 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 也是高危的,由於它常常會被調用。

下面咱們來深究一下這個過程的原理。

3.2 增長約束的內部實現

當咱們爲空間增長一個約束 Constraint 的時候,經過這些約束會組成一個多元一次方程組,這個方程組的解能夠定位那些經過約束可間接計算出的定量。而這個計算過程是 Auto Layout 引擎來完成處理的。求出的解集在 UIView 渲染過程當中,當作其 frame 屬性中的值來使用。下圖就是反應了這麼一個過程。

在計算引擎計算出解集後,計算引擎還有他最後的一個工做,就是發送通知,使得對應的 UIView 調用其父視圖的 setNeedsLayout() 方法。這也就是咱們以前提到的更新約束這個步驟,經過向外層調用 setNeedsLayouts() 方法,咱們能夠驗證這個由內向外的步驟。

在約束更新完成以後,進入了第二個步驟,也就是 Layout 調整階段。每一個視圖會從計算引擎中獲取到其子視圖所需的全部數據,獲取到以後從新爲子視圖賦值。從這點看出,Layout 調整階段是自外向內的。

咱們再來思考一下上文說起到的 規則攪動 問題,若是咱們每次將約束規則刪除、從新添加,則每一次刷新視圖都會重新經歷一遍引擎的解集重計算、由內向外的 setNeedsLayout()、自外向內的 Layout 調整。而這些實際上是不須要的。

對於一次約束的增長過程至此也就大致講完了。咱們來總結一下這裏我提到的一些主要內容:

  1. 不要因爲自身的問題從而帶來 規則攪動 的錯誤;
  2. Auto Layout 的數學原理,就是基本的代數運算。
  3. Auto Layout 計算引擎是一個佈局緩存和關係依賴的跟蹤器;
  4. 須要什麼就對什麼作出約束,不要增長額外的約束,避免形成沒必要要的開銷;

4. 創建一個有效的 Layout

4.1 使用 Instrument 來捕捉規則攪動

在使用 Auto Layout 佈局來實現 UITableView,咱們常常會發現滑動卡頓的問題。這些問題在開發的時候很難查出緣由所在。爲了方便的解決並排查問題,新版的 Xcode 增長了一個新的工具 - Instrument for Layout

這個工具的第一行 Layout Time 反應了 CPU 的使用狀況,經過運算時間能夠和後面的異常值進行比對。

第二行用於檢測咱們上文提到的 規則攪動 的問題,當代碼中出現大量的重複添加相同約束的錯誤時,會以直方圖時間複雜度的形式呈現出來,便於咱們作進一步的代碼排查。

第三行來顯示約束的增、刪、改的操做。

最後一行,咱們會對 UILabel 這個控件的 Layout 佔中單獨展現出來。由於咱們的示例 App 中只有 UILabel,固然若是你的應用中有其餘的視圖,也會按照類型來分行呈現。

其實縱觀這個工具,他可以幫助咱們的僅僅是查看約束的計算耗時以及是否出現了 規則攪動。可是這些都是咱們在代碼中能夠直接避免的。這裏有幾個關於避免 規則攪動 的 Tips 告訴你們:

  1. 儘可能不要刪除全部的約束(Avoid removing all constraints);
  2. 如果一個靜態約束,僅作一次添加操做便可;
  3. 僅改變須要改變的約束;
  4. 儘可能不要作刪除視圖的操做,反之用 hide() 方法替代;

通常作到這四點,能夠避免絕大多數的 規則攪動 代碼層面的錯誤。

某些控件是十分特殊的,例如 UIImageViewUILabel 這種,他們都有一個自適應的尺寸,這裏咱們稱之爲固有尺寸(Intrinsic Content Size),當咱們不對其做出特殊化的 height 和 width 限制時,UIView 會直接用他們的固有尺寸(UIImageView 即圖片尺寸,UILabel 即文本尺寸)來當作約束條件。

4.2 Override intrinsicContentSize 來調整 UILabel 約束性能

在不少控件組成的頁面中,UILabel 的 Size 計算會在全部的計算開銷中佔很大的比重。這時候追求極致,咱們能夠 Override UILabelintrinsicContentSize 來告訴計算引擎,如何抉擇 UILabel 的 Size 問題。若是已知一個 UILabel 的展現 Size,直接 Override 其屬性便可,不然對其設置成 UIView.noIntrinsicMetric

override var intrinsicContentSize: CGSize {
    return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
}
複製代碼

4.3 不要過分使用 systemLayoutSizeFitting()

systemLayoutSizeFitting() 雖然能幫助咱們根據 Layout 來自動計算其約束,可是縱觀整個 Layout 過程,其計算的時間開銷是十分大的。這個方法調用,其目的是從計算引擎中從新得到調用方法對應視圖的 Size。然而這個過程較爲複雜。

也許整個流程並不複雜,可是對於咱們 Render Loop 過程,至關於做出了一次重複步驟。在 iOS 12 中,Apple 再次對自適應 Cell 做出了優化,因此在大多數狀況下,減小 systemLayoutSizeFitting() 的調用可使得時間開銷再次削減。

5 總述

以上即是筆者對於這個 Session 的全部記錄和脫水敘述。如同 Ken 所說,也許簡單的對於 Auto Layout 中約束的 Tips 並不能知足於你,這裏還有一些資料能夠供你去繼續學習。

  • 學習 Auto Layout 中的日誌能夠有效地幫助你 Debug;
  • 學習 Debug 的相關 Session;
  • 能夠前往 WWDC 2015 查看 Session 219 - Mysteries of Auto Layout, Part 2,爲你帶來 Auto Layout 實現及原理。
相關文章
相關標籤/搜索