讀 SnapKit和Masonry 自動布局框架源碼
前言
一直覺得 SnapKit 和 Masonry 這兩個框架設計和封裝的很好,用起來的體驗也是一致的,翻了下它們的源碼,對其設計方式和涉及的技術做了下記錄。文章打算圍繞,給誰做約束?如何設置約束?設置完後如何處理?這三個問題看看 SnapKit 和 Masnory 分別是怎麼做的,正好也能夠窺探下作者是如何利用 Swift 和 Objective-C 兩個不同語言的不同特性做到一致的使用體驗的。
如果還不了解這兩個框架的使用的話可以參看它們項目 GitHub 說明:GitHub - SnapKit/SnapKit: A Swift Autolayout DSL for iOS & OS X,GitHub - SnapKit/Masonry: Harness the power of AutoLayout NSLayoutConstraints with a simplified, chainable and expressive syntax. Supports iOS and OSX Auto Layout
如果還不了解自動布局或者還沒有用過的同學可以參看我三年前這篇文章,裡面有詳細的介紹和相關資料:深入剖析Auto Layout,分析iOS各版本新增特性 | 星光社 - 戴銘的博客
進入那三個問題之前我們先看看兩個框架的整體結構圖,對它們有個大概的印象。
SnapKit 源碼結構圖
Masonry 源碼結構圖
接下來我們來詳細看看兩個框架的內部,首先來看看剛才那三個問題中的第一個問題。
給誰做約束?
SnapKit
ConstraintView
這個 View 實際上在 iOS 里就是 UIView,在 macOS 上就是 NSView。
對 ConstraintView 做擴展,裡面定義里一個屬性 snp
這個 snp 屬性的類型就是結構體 ConstraintViewDSL。下面來看看 ConstraintViewDSL 這個結構體做什麼用的。
ConstraintViewDSL
這個結構體會在初始化時通過 view 屬性持有 ConstraintView。
同時還提供了那些我們必調用的 makeConstraints,contentHuggingHorizontalPriority 等等函數。這樣我們就可以在 UIView 中直接調用這些函數來進行視圖的約束設置了。
ConstraintViewDSL 是繼承自 ConstraintAttributesDSL。
ConstraintAttributesDSL
ConstraintAttributesDSL 是個協議,繼承於 ConstraintBasicAttributesDSL 這個協議,為什麼要多這一層呢,因為 ConstraintAttributesDSL 這個裡面定了 iOS 8 系統出現的新的屬性,比如 lastBaseline,firstBaseline,leftMargin 等。而 ConstraintBasicAttributesDSL 里定義的是一開始就有的那些屬性比如 left,top,centerX,size 等。
Masonry
接下來我們看看 Masonry 是給誰做的約束。
View+MASAdditions
View+MASAdditions 就是 Masonry 的一個外部的入口,實質上就是 UIView 的一個 Category 作用就是用來設置 MASViewAttribute 的屬性,並實例化,並且指定當前的 UIView 對應的 LayoutAttribute。和 SnapKit 一樣, Masonry 也對 iOS 和 macOS 做了兼容,在 macOS 里就是 NSView,相關代碼在 MASUtilities.h 文件里,這裡除了平台相關代碼外,還有些宏的定義和靜態方法。這裡我們可以看看靜態方法 static inline id _MASBoxValue(const char *type, …) 的作用:
看這段代碼是不是就能猜出來是做什麼的了,對,它就是我們經常使用的 mas_equalTo 這個方法,這裡可以看到它是如何支持變參和如何將 float,double,int 這樣的值類型數據轉換成和 equalTo 一樣的對象 NSNumber 數據的。這個寫法靈感來自GitHub - specta/expecta: A Matcher Framework for Objective-C/Cocoa。 mas_equalTo 和 equalTo 都是宏定義的。
MASBoxValue 這個宏定義就是上面的 _MASBoxValue 這個方法。細心同學會發現這兩個 equal 的宏對應的方法是不同的,一個是 equalTo(MASBoxValue((VA_ARGS))) 另一個是 mas_equalTo(VA_ARGS) 但是這兩個方法的實現是一樣的。
這樣寫就是避免宏定義衝突的一種方式。
這個 Category 還有那些我們總是調用的 mas_makeConstraints,mas_updateConstraints,mas_remakeConstraint 等方法。
mas_makeConstraints 的 block 參數會將創建的 MASConstraintMaker 這個工廠類對象暴露出去,讓我們去設置這個類對象中的 MASConstraint 屬性,然後通過該對象的 install 方法將當前視圖中所有添加的約束添加到一個數組裡。該數組裡存儲是 MASViewConstraint 對象,對應的就是 NSLayoutConstraint。具體代碼如下:
這種設計模式和 SnapKit 的一樣使用了閉包來獲取用戶設置的數據,在設計模式里叫做好萊塢原則。
mas_updateConstraints 和 mas_makeConstraints 差不多,不過裡面多了一行:
這樣當添加約束時會通過這個屬性是否為真來檢查約束是否 intall 了,是的話就更新,沒有就添加。
mas_remakeConstraints 的話是添加了這一句:
設置為 YES 後會將以前設置的約束 uninstall 掉,後面再把新設置的約束添加上。
最後還有個方法 mas_closestCommonSuperview,這個方法一般我們都不會主動調用,所以很多人應該都太熟悉,不過這個斷言報錯大家應該會有很深刻的印象 couldn』t find a common superview for … 。所以這個方法如其名就是去找共同的父視圖,還是最近的。框架內部也就在 MASViewConstraint 的 install 方法里用了一次。
這個查找是 N 方的,誰有辦法能夠優化下么。
如何設置約束?
SnapKit
先看看這張圖,裡面是我們使用框架時用的最多的設置 make 的過程,圖裡將每個操作對應的不同 ConstraintMaker 做了說明。
下面來對這幾種 ConstraintMaker 來詳細說下。
ConstraintMaker
這個是設置的入口,makeConstraints 函數一個閉包參數可以提供外部去設置ConstraintMaker 自己的 left,right,top 等屬性來描述約束。這些屬性的 getter 方法會返回 ConstraintMakerExtendable 實例。
先看看 ConstraintMaker 的構造函數:
LayoutConstraintItem 會通過給擴展 ConstraintLayoutGuide 和 ConstraintView 來達到約束 item 類型的作用。下面看看 prepare 這個函數的作用。
看,禁用 AutoresizeMask 是在這裡統一處理了。
接下來看看閉包參數設置屬性的 getter 方法。
ConstraintMaker 包含了一個 ConstraintDescription 數組,裡面會記錄用戶設置的各個屬性,然後返回 ConstraintMakerExtendable。
OptionSet
這裡的 ConstraintAttributes 是個 OptionSet,ConstraintAttributes 結構體來遵從 OptionSet 選項集合協議,為什麼不用枚舉呢?因為在一次只有一個選項被選中是枚舉是 OK 的。但是在 Swift 里的枚舉是沒法將多個枚舉選項組成一個值的,比如 ConstraintAttributes 里的 edges,size 和 center 等就是組合而成的。而 OptionSet 結構體使用了高效的位域來表示的。還有,OptionSet 繼承於 ExpressibleByArrayLiteral,這樣還能夠使用數組字面量來生成選項的集合。下面看看這個 ConstraintAttributes 是如何定義的。
可以看到組合的 size 就是 width(64) + height(128)= size(192)。
重載和自定義操作符
ConstraintAttributes 重載了 +,+=,-= 和 == 這些操作符。我們先看看代碼
這種重載很適合對自定義的結構體進行一些熟悉的簡化符號操作。
如果希望自定義一些操作符的話就需要先聲明下,讓編譯器知道這是個操作符,比如我們自定義一個操作符?
這裡的 infix 是中間運算符的意思,還有前置運算符 prefix 和後置運算符 postfix。自定義運算符之能是類似,/,=,-,+,*,%,,!,&,|,^,。,~ 等這樣的符號組成,也能支持一些特殊的字元比如剛才的用的?,還有?,? 這樣的特殊符號。
自定義運算符還能夠指定優先順序分組 precedencegroup,如下:
下面列下常用類型對應的group
完整的操作符的定義和 precedencegroup 之間的優先順序關係在 Swift 源碼的 swift/stdlib/public/core/Policy.swift 文件里,在線看地址是:https://github.com/apple/swift/blob/a7ff0da33488b9050cf83df95f46e5b9aa2348d5/stdlib/public/core/Policy.swift 。那些操作符優先順序高些或者低些在這個文件里是一目了然。
ConstraintMakerExtendable
ConstraintMakerExtendable 繼承 ConstraintMakerRelatable,它可以實現鏈式的多屬性,有left,right,top 等等這樣的屬性,用以產生一個 ConstraintMakerRelatable 類型的實例。
我們看看 left 屬性的 getter 定義:
這裡可以看到通過重載的操作符 += 能夠將 .left 加到 ConstraintAttributes 里。
ConstraintMakerRelatable
用於指定約束關係比如常用的 equalTo。equalTo 函數裡面是調用的 relatedTo 函數,返回 ConstraintMakerEditable 類型的實例。
這裡的 ConstraintRelatableTarget 是約束,equalTo 這個方法裡面能傳的參數類型比較多,可以通過這個協議來擴展下只支持的類型,達到限制類型的功能。ConstraintPriorityTarget,ConstraintInsetTarget,ConstraintOffsetTarget 和 ConstraintInsetTarget 也都有類似的作用,不過這幾個還有個作用就是將 Float,Double,Int 和 UInt 這幾種類型都轉成 CGFloat。我們拿 ConstraintInsetTarget 來看看實現如下:
ConstraintMakerEditable
ConstraintMakerEditable 繼承 ConstraintMakerPriortizable,主要是設置約束的 offset 和 inset 還有 multipliedBy 和 dividedBy 函數。
ConstraintMakerPriortizable
ConstraintMakerPriortizable 繼承 ConstraintMakerFinalizable,用來設置優先順序,返回 ConstraintMakerFinalizable 類型的實例。
ConstraintMakerFinalizable
裡面類型為 ConstraintDescription 的屬性的類是一個完整的約束描述,有了這個描述就可以做後面的處理了。裡面的內容是完整的,這個類是一個描述類, 用於描述一條具體的約束, 包含了包括 ConstraintAttributes 在內的各種與約束有關的元素,一個 ConstraintDescription 實例,就可以提供與一種約束有關的所有內容。可以看到前面設置的屬性,關係,乘除係數,優先順序等因有盡有,如下:
Masonry
在 Masonry 也有對應的 ConstraintMaker。
MASConstraintMaker
MASConstraintMaker 是創建 MASConstraint 對象的。裡面有個 constraints 數組專門用來存儲創建的這些對象。前面 mas_makeConstraints 的那個 Block 暴露出的就是 MASConstraintMaker 對象。
接下來看看 MASConstraint 屬性的 getter 方法:
會發現這些 getter 方法都會調用 addConstraintWithLayoutAttribute 這個方法。
這裡會發現每次 getter 都會創建一個新的 MASViewConstraint 對象,這裡通過將新的 MASViewConstraint 對象的 delegate 設置成自己的方式讓新對象也能夠調用相同的方法創建一個新的 MASViewConstraint 對象,使得能夠支持進行鏈式的調用。
設置完後如何處理?
SnapKit
下面通過 makeConstraints 我們來看看 ConstraintMaker 是如何在外部通過一個閉包來寫約束關係的。
這個閉包給叫做 maker 的 ConstraintMaker 實例寫入了信息,遍歷 maker 的 descriptions 之後(我們之前說一條約束語句最終得到一個 self.description,但往往會有多條約束,所以 ConstraintMakerFinalizable 裡面的 self.description,在 ConstraintMaker 里被一個數組維護),我們得到了 Constraint 數組。
跟進 Constraint 里的 activateIfNeeded 這個函數看看約束是怎麼寫出來的了
Masonry
MASViewConstraint
這個類是對 NSLayoutConstriant 的封裝。它的父類是 MASConstraint,MASConstraint 是一個抽象不可實例的類,裡面有介面和協議。它的兄弟類是 MASCompositeConstraint,裡面有個數組專門存儲 MASViewConstraint 對象。
MASViewConstraint 對象的 install 方法會將各個約束 install 到對應的視圖上。我們看看 MASConstraintMaker 的 install 方法:
這個方法會遍歷 constraints 里每個約束進行 install。在這個 install 方法里會創建 MASLayoutConstraint 對象,然後把這個對象添加到對應的的視圖上。
創建完 MASLayoutConstraint 對象後,會根據約束的設置判斷將約束添加到哪個視圖上。
通過上面代碼里的條件判斷可以看出,如果有設置相對的那個視圖就用先前提到的那個 mas_closestCommonSuperview 方法去找兩視圖的共同父視圖,不然如果只設置了高寬,就把約束加到當前視圖上,其它情況就加到當前視圖的父視圖上。
※Swift 項目中涉及到 JSONDecoder,網路請求,泛型協議式編程的一些記錄和想法
TAG:starming |