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 则是将依赖激活使其生效

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容