SnapKit源码分析

官方Github说明上的实例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import SnapKit

class MyViewController: UIViewController {

lazy var box = UIView()

override func viewDidLoad() {
super.viewDidLoad()

self.view.addSubview(box)
box.snp.makeConstraints { (make) -> Void in
make.width.height.equalTo(50)
make.center.equalTo(self.view)
}
}

}

snp是什么

它定义在ConstraintView+Extensions.swift中,是一个对ConstraintView的扩展,ConstraintView根据对应的平台不同代表UIView或者NSView

1
2
3
4
5
6
7
8
9
10
11
12
#if os(iOS) || os(tvOS)
import UIKit
#else
import AppKit
#endif


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

统一各平台View的差异性,定义了一个统一的别名。

1
2
3
4
5
6
7
8
9
10
11
12
13
public extension ConstraintView {

//最初版本的方法被弃用了
@available(*, deprecated, message:"Use newer snp.* syntax.")
var snp_left: ConstraintItem { return self.snp.left }
......

//扩展了一个snp计算型属性
var snp: ConstraintViewDSL {
return ConstraintViewDSL(view: self)
}

}

该扩展为UIView扩展了一个名叫snp的计算型属性,返回一个持有了view实例的ConstraintViewDSL对象,结合官方给的例子,就是持有了boxConstraintViewDSL对象。

makeConstraints如何执行的

ConstraintViewDSL是一个实现了ConstraintAttributesDSL的协议的结构体,ConstraintAttributesDSL协议又继承自ConstraintBasicAttributesDSL协议,最后继承到ConstraintDSL协议,完整的继承关系如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public protocol ConstraintDSL {

var target: AnyObject? { get }

func setLabel(_ value: String?)
func label() -> String?

}

public protocol ConstraintBasicAttributesDSL : ConstraintDSL {
}

public protocol ConstraintAttributesDSL : ConstraintBasicAttributesDSL {
}

这协议都通过extension定义了一些默认的方法实现首先ConstraintDSL实现如下:

1
2
3
4
5
6
7
8
9
10
extension ConstraintDSL {

public func setLabel(_ value: String?) {
objc_setAssociatedObject(self.target as Any, &labelKey, value, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
public func label() -> String? {
return objc_getAssociatedObject(self.target as Any, &labelKey) as? String
}

}

这两个方法会为我们调用snpview添加一个标签,用于帮助我们调试,用官方例子来说,就是可以像下面这样添加一个标签:

1
box.snp.setLabel("box view") //添加用于调试的标签信息

ConstraintBasicAttributesDSLConstraintAttributesDSL协议都是定义了我们可以设置的属性比如leftwidthcenterX等等,不同的在于ConstraintBasicAttributesDSL协议中提供的属性是低版本就有的一些通用属性,而ConstraintAttributesDSL提供的是iOS8新增的属性,比如lastBaselineleftMargintopMargin等。部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
extension ConstraintBasicAttributesDSL {

// MARK: Basics

public var left: ConstraintItem {
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.left)
}

public var top: ConstraintItem {
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.top)
}

......
}

extension ConstraintAttributesDSL {

// MARK: Baselines

@available(*, deprecated, message:"Use .lastBaseline instead")
public var baseline: ConstraintItem {
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.lastBaseline)
}

@available(iOS 8.0, OSX 10.11, *)
public var lastBaseline: ConstraintItem {
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.lastBaseline)
}

@available(iOS 8.0, OSX 10.11, *)
public var firstBaseline: ConstraintItem {
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.firstBaseline)
}

// MARK: Margins

@available(iOS 8.0, *)
public var leftMargin: ConstraintItem {
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.leftMargin)
}

@available(iOS 8.0, *)
public var topMargin: ConstraintItem {
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.topMargin)
}

......
}

接下来看定义在ConstraintViewDSL结构体中的makeConstraints方法:

1
2
3
4
public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
//self.view 对应官方实例中的box, closure就是我们后面写的闭包
ConstraintMaker.makeConstraints(item: self.view, closure: closure)
}

这个方法接收一个闭包,然后将闭包传给了ConstraintMaker的静态方法中,那就看看`makeConstraints静态方法做了什么:

1
2
3
4
5
6
7
// ConstraintMaker中的静态方法
internal static func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
let constraints = prepareConstraints(item: item, closure: closure)
for constraint in constraints {
constraint.activateIfNeeded(updatingExisting: false)
}
}

这里LayoutConstraintItem又是一个协议代码像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//定义了一个是能被类遵守的协议
public protocol LayoutConstraintItem: class {
}
//还为iOS9新增的Guide也遵守了这个协议
@available(iOS 9.0, OSX 10.11, *)
extension ConstraintLayoutGuide : LayoutConstraintItem {
}

//这里为我们的UIView遵守了这个协议
extension ConstraintView : LayoutConstraintItem {
}


extension LayoutConstraintItem {
// 这个方法为代码布局的UIView对象关闭了Autoresizing自动创建AutoLayout约束防止约束冲突
internal func prepare() {
if let view = self as? ConstraintView {
view.translatesAutoresizingMaskIntoConstraints = false
}
}
......
}

再来看makeConstraints第一行代码所调用的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
internal static func prepareConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
// 构造了持有UIView实例的ConstraintMaker用于收集所有约束信息
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)
}
return constraints
}

这个方法就是实际调用闭包的地方,首先将我们的View构造了ConstraintMaker对象:

1
2
3
4
5
6
internal init(item: LayoutConstraintItem) {
// 首先持有了UIView对象
self.item = item
// 接着调用了LayoutConstraintItem扩展的prepare()方法 避免了AutoLayout约束冲突
self.item.prepare()
}

接着就调用了我们传进来的闭包closure(maker),将刚才的ConstraintMaker传进去了,所以我们才能使用闭包中的maker设置各种约束。

链式调用的实现

看看ConstraintMaker代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class ConstraintMaker {
///删除了一些属性,只保留两个有代表性的
public var width: ConstraintMakerExtendable {
//调用一个方法包装系统属性
return self.makeExtendableWithAttributes(.width)
}

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

private let item: LayoutConstraintItem
private var descriptions = [ConstraintDescription]()

internal init(item: LayoutConstraintItem) {
self.item = item
self.item.prepare()
}
//包装系统的约束属性
internal func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
// ConstraintDescription这个类记录一条约束的完整信息,目前可能还不完整但是稍后会通过ConstraintMakerExtendable类的操作补全信息
let description = ConstraintDescription(item: self.item, attributes: attributes)
// 将约束信息保存到一个数组中,因为一个对象有多条约束信息
self.descriptions.append(description)
// 返回给外部使用用于继续设置约束信息
return ConstraintMakerExtendable(description)
}

internal static func prepareConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
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)
}
return constraints
}

internal static func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
let constraints = prepareConstraints(item: item, closure: closure)
for constraint in constraints {
constraint.activateIfNeeded(updatingExisting: false)
}
}
}

由于每个属性都返回ConstraintMakerExtendable对象所以他们可以链式的调用设置属性的值,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 这个对象用于设置约束的关系 具体实现篇幅关系 省略了
public class ConstraintMakerRelatable {

internal let description: ConstraintDescription

internal init(_ description: ConstraintDescription) {
self.description = description
}

internal func relatedTo(_ other: ConstraintRelatableTarget, relation: ConstraintRelation, file: String, line: UInt) -> ConstraintMakerEditable {
......
}

@discardableResult
public func equalTo(_ other: ConstraintRelatableTarget, _ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable {
return self.relatedTo(other, relation: .equal, file: file, line: line)
}
......
}

// 这个对象主要是用于合并属性 合并属性用的是OptionSet的重在操作符
public class ConstraintMakerExtendable: ConstraintMakerRelatable {

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

public var top: ConstraintMakerExtendable {
self.description.attributes += .top
return self
}

......
}

由于每个操作都返回ConstraintMakerExtendable对象,所以链式调用就实现了

1
internal let description: ConstraintDescription

并且最后都操作在了description这个内部属性上,所以每一步的操作组合成了一条完整的约束所需要的信息。

1
2
3
4
5
6
7
for description in maker.descriptions {
// 这里取得每一个约束的描述对象构造了Constraint对象,这类里面实际构造了系统的AutoLayout约束对象和约束相关的操作,还记录了一些调试信息,因为Constraint的构造过程是可为空的所以这里判断非空才加到数组中
guard let constraint = description.constraint else {
continue
}
constraints.append(constraint)
}

启用约束

所以经过closure(maker)这么一句短短的调用,所有的约束信息就都被收集在[ConstraintDescription]数组中,所以下面的操作就是取出一条条的约束并且设置启用。

1
2
3
for constraint in constraints {
constraint.activateIfNeeded(updatingExisting: false)
}

下面就是Constraint关于启用约束的代码,简单看一下实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
internal func activateIfNeeded(updatingExisting: Bool = false) {
//首先判断这个
guard let item = self.from.layoutConstraintItem else {
print("WARNING: SnapKit failed to get from item from constraint. Activate will be a no-op.")
return
}
//我们在闭包中像make.width.height.equalTo(50)这种组合设置的约束在Constraint内部构造系统约束时都会分开构造,所以这里用数组存储
let layoutConstraints = self.layoutConstraints

//外部传递的参数 是否更新以存在的约束 对于makeConstraints方法 传递的是false
if updatingExisting {
//这里先记录View上已经存在的约束信息
var existingLayoutConstraints: [LayoutConstraint] = []
for constraint in item.constraints {
existingLayoutConstraints += constraint.layoutConstraints
}
//循环将要设置的约束信息
for layoutConstraint in layoutConstraints {
//取出第一个重复的约束
let existingLayoutConstraint = existingLayoutConstraints.first { $0 == layoutConstraint }
//如果不为空
guard let updateLayoutConstraint = existingLayoutConstraint else {
fatalError("Updated constraint could not find existing matching constraint to update: \(layoutConstraint)")
}
//就更新这个约束 先判断是否是width,height等没有相对约束的自身约束,取出要更新的属性
let updateLayoutAttribute = (updateLayoutConstraint.secondAttribute == .notAnAttribute) ? updateLayoutConstraint.firstAttribute : updateLayoutConstraint.secondAttribute
//constraintConstantTargetValueFor将得到的属性值转换成对应的格式更新到约束中
updateLayoutConstraint.constant = self.constant.constraintConstantTargetValueFor(layoutAttribute: updateLayoutAttribute)
}
} else {
//如果不需要更新直接启用新设置的约束
NSLayoutConstraint.activate(layoutConstraints)
//加到View持有的Set中保存起来
item.add(constraints: [self])
}
}

makeConstraints方法的大致流程就分析完了

不同的构造约束的方法区别

SnapKit提供了三种方法构造约束,从源码的角度看看它们有什么区别吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//最常用的 上面以及分析过
internal static func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
let constraints = prepareConstraints(item: item, closure: closure)
for constraint in constraints {
constraint.activateIfNeeded(updatingExisting: false)
}
}
//简单粗暴 删除之前的约束 设置新的约束
internal static func remakeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
self.removeConstraints(item: item)
self.makeConstraints(item: item, closure: closure)
}

//只更新以存在的约束 如果有之前不存在的约束会抛出错误
internal static func updateConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
guard item.constraints.count > 0 else {
self.makeConstraints(item: item, closure: closure)
return
}

let constraints = prepareConstraints(item: item, closure: closure)
for constraint in constraints {
constraint.activateIfNeeded(updatingExisting: true)
}
}

查找公共父视图

这是一段时间以来有名的面试题,在OC平台的Masonry中有一个方法mas_closestCommonSuperview实现了这个算法,原因在于iOS8之前的版本是没有NSLayoutConstraint.activate()这么简便的设置约束的方法的,有些约束需要设置在自身上,有些需要设置在公共父视图上,所以Masonry实现了这个算法,由于Swift iOS8以上才可以使用所以就没有必要实现那个算法了。