[ WWDC2018 ] - 高效能 AutoLayout High Performance Auto Layout

weixin_34008784發表於2018-06-19

UICollectionView效能對比,item自動適配大小,iOS 11看上去有掉幀卡頓的現象,iOS 12表現完美,沒有掉幀。

11994763-da934720e8f87b84.png
WX20180612-104339.png

下面是iOS 11和iOS 12的效能對比,灰色條是iOS 11的耗時,藍色條是iOS 12的耗時。在iOS 12上會很大程度改善你的應用程式。

11994763-e2f16f8424beca68.png
WX20180619-160559.png

實現和感觀

render loop

render loop 是一個每秒鐘跑120次的一個程式,是為了確保所有的內容都能為每一個frame做好準備。lender loop 一共包括三個步驟來更新約束,佈局和渲染。

  • 首先,每一個需要接收到更新約束的view會從子view向上傳遞,直到window
  • 然後,每一個接收到的view開始layoutsubviews,和更新約束是從相反的方向開始,layout從window開始到每一個子view進行layout。
  • 最後,每一個需要渲染的view,和layout相同,從父view向子view開始渲染。
11994763-ef5dbdbac453249b.png
WX20180619-160634.png

render loop目的是為了避免重複的工作。
舉一個例子:一個UILable 需要一個約束來描述它的大小,但是有很多屬性會影響他的大小,設定它的font,text size等等都會受到影響。當一個屬性改變的時候,可能text其他屬性也會被重新賦值
,很有可能呼叫一堆屬性的setter方法,這樣效率會很低。
只需要呼叫updateConstraints 並指定好要更新的屬性,render loop會幫助你計算好它的frame並完成渲染,從而避免多次設定的重複工作。


11994763-09c14395ec3a3210.png
WX20180619-160709.png

在設定約束的一些不好的寫法,每次開始的時候呼叫deactivate,設定結束之後呼叫activate。相當於layoutsubviews,每次呼叫layoutsubviews你銷燬你subviews,重新建立在重新新增。這樣效能不會很好。

// Don’t do this! Removes and re-adds constraints potentially at 120 frames per second
    override func updateConstraints() {
        NSLayoutConstraint.deactivate(myConstraints)
        myConstraints.removeAll()
        let views = ["text1":text1, "text2":text2]
        myConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[text1]-[text2]",
                                                        options: [.alignAllFirstBaseline],
                                                        myConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[text1]-|",
                                                                                                        metrics: nil, views: views)
            options: [],
            metrics: nil, views: views)
        NSLayoutConstraint.activate(myConstraints)
        super.updateConstraints()
    }

每次都是移除並重新新增,相當於這樣的程式碼

    // Don’t do this! Removes and re-adds constraints potentially at 120 frames per second
    override func layoutSubviews() {
        text1.removeFromSuperview()
        text1 = nil
        text1 = UILabel(frame: CGRect(x: 20, y: 20, width: 300, height: 30))
        self.addSubview(text1)
        
        text2.removeFromSuperview()
        text2 = nil
        text2 = UILabel(frame: CGRect(x: 340, y: 20, width: 300, height: 30))
        self.addSubview(text2)
        super.layoutSubviews()
    }

官方建議寫法為,約束只需要新增一次,每次呼叫super.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()
    }

render loop有很強的特定性,它的好處可以避免一些重複性的工作。但是它也很危險,因為它呼叫的頻率會很高,是非常敏感的一段程式碼。

蘋果建議使用interface builder進行佈局。


11994763-9e5147cd1ae54667.png
WX20180619-160810.png

啟用一個約束

在設定約束的時候發生了什麼事情呢?從下面的圖中可以看到整體的一個結構。
有一個view 在window上,window上面有個叫做engine的內部物件,engine是autolayout計算的核心,當新增一個約束的時候,會建立一個Equation物件,然後會把equation物件新增到engine上,equation依據variables物件。


11994763-7d7f06e4b8798859.png
WX20180619-160848.png

variables相當於每一個約束的值,比如說一個UIlabel有四個約束minX minY width height那麼minX minY width height 就是variables。


11994763-6eb8f852e55d9c99.png
WX20180619-160919.png

以下面這個圖為例,這裡只關注水平方向的佈局,首先要建立equation,然後每一個equation會新增給engine。


11994763-679395f6dccd07aa.png
WX20180619-161418.png

engine會去計算這些variables,engine會把每一個view的variables用數學公式計算出一個定量。


11994763-f45f30aa9ad7acba.png
WX20180619-161501.png

計算出定量之後,engine會傳送通知,通知view呼叫他父view的setNeedsLayout()方法,就會完成render loop的第一步更新約束,然後繼續render loop的 layout更新,最後view會直接拷貝engine計算好的定量進行賦值渲染。


11994763-5cc7c174ba41780f.png
WX20180619-161533.png

engine是一個layout的快取,和依賴的追蹤器。非常有方向性的,它知道哪些約束會影響哪些view,當你改變一些約束時,它能夠準確的更新。

不需要的約束不要加

你也可以穿過層級,為兩個沒有相同父view的view設定約束,但是這樣效能會很差。
大多數情況下,view的約束應該加在他的父view或者兄弟view上。


11994763-9bd760067ff49940.png
WX20180619-161603.png

最小限度的錯誤

當view向engine獲取約束的值的時候,engine會確保錯誤率最小


11994763-589e61d732652b3b.png
WX20180619-161637.png

構建高效能layout

建立一個layout

構建一個社交軟體的cell,通過autolayout進行佈局。


11994763-b5e72c2792fd709c.png
WX20180619-161704.png

查詢程式碼中的問題

下面是beta版的一個除錯工具,最上面第一項表示你CPU的使用情況,峰值的地方可能需要關注一下你的layout是否有效能問題,下面一行追蹤你的約束,高的地方說明是有問題的。
第二項是你對約束新增、刪除、修改等操作的記錄。
第三項是當前控制元件的大小。

11994763-33360dec164c77e2.png
WX20180619-161733.png

點選約束峰值的地方可以看詳情。

建立高效能的佈局

通過instrument除錯工具,可以看出一些佈局上的耗時問題。一下是需要注意的幾點:

  • 避免刪除所有的約束的情況
  • 對於靜態約束,只需要新增一次
  • 只改變需要改變的約束
  • 儘量用hide() 方法隱藏view,而不是remove然後在add

有些控制元件比較特殊,比如 UIImageView,它的大小是根據他的image計算確定他的content size。UILabel是根據他的text確定的。這些都會返回它們的固有尺寸,UIView 會直接通過他們的固有尺寸來當做約束條件。

重寫 intrinsicContentSize

text的計算是成本很高的,所以UIlabel的size通過text去控制計算開銷成本會很高。這個時候我們可以 通過重寫 UILabel 的 intrinsicContentSize 來直接控制它的固有尺寸。如果已知一個UILabel的展示size,直接重寫其屬性,其他情況使用UIView.noIntrinsicMetric。

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


參考:WWDC2018《High Performance Auto Layout》

相關文章