這是使用 ASDK 性能調優系列的第二篇文章,前一篇文章中講到了如何提高 iOS 應用的渲染性能,你能夠點擊 這裏 瞭解這部分的內容。html
在上一篇文章中,咱們提到了 iOS 界面的渲染過程以及如何對渲染過程進行優化。ASDK 的作法是將渲染繪製的工做拋到後臺線程進行,並在每次 Runloop 結束時,將繪製結果交給 CALayer
進行展現。前端
而這篇文章就要從 iOS 中影響性能的另外一大殺手,也就是萬惡之源 Auto Layout(自動佈局)來分析如何對 iOS 應用的性能進行優化以及 Auto Layout 到底爲何會影響性能?node
因爲在 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
Auto Layout 的原理其實很是簡單,在這裏經過一個例子先簡單的解釋一下:算法
iOS 中視圖所須要的佈局信息只有兩個,分別是 origin/center
和 size
,在這裏咱們以 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)
值,變成了描述性的代碼。
咱們仍然須要知道佈局信息所須要的四部分 x
、y
、width
以及 height
。換句話說,咱們要求解上述的八元一次方程組,將每一個視圖所須要的信息解出來;Cocoa 會在運行時求解上述的方程組,最終使用 frame
來繪製視圖。
在上世紀 90 年代,一個名叫 Cassowary) 的佈局算法解決了用戶界面的佈局問題,它經過將佈局問題抽象成線性等式和不等式約束來進行求解。
Auto Layout 其實就是對 Cassowary 算法的一種實現,可是這裏並不會對它展開介紹,有興趣的讀者能夠在文章最後的 Reference 中瞭解一下 Cassowary 算法相關的文章。
Auto Layout 的原理就是對線性方程組或者不等式的求解。
在使用 Auto Layout 進行佈局時,能夠指定一系列的約束,好比視圖的高度、寬度等等。而每個約束其實都是一個簡單的線性等式或不等式,整個界面上的全部約束在一塊兒就明確地(沒有衝突)定義了整個系統的佈局。
在涉及衝突發生時,Auto Layout 會嘗試 break 一些優先級低的約束,儘可能知足最多而且優先級最高的約束。
由於佈局系統在最後仍然須要經過 frame
來進行,因此 Auto Layout 雖然爲開發者在描述佈局時帶來了一些好處,不過它相比原有的佈局系統加入了從約束計算 frame
的過程,而在這裏,咱們須要瞭解 Auto Layout 的佈局性能如何。
由於使用 Cassowary 算法解決約束問題就是對線性等式或不等式求解,因此其時間複雜度就是多項式時間的,不難推測出,在處理極其複雜的 UI 界面時,會形成性能上的巨大損失。
在這裏咱們會對 Auto Layout 的性能進行測試,爲了更明顯的展現 Auto Layout 的性能,咱們經過 frame
的性能創建一條基準線以消除對象的建立和銷燬、視圖的渲染、視圖層級的改變帶來的影響。
你能夠在 這裏 找到此次對 Layout 性能測量使用的代碼。
代碼分別使用 Auto Layout 和 frame
對 N 個視圖進行佈局,測算其運行時間。
使用 AutoLayout 時,每一個視圖會隨機選擇兩個視圖對它的 top
和 left
進行約束,隨機生成一個數字做爲 offset
;同時,還會用幾個優先級高的約束保證視圖的佈局不會超出整個 keyWindow
。
而下圖就是對 100~1000 個視圖佈局所須要的時間的折線圖。
這裏的數據是在 OS X EL Captain,Macbook Air (13-inch Mid 2013)上的 iPhone 6s Plus 模擬器上採集的, Xcode 版本爲 7.3.1。在其餘設備上可能不會得到一致的信息,因爲筆者的 iPhone 升級到了 iOS 10,因此沒有辦法真機測試,最後的結果可能會有必定的誤差。
從圖中能夠看到,使用 Auto Layout 進行佈局的時間會是隻使用 frame
的 16 倍左右,雖然這裏的測試結果可能受外界條件影響差別比較大,不過 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 這個限制,那麼在界面上出現了多少個視圖時,我才須要考慮其它的佈局方式呢?在這裏,咱們將須要佈局的視圖數量減小一個量級,從新繪製一個圖表:
從圖中能夠看出,當對 30 個左右視圖使用 Auto Layout 進行佈局時,所須要的時間就會在 16.67 ms 左右,固然這裏不排除一些其它因素的影響;到目前爲止,會得出一個大體的結論,使用 Auto Layout 對複雜的 UI 界面進行佈局時(大於 30 個視圖)就會對性能有嚴重的影響(同時與設備有關,文章中不會考慮設備性能的差別性)。
上述對 Auto Layout 的使用仍是比較簡單的,而在平常使用中,使用嵌套的視圖層級又很是正常。
在筆者對嵌套視圖層級中使用 Auto Layout 進行佈局時,當視圖的數量超過了 500 時,模擬器直接就 crash 了,因此這裏沒有超過 500 個視圖的數據。
咱們對嵌套視圖數量在 100~500 之間佈局時間進行測量,並與 Auto Layout 進行比較:
在視圖數量大於 200 以後,隨着視圖數量的增長,使用 Auto Layout 對嵌套視圖進行佈局的時間相比非嵌套的佈局成倍增加。
雖說 Auto Layout 爲開發者在多尺寸佈局上提供了遍歷,並且支持跨越視圖層級的約束,可是因爲其實現原理致使其時間複雜度爲多項式時間,其性能損耗是僅使用 frame
的十幾倍,因此在處理龐大的 UI 界面時表現差強人意。
在三年之前,有一篇關於 Auto Layout 性能分析的文章,能夠點擊這裏瞭解這篇文章的內容 Auto Layout Performance on iOS。
Auto Layout 不止在複雜 UI 界面佈局的表現不佳,它還會強制視圖在主線程上佈局;因此在 ASDK 中提供了另外一種能夠在後臺線程中運行的佈局引擎,它的結構大體是這樣的:
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; }
子類能夠在這個方法中進行計算,經過覆寫這個方法返回一個合適的大小,不過通常狀況下都不會使用手動佈局的方式。
在 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++ 方法 ASStackUnpositionedLayout
、ASStackPositionedLayout
以及 ASStackBaselinePositionedLayout
的 compute
方法,這些方法完成了對 ASStackLayoutSpec
中視圖的佈局。
相比於 Auto Layout,ASDK 實現了一種徹底不一樣的佈局方式;比較相似與前端開發中的 Flexbox
模型,而 ASDK 其實就實現了 Flexbox
的一個子集。
在 ASDK 1.0 時代,不少開發者都表示但願 ASDK 中加入 ComponentKit 的佈局引擎;而如今,ASDK 佈局引擎的大部分代碼都是從 ComponentKit 中移植過來的(ComponentKit 是另外一個 Facebook 團隊開發的用於佈局的框架)。
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
的做用更像是一個抽象類,在真正使用 ASDK 的佈局引擎時,都不會直接使用這個類,而是會用相似 ASStackLayoutSpec
、ASRelativeLayoutSpec
、ASOverlayLayoutSpec
以及 ASRatioLayoutSpec
等子類。
筆者不打算一行一行代碼深刻講解其內容,簡單介紹一下最重要的 ASStackLayoutSpec
。
ASStackLayoutSpec
從 Flexbox
中得到了很是多的靈感,好比說 justifyContent
、alignItems
等屬性,它和蘋果的 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
的佈局計算時間進行了測試,不包括視圖的渲染以及其它時間:
測試結果代表 ASStackLayoutSpec
花費的佈局時間與結點的數量成正比,哪怕計算 100 個視圖的佈局也只須要 8.89 ms,雖然這裏沒有包括視圖的渲染時間,不過與 Auto Layout 相比性能仍是有比較大的提高。
其實 ASDK 的佈局引擎大部分都是對 ComponentKit 的封裝,不過因爲擺脫了 Auto Layout 這一套低效可是通用的佈局方式,ASDK 的佈局計算不只在後臺併發線程中進行、並且經過引入 Flexbox
提高了佈局的性能,可是 ASDK 的使用相對比較複雜,若是隻想對佈局性能進行優化,更推薦單獨使用 ComponentKit 框架。
The Cassowary Linear Arithmetic Constraint Solving Algorithm: Interface and Implementation
The Cassowary Linear Arithmetic Constraint Solving Algorithm
Solving Linear Arithmetic Constraints for User Interface Applications
Github Repo:iOS-Source-Code-Analyze
Follow: Draveness · GitHub
Source: http://draveness.me/layout-pe...