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 是如何實現(xiàn)的?

分析源碼

我們從最開始的 snplabel.snp 開始
你也許猜到了, 這個是通過給 view 添加一個擴展實現(xiàn)的
這個在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 為了實現(xiàn)多平臺將 ConstraintView 分別定義為 UIView 和 NSView 的別名. 我們這里也為了簡單起見, 不考慮多平臺適配, 我們將 ConstraintView 都替換為 UIView

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

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

ConstraintViewDSL

ConstraintViewDSL 類的構(gòu)造函數(shù)很簡單, 就是將 view 保存起來

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

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

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

ConstraintMaker

ConstraintMaker.makeConstraints 的實現(xiàn)如下所示

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

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

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

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

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

這一步其實就是禁用 View 的 AutoresizeMask 轉(zhuǎn)換.

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

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

這個只是一個簡便方法, 具體的實現(xiàn)繼續(xù)去查看定義

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

可以看到流程為首先根據(jù)約束屬性及需要添加約束的對象生成一個描述, 然后將其添加內(nèi)部的一個數(shù)組, 也就是之前 makeConstraints 中第一個 for 循環(huán)鎖遍歷的數(shù)組, 最后返回一個 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 復(fù)合而來.

ConstraintDescription

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

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 循環(huán), 里面就是去獲取 description.constraint 已達(dá)到最終構(gòu)造約束的目的

ConstraintMakerExtendable

makeExtendableWithAttributes 最后返回的時候, 返回的是一個ConstraintMakerExtendable 對象
這個類的主要目的是為了實現(xiàn)鏈?zhǔn)降亩鄬傩? 例如, 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, 這個類主要是負(fù)責(zé)構(gòu)造一個關(guān)系, 例如 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 只是對內(nèi)部函數(shù)relatedTo 的一個簡單調(diào)用

ConstraintRelatableTarget

這是一個協(xié)議, 表示一個可以被依賴的目標(biāo), 我們在手寫 NSLayoutConstraint 的時候, 依賴對象可以為 view, 可以為ConstraintLayoutGuide, 也可以為空, 為空的時候, 表示使用絕對值
ConstraintRelatableTarget 是一個協(xié)議, 分別有 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)
}
...其他均類似

可以看到這里面構(gòu)造了一個 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
    }
}

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

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

ConstraintMakerEditable

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

再次回到makeConstraints

通過上面的若干步驟, 完成了對 ConstraintDescription 的設(shè)置, 現(xiàn)在可以用他來生成 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 創(chuàng)建過程很像NSLayoutConstraint

Constraint

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

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

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

activateIfNeeded

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末领舰,一起剝皮案震驚了整個濱河市灸蟆,隨后出現(xiàn)的幾起案子急波,更是在濱河造成了極大的恐慌,老刑警劉巖摊求,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件克握,死亡現(xiàn)場離奇詭異光稼,居然都是意外死亡坤次,警方通過查閱死者的電腦和手機步鉴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門揪胃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人氛琢,你說我怎么就攤上這事喊递。” “怎么了阳似?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵骚勘,是天一觀的道長。 經(jīng)常有香客問我撮奏,道長俏讹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任畜吊,我火速辦了婚禮泽疆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘玲献。我一直安慰自己殉疼,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布捌年。 她就那樣靜靜地躺著瓢娜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪礼预。 梳的紋絲不亂的頭發(fā)上眠砾,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音托酸,去河邊找鬼荠藤。 笑死,一個胖子當(dāng)著我的面吹牛获高,可吹牛的內(nèi)容都是我干的哈肖。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼念秧,長吁一口氣:“原來是場噩夢啊……” “哼淤井!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤币狠,失蹤者是張志新(化名)和其女友劉穎游两,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體漩绵,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡贱案,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了止吐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宝踪。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖碍扔,靈堂內(nèi)的尸體忽然破棺而出瘩燥,到底是詐尸還是另有隱情,我是刑警寧澤不同,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布厉膀,位于F島的核電站,受9級特大地震影響二拐,放射性物質(zhì)發(fā)生泄漏服鹅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一百新、第九天 我趴在偏房一處隱蔽的房頂上張望企软。 院中可真熱鬧,春花似錦吟孙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至碘勉,卻和暖如春巷挥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背验靡。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工倍宾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人胜嗓。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓高职,卻偏偏與公主長得像,于是被迫代替她去往敵國和親辞州。 傳聞我的和親對象是個殘疾皇子怔锌,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容