從 Auto Layout 的佈局算法談性能

這是使用 ASDK 性能調優系列的第二篇文章,前一篇文章中講到了如何提高 iOS 應用的渲染性能,你能夠點擊 這裏 瞭解這部分的內容。html

在上一篇文章中,咱們提到了 iOS 界面的渲染過程以及如何對渲染過程進行優化。ASDK 的作法是將渲染繪製的工做拋到後臺線程進行,並在每次 Runloop 結束時,將繪製結果交給 CALayer 進行展現。前端

而這篇文章就要從 iOS 中影響性能的另外一大殺手,也就是萬惡之源 Auto Layout(自動佈局)來分析如何對 iOS 應用的性能進行優化以及 Auto Layout 到底爲何會影響性能?node

box-layout

把 Auto Layout 批判一番

因爲在 2012 年蘋果發佈了 4.0 寸的 iPhone5,在 iOS 平臺上出現了不一樣尺寸的移動設備,使得原有的 frame 佈局方式沒法很好地適配不一樣尺寸的屏幕,因此,爲了解決這一問題 Auto Layout 就誕生了。ios

Auto Layout 的誕生並無如同蘋果的其它框架同樣收到開發者的好評,它自誕生的第一天起就飽受 iOS 開發者的批評,其蹩腳、冗長的語法使得它在剛剛面世就被無數開發者吐槽,寫了幾個屏幕的代碼都不能完成一個簡單的佈局,哪怕是 VFL(Visual Format Language)也拯救不了它。git

真正使 Auto Layout 大規模投入使用的應該仍是 Masonry,它使用了鏈式的語法對 Auto Layout 進行了很好的封裝,使得 Auto Layout 更加簡單易用;時至今日,開發者也在平常使用中發現了 Masonry 的各類問題,因而出現了各類各樣的佈局框架,不過這都是後話了。github

masonry

Auto Layout 的原理和 Cassowary

Auto Layout 的原理其實很是簡單,在這裏經過一個例子先簡單的解釋一下:算法

view-demonstrate

iOS 中視圖所須要的佈局信息只有兩個,分別是 origin/centersize,在這裏咱們以 origin & size 爲例,也就是 frame 時代下佈局的須要的兩個信息;這兩個信息由四部分組成:數組

  • x & y
  • width & height

以左上角的 (0, 0) 爲座標的原點,找到座標 (x, y),而後繪製一個大小爲 (width, height) 的矩形,這樣就完成了一個最簡單的佈局。而 Auto Layout 的佈局方式與上面所說的 frame 有些不一樣,frame 表示與父視圖之間的絕對距離,可是 Auto Layout 中大部分的約束都是描述性的,表示視圖間相對距離,以上圖爲例:緩存

A.left = Superview.left + 50
A.top  = Superview.top + 30
A.width  = 100
A.height = 100

B.left = (A.left + A.width)/(A.right) + 30
B.top  = A.top
B.width  = A.width
B.height = A.height複製代碼

雖然上面的約束很好的表示了各個視圖之間的關係,可是 Auto Layout 實際上並無改變原有的 Hard-Coded 形式的佈局方式,只是將原有沒有太多意義的 (x, y) 值,變成了描述性的代碼。併發

咱們仍然須要知道佈局信息所須要的四部分 xywidth 以及 height。換句話說,咱們要求解上述的八元一次方程組,將每一個視圖所須要的信息解出來;Cocoa 會在運行時求解上述的方程組,最終使用 frame 來繪製視圖。

layout-phase

Cassowary 算法

在上世紀 90 年代,一個名叫 Cassowary) 的佈局算法解決了用戶界面的佈局問題,它經過將佈局問題抽象成線性等式和不等式約束來進行求解。

Auto Layout 其實就是對 Cassowary 算法的一種實現,可是這裏並不會對它展開介紹,有興趣的讀者能夠在文章最後的 Reference 中瞭解一下 Cassowary 算法相關的文章。

Auto Layout 的原理就是對線性方程組或者不等式的求解。

Auto Layout 的性能

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

在涉及衝突發生時,Auto Layout 會嘗試 break 一些優先級低的約束,儘可能知足最多而且優先級最高的約束。

由於佈局系統在最後仍然須要經過 frame 來進行,因此 Auto Layout 雖然爲開發者在描述佈局時帶來了一些好處,不過它相比原有的佈局系統加入了從約束計算 frame 的過程,而在這裏,咱們須要瞭解 Auto Layout 的佈局性能如何。

performance-loss

由於使用 Cassowary 算法解決約束問題就是對線性等式或不等式求解,因此其時間複雜度就是多項式時間的,不難推測出,在處理極其複雜的 UI 界面時,會形成性能上的巨大損失。

在這裏咱們會對 Auto Layout 的性能進行測試,爲了更明顯的展現 Auto Layout 的性能,咱們經過 frame 的性能創建一條基準線以消除對象的建立和銷燬、視圖的渲染、視圖層級的改變帶來的影響

你能夠在 這裏 找到此次對 Layout 性能測量使用的代碼。

代碼分別使用 Auto Layout 和 frame 對 N 個視圖進行佈局,測算其運行時間。

使用 AutoLayout 時,每一個視圖會隨機選擇兩個視圖對它的 topleft 進行約束,隨機生成一個數字做爲 offset;同時,還會用幾個優先級高的約束保證視圖的佈局不會超出整個 keyWindow

而下圖就是對 100~1000 個視圖佈局所須要的時間的折線圖。

這裏的數據是在 OS X EL Captain,Macbook Air (13-inch Mid 2013)上的 iPhone 6s Plus 模擬器上採集的, Xcode 版本爲 7.3.1。在其餘設備上可能不會得到一致的信息,因爲筆者的 iPhone 升級到了 iOS 10,因此沒有辦法真機測試,最後的結果可能會有必定的誤差。

performance-chart-100-1000

從圖中能夠看到,使用 Auto Layout 進行佈局的時間會是隻使用 frame16 倍左右,雖然這裏的測試結果可能受外界條件影響差別比較大,不過 Auto Layout 的性能相比 frame 確實差不少,若是去掉設置 frame 的過程消耗的時間,Auto Layout 過程進行的計算量也是很是巨大的。

在上一篇文章中,咱們曾經提到,想要讓 iOS 應用的視圖保持 60 FPS 的刷新頻率,咱們必須在 1/60 = 16.67 ms 以內完成包括佈局、繪製以及渲染等操做。

也就是說若是當前界面上的視圖大於 100 的話,使用 Auto Layout 是很難達到絕對流暢的要求的;而在使用 frame 時,同一個界面下哪怕有 500 個視圖,也是能夠在 16.67 ms 以內完成佈局的。不過在通常狀況下,在 iOS 的整個 UIWindow 中也不會一次性出現如此多的視圖。

咱們更關心的是,在平常開發中不免會使用 Auto Layout 進行佈局,既然有 16.67 ms 這個限制,那麼在界面上出現了多少個視圖時,我才須要考慮其它的佈局方式呢?在這裏,咱們將須要佈局的視圖數量減小一個量級,從新繪製一個圖表:

performance-layout-10-90

從圖中能夠看出,當對 30 個左右視圖使用 Auto Layout 進行佈局時,所須要的時間就會在 16.67 ms 左右,固然這裏不排除一些其它因素的影響;到目前爲止,會得出一個大體的結論,使用 Auto Layout 對複雜的 UI 界面進行佈局時(大於 30 個視圖)就會對性能有嚴重的影響(同時與設備有關,文章中不會考慮設備性能的差別性)。

上述對 Auto Layout 的使用仍是比較簡單的,而在平常使用中,使用嵌套的視圖層級又很是正常。

在筆者對嵌套視圖層級中使用 Auto Layout 進行佈局時,當視圖的數量超過了 500 時,模擬器直接就 crash 了,因此這裏沒有超過 500 個視圖的數據。

咱們對嵌套視圖數量在 100~500 之間佈局時間進行測量,並與 Auto Layout 進行比較:

performance-nested-autolayout-frame

在視圖數量大於 200 以後,隨着視圖數量的增長,使用 Auto Layout 對嵌套視圖進行佈局的時間相比非嵌套的佈局成倍增加。

雖說 Auto Layout 爲開發者在多尺寸佈局上提供了遍歷,並且支持跨越視圖層級的約束,可是因爲其實現原理致使其時間複雜度爲多項式時間,其性能損耗是僅使用 frame 的十幾倍,因此在處理龐大的 UI 界面時表現差強人意。

在三年之前,有一篇關於 Auto Layout 性能分析的文章,能夠點擊這裏瞭解這篇文章的內容 Auto Layout Performance on iOS

ASDK 的佈局引擎

Auto Layout 不止在複雜 UI 界面佈局的表現不佳,它還會強制視圖在主線程上佈局;因此在 ASDK 中提供了另外一種能夠在後臺線程中運行的佈局引擎,它的結構大體是這樣的:

layout-hierarchy

ASLayoutSpec 與下面的全部的 Spec 類都是繼承關係,在視圖須要佈局時,會調用 ASLayoutSpec 或者它的子類的 - measureWithSizeRange: 方法返回一個用於佈局的對象 ASLayout

ASLayoutable 是 ASDK 中一個協議,遵循該協議的類實現了一系列的佈局方法。

當咱們使用 ASDK 佈局時,須要作下面四件事情中的一件:

  • 提供 layoutSpecBlock
  • 覆寫 - layoutSpecThatFits: 方法
  • 覆寫 - calculateSizeThatFits: 方法
  • 覆寫 - calculateLayoutThatFits: 方法

只有作上面四件事情中的其中一件才能對 ASDK 中的視圖或者說結點進行佈局。

方法 - calculateSizeThatFits: 提供了手動佈局的方式,經過在該方法內對 frame 進行計算,返回一個當前視圖的 CGSize

- layoutSpecThatFits:layoutSpecBlock 其實沒什麼不一樣,只是前者經過覆寫方法返回 ASLayoutSpec;後者經過 block 的形式提供一種不須要子類化就能夠完成佈局的方法,二者能夠看作是徹底等價的。

- calculateLayoutThatFits: 方法有一些不一樣,它把上面的兩種佈局方式:手動佈局和 Spec 佈局封裝成了一個接口,這樣,不管是 CGSize 仍是 ASLayoutSpec 最後都會以 ASLayout 的形式返回給方法調用者。

手動佈局

這裏簡單介紹一下手動佈局使用的 -[ASDisplayNode calculatedSizeThatFits:] 方法,這個方法與 UIView 中的 -[UIView sizeThatFits:] 很是類似,其區別只是在 ASDK 中,全部的計算出的大小都會經過緩存來提高性能。

- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize {
  return _preferredFrameSize;
}複製代碼

子類能夠在這個方法中進行計算,經過覆寫這個方法返回一個合適的大小,不過通常狀況下都不會使用手動佈局的方式。

使用 ASLayoutSpec 佈局

在 ASDK 中,更加經常使用的是使用 ASLayoutSpec 佈局,在上面提到的 ASLayout 是一個保存佈局信息的媒介,而真正計算視圖佈局的代碼都在 ASLayoutSpec 中;全部 ASDK 中的佈局(手動 / Spec)都是由 -[ASLayoutable measureWithSizeRange:] 方法觸發的,在這裏咱們以 ASDisplayNode 的調用棧爲例看一下方法的執行過程:

-[ASDisplayNode measureWithSizeRange:]
    -[ASDisplayNode shouldMeasureWithSizeRange:]
    -[ASDisplayNode calculateLayoutThatFits:]
        -[ASDisplayNode layoutSpecThatFits:]
        -[ASLayoutSpec measureWithSizeRange:]
        +[ASLayout layoutWithLayoutableObject:constrainedSizeRange:size:sublayouts:]
        -[ASLayout filteredNodeLayoutTree]複製代碼

ASDK 的文檔中推薦在子類中覆寫 - layoutSpecThatFits: 方法,返回一個用於佈局的 ASLayoutSpec 對象,而後使用 ASLayoutSpec 中的 - measureWithSizeRange: 方法對它指定的視圖進行佈局,不過經過覆寫 ASDK 的佈局引擎 一節中的其它方法也都是能夠的。

若是咱們使用 ASStackLayoutSpec 對視圖進行佈局的話,方法調用棧大概是這樣的:

-[ASDisplayNode measureWithSizeRange:]
    -[ASDisplayNode shouldMeasureWithSizeRange:]
    -[ASDisplayNode calculateLayoutThatFits:]
        -[ASDisplayNode layoutSpecThatFits:]
        -[ASStackLayoutSpec measureWithSizeRange:]
            ASStackUnpositionedLayout::compute
            ASStackPositionedLayout::compute            ASStackBaselinePositionedLayout::compute        +[ASLayout layoutWithLayoutableObject:constrainedSizeRange:size:sublayouts:]
        -[ASLayout filteredNodeLayoutTree]複製代碼

這裏只是執行了 ASStackLayoutSpec 對應的 - measureWithSizeRange: 方法,對其中的視圖進行佈局。在 - measureWithSizeRange: 中調用了一些 C++ 方法 ASStackUnpositionedLayoutASStackPositionedLayout 以及 ASStackBaselinePositionedLayoutcompute 方法,這些方法完成了對 ASStackLayoutSpec 中視圖的佈局。

相比於 Auto Layout,ASDK 實現了一種徹底不一樣的佈局方式;比較相似與前端開發中的 Flexbox 模型,而 ASDK 其實就實現了 Flexbox 的一個子集。

在 ASDK 1.0 時代,不少開發者都表示但願 ASDK 中加入 ComponentKit 的佈局引擎;而如今,ASDK 佈局引擎的大部分代碼都是從 ComponentKit 中移植過來的(ComponentKit 是另外一個 Facebook 團隊開發的用於佈局的框架)。

ASLayout

ASLayout 表示當前的結點在佈局樹中的大小和位置;固然,它還有一些其它的奇怪的屬性:

@interface ASLayout : NSObject

@property (nonatomic, weak, readonly) id<ASLayoutable> layoutableObject;
@property (nonatomic, readonly) CGSize size;
@property (nonatomic, readwrite) CGPoint position;
@property (nonatomic, readonly) NSArray<ASLayout *> *sublayouts;
@property (nonatomic, readonly) CGRect frame;

...

@end複製代碼

代碼中的 layoutableObject 表示當前的對象,sublayouts 表示當前視圖的子佈局 ASLayout 數組。

整個類的實現都沒有什麼值得多說的,除了大量的構造方法,惟一一個作了一些事情的就是 -[ASLayout filteredNodeLayoutTree] 方法了:

- (ASLayout *)filteredNodeLayoutTree {
  NSMutableArray *flattenedSublayouts = [NSMutableArray array];
  struct Context {
    ASLayout *layout;
    CGPoint absolutePosition;
  };
  std::queue<Context> queue;
  queue.push({self, CGPointMake(0, 0)});
  while (!queue.empty()) {
    Context context = queue.front();
    queue.pop();

    if (self != context.layout && context.layout.type == ASLayoutableTypeDisplayNode) {
      ASLayout *layout = [ASLayout layoutWithLayout:context.layout position:context.absolutePosition];
      layout.flattened = YES;
      [flattenedSublayouts addObject:layout];
    }

    for (ASLayout *sublayout in context.layout.sublayouts) {
      if (sublayout.isFlattened == NO) queue.push({sublayout, context.absolutePosition + sublayout.position});
  }

  return [ASLayout layoutWithLayoutableObject:_layoutableObject
                         constrainedSizeRange:_constrainedSizeRange
                                         size:_size
                                   sublayouts:flattenedSublayouts];
}複製代碼

而這個方法也只是將 sublayouts 中的內容展平,而後實例化一個新的 ASLayout 對象。

ASLayoutSpec

ASLayoutSpec 的做用更像是一個抽象類,在真正使用 ASDK 的佈局引擎時,都不會直接使用這個類,而是會用相似 ASStackLayoutSpecASRelativeLayoutSpecASOverlayLayoutSpec 以及 ASRatioLayoutSpec 等子類。

筆者不打算一行一行代碼深刻講解其內容,簡單介紹一下最重要的 ASStackLayoutSpec

stack

ASStackLayoutSpecFlexbox 中得到了很是多的靈感,好比說 justifyContentalignItems 等屬性,它和蘋果的 UIStackView 比較相似,不過底層並無使用 Auto Layout 進行計算。若是沒有接觸過 ASStackLayoutSpec 的開發者,能夠經過這個小遊戲 Foggy-ASDK-Layout 快速學習 ASStackLayoutSpec 的使用。

關於緩存以及異步併發

由於計算視圖的 CGRect 進行佈局是一種很是昂貴的操做,因此 ASDK 在這裏面加入了緩存機制,在每次執行 - measureWithSizeRange: 方法時,都會經過 -shouldMeasureWithSizeRange: 判斷是否須要從新計算佈局:

- (BOOL)shouldMeasureWithSizeRange:(ASSizeRange)constrainedSize {
  return [self _hasDirtyLayout] || !ASSizeRangeEqualToSizeRange(constrainedSize, _calculatedLayout.constrainedSizeRange);
}

- (BOOL)_hasDirtyLayout {
  return _calculatedLayout == nil || _calculatedLayout.isDirty;
}複製代碼

在通常狀況下,只有當前結點被標記爲 dirty 或者這一次佈局傳入的 constrainedSize 不一樣時,才須要進行從新計算。在不須要從新計算佈局的狀況下,只須要直接返回 _calculatedLayout 佈局對象就能夠了。

由於 ASDK 實現的佈局引擎其實只是對 frame 的計算,因此不管是在主線程仍是後臺的異步併發進程中都是能夠執行的,也就是說,你能夠在任意線程中調用 - measureWithSizeRange: 方法,ASDK 中的一些 ViewController 好比:ASDataViewController 就會在後臺併發進程中執行該方法:

- (NSArray<ASCellNode *> *)_layoutNodesFromContexts:(NSArray<ASIndexedNodeContext *> *)contexts {
  ...

  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  dispatch_apply(nodeCount, queue, ^(size_t i) {
    ASIndexedNodeContext *context = contexts[i];
    ASCellNode *node = [context allocateNode];
    if (node == nil) node = [[ASCellNode alloc] init];

    CGRect frame = CGRectZero;
    frame.size = [node measureWithSizeRange:context.constrainedSize].size;
    node.frame = frame;

    [ASDataController _didLayoutNode];
  });

  ...

  return nodes;
}複製代碼

上述代碼作了比較大的修改,將原有一些方法調用放到了當前方法中,並省略了大量的代碼。

關於性能的對比

因爲 ASDK 的佈局引擎的問題,其性能比較難以測試,在這裏只對 ASDK 使用 ASStackLayoutSpec佈局計算時間進行了測試,不包括視圖的渲染以及其它時間:

async-node-calculate

測試結果代表 ASStackLayoutSpec 花費的佈局時間與結點的數量成正比,哪怕計算 100 個視圖的佈局也只須要 8.89 ms,雖然這裏沒有包括視圖的渲染時間,不過與 Auto Layout 相比性能仍是有比較大的提高。

總結

其實 ASDK 的佈局引擎大部分都是對 ComponentKit 的封裝,不過因爲擺脫了 Auto Layout 這一套低效可是通用的佈局方式,ASDK 的佈局計算不只在後臺併發線程中進行、並且經過引入 Flexbox 提高了佈局的性能,可是 ASDK 的使用相對比較複雜,若是隻想對佈局性能進行優化,更推薦單獨使用 ComponentKit 框架。

References

Github Repo:iOS-Source-Code-Analyze

Follow: Draveness · GitHub

Source: draveness.me/layout-perf…

相關文章
相關標籤/搜索