SnapKit 源碼解讀

SnapKit 是一個使用 Swift 編寫而來的 AutoLayout 框架, 通過使用 Snapkit, 我們可以通過簡短的代碼完成布局
例如, 我們要一個 label 居中展示

snplabel.snp.makeConstraints { (make) in
    make.center.equalTo(self.view.snp.center)
}

如果不用 SnapKit, 我們需要做

rawlabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint(item: rawlabel, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: rawlabel, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1, constant: 20).isActive = true

看起來很神奇的 SnapKit 是如何實現的?

分析源碼

我們從最開始的 snplabel.snp 開始
你也許猜到了, 這個是通過給 view 添加一個擴展實現的
這個在ConstraintView+Extensions.swift 文件里面, 這個文件里面有很多廢棄的方法, 為了方便查看, 我們先直接去掉這些廢棄的方法, 去掉之后, 就是這樣的

public extension ConstraintView {
     public var snp: ConstraintViewDSL {
        return ConstraintViewDSL(view: self)
    }
}

擴展

你也許注意到, 并不是直接擴展的 UIView, 我們來看看 ConstraintView 的定義

#if os(iOS) || os(tvOS)
    public typealias ConstraintView = UIView
#else
    public typealias ConstraintView = NSView
#endif

可以看到, SnapKit 為了實現多平臺將 ConstraintView 分別定義為 UIView 和 NSView 的別名. 我們這里也為了簡單起見, 不考慮多平臺適配, 我們將 ConstraintView 都替換為 UIView

public extension UIView {
     public var snp: ConstraintViewDSL {
        return ConstraintViewDSL(view: self)
    }
}

可以看到, snp 最后是生成了一個 ConstraintViewDSL 對象

ConstraintViewDSL

ConstraintViewDSL 類的構造函數很簡單, 就是將 view 保存起來

internal init(view: UIView) {
    self.view = view
}

而makeConstraints 函數也是定義如下, 這里看到, 這里只是將傳進來的閉包傳遞給ConstraintMaker 這個類去處理了

public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
    ConstraintMaker.makeConstraints(item: self.view, closure: closure)
}

ConstraintMaker

ConstraintMaker.makeConstraints 的實現如下所示

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)
    }
}

從這里可以看到一個大致流程, 首先是構造一個 maker, 然后調用閉包, 閉包內部會添加一些約束, 接下來就是獲取這些約束, 最后將約束激活.
這個類的構造函數依舊很簡單

internal init(item: LayoutConstraintItem) {
    self.item = item
    self.item.prepare()
}
LayoutConstraintItem

這里出現了一個新的類型 LayoutConstraintItem, 表示一個可布局的對象, 通過查看定義, 可以看到是一個協議, UIView 和 ConstraintLayoutGuide 都實現了這個協議, 內部實現了一些方法, 其中就有這個 prepare

internal func prepare() {
        if let view = self as? UIView {
            view.translatesAutoresizingMaskIntoConstraints = false
        }
    }

這一步其實就是禁用 View 的 AutoresizeMask 轉換.

回到最開始的閉包, 里面我們寫的make.center.equalTo(self.view.snp.center)
通過上下文我們可以猜到, 我們可以通過這個函數生成一些約束對象.
首先我們都知道, 每一個約束, 首先需要添加到一個對象上面, 還需要約束的屬性, 關系(大于, 等于,小于), 如果不是常量類型, 還需要另一個依賴的對象, 以及依賴的屬性, 系數以及一個偏移常量.
這里的 make.center 就是說添加到當前, 并設置約束屬性為 center, equalTo, 則是表示關系為等于, self.view.snp.center, 則表示依賴的對象是 self.view, 依賴的屬性也是 center, 系數及偏移值這里均沒有指定, 表示使用默認值
那 make.center 這個是如何實現的? 通過查找定義, 可以發現實現如下

public var center: ConstraintMakerExtendable {
    return self.makeExtendableWithAttributes(.center)
}

這個只是一個簡便方法, 具體的實現繼續去查看定義

internal func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
    let description = ConstraintDescription(item: self.item, attributes: attributes)
    self.descriptions.append(description)
    return ConstraintMakerExtendable(description)
}

可以看到流程為首先根據約束屬性及需要添加約束的對象生成一個描述, 然后將其添加內部的一個數組, 也就是之前 makeConstraints 中第一個 for 循環鎖遍歷的數組, 最后返回一個 ConstraintMakerExtendable 對象

ConstraintAttributes

首先我們來看看這個屬性center
ConstraintAttributes 本身是一個 OptionSet, 里面定義了許多屬性, 例如 left, right, center

internal struct ConstraintAttributes : OptionSet {
    
    internal private(set) var rawValue: UInt
    internal init(rawValue: UInt) {
        self.rawValue = rawValue
    }
    internal static var left: ConstraintAttributes { return self.init(1) }
    internal static var top: ConstraintAttributes {  return self.init(2) }
    internal static var right: ConstraintAttributes { return self.init(4) }
    ...這里有省略
    internal static var center: ConstraintAttributes { return self.init(768) }

使用 OptionSet 的意義在于, 可以通過組合操作, 同時添加多個屬性, 例如, center 這個屬性就是由 centerX 和 centerY 復合而來.

ConstraintDescription

這個類是一個描述類, 用于描述一條具體的約束, 里面包含了約束的屬性, 關系等

public class ConstraintDescription {
    internal let item: LayoutConstraintItem
    internal var attributes: ConstraintAttributes
    internal var relation: ConstraintRelation? = nil
    internal var sourceLocation: (String, UInt)? = nil
    internal var label: String? = nil
    internal var related: ConstraintItem? = nil
    internal var multiplier: ConstraintMultiplierTarget = 1.0
    internal var constant: ConstraintConstantTarget = 0.0
    internal var priority: ConstraintPriorityTarget = 1000.0
    internal lazy var constraint: Constraint? = ...
    internal init(item: LayoutConstraintItem, attributes: ConstraintAttributes) {
        self.item = item
        self.attributes = attributes
    }

回到ConstraintMaker.makeConstraints 中的第一個 for 循環, 里面就是去獲取 description.constraint 已達到最終構造約束的目的

ConstraintMakerExtendable

makeExtendableWithAttributes 最后返回的時候, 返回的是一個ConstraintMakerExtendable 對象
這個類的主要目的是為了實現鏈式的多屬性, 例如, make.center.equalTo(self.view.snp.center) 這一句可以寫為, make.centerX.centerY.equalTo(self.view.snp.center)

public class ConstraintMakerExtendable: ConstraintMakerRelatable {
    public var left: ConstraintMakerExtendable {
        self.description.attributes += .left
        return self
    }
    ...
}
ConstraintMakerRelatable

另外, ConstraintMakerExtendable 繼承自 ConstraintMakerRelatable, 這個類主要是負責構造一個關系, 例如 equalTo

public func equalTo(_ other: ConstraintRelatableTarget, _ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable {
    return self.relatedTo(other, relation: .equal, file: file, line: line)
}
internal func relatedTo(_ other: ConstraintRelatableTarget, relation: ConstraintRelation, file: String, line: UInt) -> ConstraintMakerEditable {
    let related: ConstraintItem
    let constant: ConstraintConstantTarget
    
    if let other = other as? ConstraintItem {
        guard other.attributes == ConstraintAttributes.none ||
              other.attributes.layoutAttributes.count <= 1 ||
              other.attributes.layoutAttributes == self.description.attributes.layoutAttributes ||
              other.attributes == .edges && self.description.attributes == .margins ||
              other.attributes == .margins && self.description.attributes == .edges else {
            fatalError("Cannot constraint to multiple non identical attributes. (\(file), \(line))");
        }
        
        related = other
        constant = 0.0
    } else if let other = other as? UIView {
        related = ConstraintItem(target: other, attributes: ConstraintAttributes.none)
        constant = 0.0
    } else if let other = other as? ConstraintConstantTarget {
        related = ConstraintItem(target: nil, attributes: ConstraintAttributes.none)
        constant = other
    } else if #available(iOS 9.0, OSX 10.11, *), let other = other as? ConstraintLayoutGuide {
        related = ConstraintItem(target: other, attributes: ConstraintAttributes.none)
        constant = 0.0
    } else {
        fatalError("Invalid constraint. (\(file), \(line))")
    }
    
    let editable = ConstraintMakerEditable(self.description)
    editable.description.sourceLocation = (file, line)
    editable.description.relation = relation
    editable.description.related = related
    editable.description.constant = constant
    return editable
}

equalTo 只是對內部函數relatedTo 的一個簡單調用

ConstraintRelatableTarget

這是一個協議, 表示一個可以被依賴的目標, 我們在手寫 NSLayoutConstraint 的時候, 依賴對象可以為 view, 可以為ConstraintLayoutGuide, 也可以為空, 為空的時候, 表示使用絕對值
ConstraintRelatableTarget 是一個協議, 分別有 Int, Double, CGPoint等字面值, 也有UIView, ConstraintLayoutGuide , 同時, 也有ConstraintItem, 讓我們可以指定依賴的具體值, 我們之前的代碼 make.center.equalTo(self.view.snp.center) 中的self.view.snp.center 就是 ConstraintItem 對象

ConstraintItem

view.snp 返回的是一個 ConstraintViewDSL, ConstraintViewDSL 是繼承自 ConstraintAttributesDSL, 而ConstraintAttributesDSL 則是繼承自 ConstraintBasicAttributesDSLConstraintAttributesDSLConstraintBasicAttributesDSL 中定義了大量的布局屬性, 如 top, bottom 等

public var center: ConstraintItem {
    return ConstraintItem(target: self.target, attributes: ConstraintAttributes.center)
}
...其他均類似

可以看到這里面構造了一個 ConstraintItem 對象

public final class ConstraintItem {
    
    internal weak var target: AnyObject?
    internal let attributes: ConstraintAttributes
    
    internal init(target: AnyObject?, attributes: ConstraintAttributes) {
        self.target = target
        self.attributes = attributes
    }
    
    internal var layoutConstraintItem: LayoutConstraintItem? {
        return self.target as? LayoutConstraintItem
    }
}

這個類也很簡單, 主要就是保存一下布局的目標對象與目標屬性

回到 relateTo 這個方法中, 這個方法有4 個主要分支
第一個分支就是對象為 ConstraintItem 的分支
首先使用了 guard 判斷了是否為一個合法的對象, 之后就進入后續處理, 而對于 UIView 和 ConstraintLayoutGuide 則直接將屬性設置為 none, 而字面值類型, 則直接將值保存起來
獲取了 related 與 constant 之后, 后續會使用 description 生成一個 ConstraintMakerEditable, 并在之后, 修改 description , 添加新增的屬性.

ConstraintMakerEditable

ConstraintMakerEditable 這個類主要是設置Autolayout 中的兩個常量multiplier 和 constant 與優先級
使用方法如make.center.equalTo(self.view.snp.center).offset(20)

再次回到makeConstraints

通過上面的若干步驟, 完成了對 ConstraintDescription 的設置, 現在可以用他來生成 Constraint 了, 生成的部分在ConstraintDescription 的 constraint 屬性里面,

internal lazy var constraint: Constraint? = {
    guard let relation = self.relation,
          let related = self.related,
          let sourceLocation = self.sourceLocation else {
        return nil
    }
    let from = ConstraintItem(target: self.item, attributes: self.attributes)
    
    return Constraint(
        from: from,
        to: related,
        relation: relation,
        sourceLocation: sourceLocation,
        label: self.label,
        multiplier: self.multiplier,
        constant: self.constant,
        priority: self.priority
    )
}()

Constraint 創建過程很像NSLayoutConstraint

Constraint

這個類主要就是生成和操縱 NSLayoutConstraint.
構造函數有點長, 下面是去掉一些簡單的賦值和多平臺適配后的代碼

internal init(...) {
    self.layoutConstraints = []
    // get attributes
    let layoutFromAttributes = self.from.attributes.layoutAttributes
    let layoutToAttributes = self.to.attributes.layoutAttributes
    
    // get layout from
    let layoutFrom = self.from.layoutConstraintItem!
    
    // get relation
    let layoutRelation = self.relation.layoutRelation
    
    for layoutFromAttribute in layoutFromAttributes {
        // get layout to attribute
        let layoutToAttribute: NSLayoutAttribute
        if layoutToAttributes.count > 0 {
            if self.from.attributes == .edges && self.to.attributes == .margins {
                switch layoutFromAttribute {
                case .left:
                    layoutToAttribute = .leftMargin
                case .right:
                    layoutToAttribute = .rightMargin
                case .top:
                    layoutToAttribute = .topMargin
                case .bottom:
                    layoutToAttribute = .bottomMargin
                default:
                    fatalError()
                }
            } else if self.from.attributes == .margins && self.to.attributes == .edges {
                switch layoutFromAttribute {
                case .leftMargin:
                    layoutToAttribute = .left
                case .rightMargin:
                    layoutToAttribute = .right
                case .topMargin:
                    layoutToAttribute = .top
                case .bottomMargin:
                    layoutToAttribute = .bottom
                default:
                    fatalError()
                }
            } else if self.from.attributes == self.to.attributes {
                layoutToAttribute = layoutFromAttribute
            } else {
                layoutToAttribute = layoutToAttributes[0]
            }
        } else {
            if self.to.target == nil && (layoutFromAttribute == .centerX || layoutFromAttribute == .centerY) {
                layoutToAttribute = layoutFromAttribute == .centerX ? .left : .top
            } else {
                layoutToAttribute = layoutFromAttribute
            }
        }
        // get layout constant
        let layoutConstant: CGFloat = self.constant.constraintConstantTargetValueFor(layoutAttribute: layoutToAttribute)
        
        // get layout to
        var layoutTo: AnyObject? = self.to.target
        
        // use superview if possible
        if layoutTo == nil && layoutToAttribute != .width && layoutToAttribute != .height {
            layoutTo = layoutFrom.superview
        }
        
        // create layout constraint
        let layoutConstraint = LayoutConstraint(
            item: layoutFrom,
            attribute: layoutFromAttribute,
            relatedBy: layoutRelation,
            toItem: layoutTo,
            attribute: layoutToAttribute,
            multiplier: self.multiplier.constraintMultiplierTargetValue,
            constant: layoutConstant
        )
        
        // set label
        layoutConstraint.label = self.label
        
        // set priority
        layoutConstraint.priority = self.priority.constraintPriorityTargetValue
        
        // set constraint
        layoutConstraint.constraint = self
        
        // append
        self.layoutConstraints.append(layoutConstraint)
    }
}

函數中第一行的self.layoutConstraints = [] 使用來存放所有最后生成的NSLayoutConstraint
后面的兩行是獲取兩個對象的約束屬性. 而 layoutFrom 則是約束屬性的起始對象, 在我們最初那段代碼中, 就表示了snplabel 這個視圖.
后面則是獲取約束的關系, 如等于, 大于
主要的代碼都在那個循環中, 主要邏輯是遍歷添加在起始對象上的約束屬性, 然后獲取預支對應的目標對象及目標對象的約束屬性, 最后生成 LayoutConstraint
其中第一個 if else 分支中在確定目標屬性該使用何種值, 通過分析可以看出, 我們之前那段代碼, 其實可以將make.center.equalTo(self.view.snp.center) 中直接寫為make.center.equalTo(self.view)(這個實現原理在第一個else 語句中的 else 語句中實現)
后面則是根據不同的目標屬性, 獲取適當的偏移值. 以及獲取目標對象.
后面 LayoutConstraint(xxx) 中的 LayoutConstraint 其實只是一個 NSLayoutConstraint 的子類, 只是在其中添加了一個標簽與創建者(Constraint) 的引用

activateIfNeeded

makeConstraints最后一步則是激活, 在 iOS 8 以前, 所有的依賴屬性, 都必須使用 view.addConstraint(xxx) 方法將依賴激活, iOS 8 后, 則直接將依賴激活即可生效.
activateIfNeeded 則是將依賴激活使其生效

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容