SnapKit 是怎樣煉成的 | 掘金技術徵文

KyXu發表於2016-12-29

前言

這是對 Swift 佈局框架 SnapKit 的原始碼的一點分析,嘗試搞清,一個好的佈局框架,背後都做了些什麼。

介紹 SnapKit 中的一些類

ConstraintView
等同於 UIView

ConstraintAttributes
用於構造約束關係的各種元素(上下左右等)

ConstraintDescription
包含了包括 ConstraintAttributes 在內的各種與約束有關的元素,一個 ConstraintDescription 例項,就可以提供與一種約束有關的所有內容。

ConstraintMaker
構造約束關係的起點,提供了 makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) 方法來為程式設計師提供了描述約束的空間,也可以通過 left right top bottom centerX centerY 等屬性,去生成一個 ConstraintMakerExtendable 例項(見下面)

ConstraintMakerExtendable(繼承 ConstraintMakerRelatable)
提供 left right top bottom leading trailing edges size margins 等內容,用以產生一個 ConstraintMakerRelatable 型別的例項

ConstraintMakerRelatable
直接用於構造約束關係,也是常用方法 equalTo(_ other: ConstraintRelatableTarget) -> ConstraintMakerEditableequalToSuperview 的來源。核心方法是 relatedTo(_ other: ConstraintRelatableTarget, relation: ConstraintRelation, file: String, line: UInt) -> ConstraintMakerEditable,返回 ConstraintMakerEditable 型別的例項

ConstraintMakerEditable(繼承 ConstraintMakerPriortizable)
在設定約束的寬度、高度以及偏移的時候,提供相應的加減乘除方法,返回 ConstraintMakerPriortizable 型別的例項

ConstraintMakerPriortizable(繼承 ConstraintMakerFinalizable)
提供方法來設定約束的 priority,返回 ConstraintMakerFinalizable 型別的例項

ConstraintMakerFinalizable
一個只有一個型別為 ConstraintDescription 的屬性的類,正如它的類名,有一個 ConstraintMakerFinalizable 例項,就得到了對於一個約束的完整描述。


至此,我們已經知道 SnapKit 是靠什麼來確定了三個東西:

  1. 誰在做約束(ConstraintView)
  2. 怎麼做約束(ConstraintMaker)
  3. 約束是什麼(ConstraintDescription)
let aView = UIView()
aView.snp.makeConstraints({ make in
    make.width.equalToSuperview().dividedBy(2).priority(100)
})複製程式碼

當我們寫下這樣的語句時,先忽略掉 snp 是什麼不管,裡面設定 aView 的寬度為它的父檢視的一半的這行約束語句,執行了這樣的邏輯:

  1. ConstraintMaker 提供 makeConstraints 方法來讓我們寫約束的同時,開始維護了一個 ConstraintDescription 陣列,叫 descriptions
  2. make 本身是 ConstraintMaker 型別的
  3. 在我們寫下 .width 時,descriptions 陣列第一次加入內容(self.description),同時我們用這個內容生成了一個 ConstraintMakerRelatable 例項
  4. 在我們寫下 .equalToSuperview() 時,上一步中的內容(self.description)繼續新增資訊,同時我們用它生成了一個 ConstraintMakerEditable 例項
  5. 之後的 .dividedBy(2).priority(100) 使得之前的 ConstraintMakerEditable 例項變成了一個 ConstraintMakerFinalizable 例項,這個例項的 description 屬性的型別是 ConstraintDescription,它包含了我們所描述的全部內容。但由於 ConstraintMakerEditable 本身就繼承自 ConstraintMakerFinalizable,所以 .dividedBy(2).priority(100) 這一部分即便不寫,這條語句在語法上也已經完成。
    SnapKit 是怎樣煉成的 | 掘金技術徵文

做個總結:到這裡我們發現 ConstraintMaker 以及和它相關的類,構造了一套 DSL 來讓我們可以輕鬆地寫出約束語句,而這些語句把資訊都放到了一個 ConstraintDescription 例項(self.description)裡面,但我們仍然不知道它是如何以 UIKit 裡面的 NSLayoutConstraint 的形式作用的。

snp 是什麼

SnapKit 裡面存在這樣一些東西:
public protocol ConstraintDSL {}
public protocol ConstraintBasicAttributesDSL : ConstraintDSL {}
public protocol ConstraintAttributesDSL : ConstraintBasicAttributesDSL {}
public struct ConstraintViewDSL: ConstraintAttributesDSL {}

上面我們知道了 aView 作為一個 UIView,它同時也就是一個 ConstraintView,ConstraintView 有一個 snp 的屬性,這給我們提供了入口來通過 SnapKit 給任意的 UIView 或 AppKit 裡面的 NSView 通過 .snp 這樣的語法來寫約束。

這個 snp 屬性的型別就是結構體 ConstraintViewDSL

一看就是面向協議的寫法,通過一個個的 extension 來給 protocol 新增功能,最後用 struct 實現出來,就有了 snp 這個屬性。

let topView = UIView()
let centerView = UIView()
centerView.snp.makeConstraints({ make in
    make.top.equalTo(topView.snp.bottom).offset(16)
})複製程式碼

這段程式碼展現了 snp 的兩個作用:

  1. snp 有 left top right bottom edges size 等一大堆屬性,這些屬性的型別是 ConstraintItem,這是用於構造約束位置關係的
    SnapKit 是怎樣煉成的 | 掘金技術徵文
  2. snp 作為 ConstraintViewDSL,有 prepareConstraints makeConstraints remakeConstraints updateConstraints removeConstraints 等函式,我們最常用的是 makeConstraints ,傳入一個 closure,在裡面寫約束關係。這裡要注意,我們使用的 makeConstraints 方法來源於 ConstraintViewDSL,但真正實現了構造約束的其實是我們上文裡面寫的 ConstraintMaker 裡面的 makeConstraints 方法,見圖:
    SnapKit 是怎樣煉成的 | 掘金技術徵文

約束是如何作用的

到現在我們還是沒說,從 snp 到 ConstraintMaker,再到 ConstraintMakerFinalizable 的 description 屬性,到底哪裡建立了 NSLayoutConstraint,答案其實在之前提過多次的 ConstraintMaker 裡面

// public class ConstraintMaker

internal static func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
    let maker = ConstraintMaker(item: item)
    closure(maker)
    var constraints: [Constraint] = []
    for description in maker.descriptions {
        guard let constraint = description.constraint else {
            continue
        }
        constraints.append(constraint)
    }
    for constraint in constraints {
        constraint.activateIfNeeded(updatingExisting: false)
    }
}

internal static func updateConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
    guard item.constraints.count > 0 else {
        self.makeConstraints(item: item, closure: closure)
        return
    }

    let maker = ConstraintMaker(item: item)
    closure(maker)
    var constraints: [Constraint] = []
    for description in maker.descriptions {
        guard let constraint = description.constraint else {
            continue
        }
        constraints.append(constraint)
    }
    for constraint in constraints {
        constraint.activateIfNeeded(updatingExisting: true)
    }
}複製程式碼

我們傳入一個閉包來寫約束關係時,這個閉包給叫做 maker 的 ConstraintMaker 例項寫入了資訊,遍歷 maker 的 descriptions 之後(我們之前說一條約束語句最終得到一個 self.description,但往往會有多條約束,所以 ConstraintMakerFinalizable 裡面的 self.description,在 ConstraintMaker 裡被一個陣列維護),我們得到了 Constraint 陣列。

SnapKit 是怎樣煉成的 | 掘金技術徵文

Constraint 這個類還沒有介紹過,不過上面這個核心方法加上以前的內容,已經可以讓我們猜出來,約束是怎麼寫出來的了:
SnapKit 是怎樣煉成的 | 掘金技術徵文

其他內容補充 1

SnapKit 是怎樣煉成的 | 掘金技術徵文

隨便寫了兩句,展示一下各個方法傳入的引數的型別,發現有各種 Target,貌似很複雜,不過點開之後發現是這種景象:
SnapKit 是怎樣煉成的 | 掘金技術徵文

說白了就是因為 equalTo: 這個方法裡面能傳的引數型別比較多,手動來一個一個限制一下,我們看到 ConstraintRelatableTarget 這裡可以放一些原生的可以代表數字的型別,外加四個自定義的 Constraint 型別。其他的 Target 協議也差不多是這種情況。

個人覺得這種做法還是挺值得學習的。

其他內容補充 2

SnapKit 裡面用來表示位置主體的類其實不是 ConstraintView,而是 ConstraintItem
我們管這個“主體”叫 target,一個 target,再加上一個 ConstraintAttributes 例項,就可以組成一個 ConstraintItem。

SnapKit 是怎樣煉成的 | 掘金技術徵文

有 attributes 屬性很好理解,因為比如我們去做對齊,可以是 aView 的 top 和 bView 的 bottom 對齊,而不能是 aView 和 bView 對齊。但是為什麼 target 的型別是 AnyObject 而不是 ConstraintView,即 UIView 或 NSView 呢?

在 ConstraintViewDSL 裡面,target 確實是 ConstraintView 型別,
但在 ConstraintLayoutSupportDSL 裡面,target 是 ConstraintLayoutSupport 型別,
在 ConstraintLayoutGuideDSL 裡面,target 是 ConstraintLayoutGuide 型別

這部分就不具體解釋了,想一探究竟的去看 LayoutConstraintItem.swift 這個檔案吧。

掘金技術徵文:gold.xitu.io/post/58522d…

相關文章