AsyncDisplayKit介紹(二)佈局系統

即刻技術團隊發表於2017-04-14

在上一篇介紹中我們曾經討論過Autolayout的效能問題。然而在iOS中,除了Autolayout,能選擇的只有autoresizingMask,或者純手動佈局。在寫了無數view.frame = CGRect(…)之後,我們才發現,一個在HTML中非常簡單的流式佈局,到iOS9才有相應的UIStackView予以支援。儘管你可以使用 FDStackView 將系統要求降低到iOS6,對於複雜的佈局仍然需要多層View巢狀來實現——其實對於實現stack佈局的時候,並不需要一個UIView例項來承載所有的元素,只需要它的佈局功能而已。

做過Web開發的朋友已經習慣了高效的css佈局:宣告式、易於除錯,boxModel結構化清晰等等優點,特別是使用自動化工具之後,只需要儲存檔案就可以立即在瀏覽器中看到更新後的效果;而這一切在iOS中就變得遙不可及:命令式賦值、編譯後(Objc速度尚可,而swift編譯速度較慢)才能看到結果、Autolayout閉源、檢視除錯複雜、xcode經常crash等等。如果要實現一個高效能的tableView,手寫佈局幾乎是唯一選擇。

Layout的本質

在決定使用哪種佈局方式之前,先看看設計師是怎麼思考佈局的:

  1. 每個元素的大小(size)
  2. 兄弟元素的對齊方式:上下左右對齊、居中對齊(alignment)和間距(spacing);父子元素之間的容納關係:子元素的邊距(insets)

設計師最終確定的,是一個佈局規則。

設計稿到了工程師這邊以後,絕大多數時間都在做以下兩件事:

  1. 確定UIView自身的大小(覆蓋sizeThatFits方法),與parent view無關
  2. 根據規則,將subview放在父parent內的指定位置(對應layoutSubviews),由parent view決定

這樣層層遞迴,最終完成整個螢幕內所有子元素的佈局。工程師完成的,是一個基於佈局規則的具體實現。

也就是說,手動佈局或者通過約束(Autolayout)其實是在『實現』佈局規則,而不是『宣告』佈局規則。抽象層級下降了,隨之而來的是多到令人頭疼的frame、size、origin計算,可讀性、可維護性都非常差。那能不能用一個資料結構來表示佈局規則,提高抽象層級,從而擺脫繁重又容易出錯的數值計算,讓佈局系統根據規則自動地、非同步地計算呢?

AsyncDisplayKit的三種佈局方式

1. 手動佈局

這是最基礎的一種佈局方式。在ASDK中的手動佈局與UIKit類似,只是方法名從sizeThatFits變成了calculatedSizeThatFits,layoutSubviews方法變成了layout方法。略微不同的是ASDK會將佈局結果非同步預先計算,並快取下來以提升效能,這點對於tableView滾動效能來說有非常大的幫助。

然而,跟普通手動佈局類似,最大的缺點是可讀性和可維護性差。由於自身的大小常常和子元素的佈局相關聯,這兩個方法中的程式碼容易重複;同時由於佈局針對自身subView,很難將其程式碼與其他View進行復用。

2. Unified layout

這也是在ASDK中新出現的概念。先介紹一下ASLayout:

AsyncDisplayKit介紹(二)佈局系統

一個ASLayout物件包含以下元素:

  • 它所代表的佈局元素
  • 元素的尺寸
  • 元素的位置
  • 它所包含的sublayouts

可以看出,當一個node具備了確定的ASLayout物件時,它自身的佈局也就隨之確定了。為了生成ASLayout,ASDisplayNode為subclass提供瞭如下覆蓋點:

- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize複製程式碼

只要Node能夠計算出自己的ASLayout,父元素就可以完成對其的佈局。這種方法將sizeThatFits和layoutSubviews結合在一起,一定程度上避免了相似程式碼的尷尬,但是計算上仍然是手動佈局,不夠簡便。

3. Automatic Layout(不是Autolayout)

有了ASLayout這一層抽象,再來看如何宣告規則來生成它。這也是最艱鉅的部分。

Facebook將React的概念延伸到native,同時也把宣告式的語法帶到了iOS,產生了ComponentKit框架。早在ASDK1.0的年代大家紛紛表示希望能將其與ComponentKit相融合,幸運的是,在2.0版本中實現了。我選用了作者Scott
Goodson在NSSpain的演講pdf作為插圖,方便大家瞭解新的佈局系統。

因此Automatic Layout是ASDK最推薦也是最為高效的佈局方式。它引入了ASLayoutSpec的概念,可以理解為一個抽象的容器(並不需要建立對應node或者view來裝載子元素),只需要為它制定一系列的佈局規則,它就能對其負責的子元素進行佈局。

我們先來看一下它們之間的關係:

AsyncDisplayKit介紹(二)佈局系統

可以看到ASLayoutSpec和ASDisplayNode都實現了ASLayoutable介面,因此他們都具備生成ASLayout的能力,這樣就能唯一確定自身的大小。

  • 對於以上提到的Unified佈局,當我們實現了calculateLayoutThatFits,ASDK會在佈局過程中呼叫measureWithSizeRange(如果沒有快取過再呼叫calculateLayoutThatFits)來算出ASLayout。
  • 如果ASDisplayNode選擇實現layoutSpecThatFits,由更為抽象的ASLayoutSpec來負責指定佈局規則,由於ASLayoutSpec也實現了ASLayoutable介面,同樣也可以通過呼叫measureWithSizeRange來獲得ASLayout。

由於ASLayoutSpec只負責指定佈局規則,而不關心其佈局的ASLayoutable具體是Node還是其他ASLayoutSpec,我們可以輕鬆地將ASLayoutSpec生成邏輯獨立出來,達到複用的目的。

挑大樑的ASStackLayoutSpec

ASLayoutSpec主要有以下幾種:

AsyncDisplayKit介紹(二)佈局系統

通過它們的命名就可以瞭解其大致作用。其中用途最廣泛的無疑是ASStackLayoutSpec,與UIStackView有異曲同工之妙。

ASStackLayoutSpec大量借鑑了CSS的FlexBox概念,寫過Web程式碼的同學應該能一眼認出許多熟悉的屬性:justifyContent/flexDirection/alignSelf/alignItems/flexBasis等等。不熟悉的朋友也可以通過一個有趣的遊戲來學習flexbox:flexboxfroggy.com/

示例

(圖中的Huy Nguyen也是ASDK佈局的主要貢獻者之一)

AsyncDisplayKit介紹(二)佈局系統

圖中左邊是一個imageNode,右邊是由兩個textNode組成的一個vertical
stack,再和imageNode組合成一個橫向的stack,最後加上邊緣inset完成整個佈局。

事實上,與css類似,絕大多數的佈局都可以通過stack和inset的組合來完成;而和UIStackView不同的是,layout spec只是一個存在於記憶體之中的資料結構,並不需要額外建立view容器來承載子元素,大大節約了複雜佈局帶來的開銷;同時因為它的輕量級和獨立性,因此能夠將佈局規則放到後臺執行緒獨立計算,並快取在node之中,對於大量tableView/collectionView的CellNode快速佈局有相當大的幫助。

Huy Nguyen也為ASLayoutSpec寫了一個寓教於樂的小遊戲,只是將上面提到的的針對css的小遊戲版本改成了Objc,方便大家快速學習。

在最近的版本中,ASDK也同時支援 Yoga (一個跨平臺的flexbox引擎,使用C語言實現)來計算基於flexbox的佈局,與ASDK本身實現的flexbox概念類似。

即刻的實踐

即刻在多個訊息頁面使用了ASDK新的佈局系統,例如:

AsyncDisplayKit介紹(二)佈局系統

從我們的實踐經驗來看有以下優缺點:

優點

  • 實現一個layout的視角從專注view之間的constraint轉變成對定義多個view子區域的劃分,抽象層級變高,可讀性大大增強。
  • 由於ASDisplayNode的顯示是非同步的,因此無論佈局是否依賴於顯示結果,都可以在主執行緒以外進行,並且有快取,效能有很大提升。
  • 借用css成熟的flexbox佈局模型,有大量現成資料和案例來學習,避免了新造輪子的尷尬。
  • 由於ASDK是開源的,除錯難度大大降低。
  • 佈局規則可以脫離實際佈局物件進行獨立宣告,容易複用。

缺點

  • 由於與iOS原生布局方式完全不同,學習適應需要一定時間(當然如果flexBox漸漸成為前端標準,花一些時間瞭解也是完全值得的。)
  • 對於ASDK依賴非常重,對於需要使用flexBox佈局的view,只能重新使用ASDisplayNode來實現。如果不方便重新寫,只能選用類似Yoga的獨立框架來實現UIView的流式佈局。
  • 雖然是宣告式佈局,然而相對css而言仍較為繁瑣。去年我曾經到Pinterest與Scott有過一些當面交流,也嘗試說服他們做一些DSL來簡化佈局宣告,但是目前由於ASDK的複雜性和基礎性(類似UIKit),他們仍然將大部分時間放在優化非同步渲染和佈局效能,並沒有太多精力在layout的語法上做出進一步突破。

雖然我們沒有在整個專案都採用新的的佈局方式,然而ASDK帶來的啟發是深遠的:宣告式、非同步佈局、flexBox、快取等等。我們也看到有一些其他佈局庫在宣告式佈局做許多的嘗試,如Brickkit, Layoutkit等等,或許這就會是將來佈局系統發展的趨勢,大大加快我們平時的開發和維護效率。

相關文章