讀 SnapKit 和 Masonry 自動布局框架源碼

vip60402 6年前發布 | 29K 次閱讀 Masonry 移動開發

一直覺得 SnapKit 和 Masonry 這兩個框架設計和封裝的很好,用起來的體驗也是一致的,翻了下它們的源碼,對其設計方式和涉及的技術做了下記錄。文章打算圍繞,給誰做約束?如何設置約束?設置完后如何處理?這三個問題看看 SnapKit 和 Masnory 分別是怎么做的,正好也能夠窺探下作者是如何利用 Swift 和 Objective-C 兩個不同語言的不同特性做到一致的使用體驗的。

如果還不了解這兩個框架的使用的話可以參看它們項目 GitHub 說明: GitHub - SnapKit/SnapKit: A Swift Autolayout DSL for iOS & OS XGitHub - SnapKit/Masonry: Harness the power of AutoLayout NSLayoutConstraints with a simplified, chainable and expressive syntax. Supports iOS and OSX Auto Layout

如果還不了解自動布局或者還沒有用過的同學可以參看我三年前這篇文章,里面有詳細的介紹和相關資料: 深入剖析Auto Layout,分析iOS各版本新增特性 | 星光社 - 戴銘的博客

進入那三個問題之前我們先看看兩個框架的整體結構圖,對它們有個大概的印象。

SnapKit 源碼結構圖

Masonry 源碼結構圖

接下來我們來詳細看看兩個框架的內部,首先來看看剛才那三個問題中的第一個問題。

給誰做約束?

SnapKit

ConstraintView

這個 View 實際上在 iOS 里就是 UIView,在 macOS 上就是 NSView。

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

對 ConstraintView 做擴展,里面定義里一個屬性 snp

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

這個 snp 屬性的類型就是結構體 ConstraintViewDSL。下面來看看 ConstraintViewDSL 這個結構體做什么用的。

ConstraintViewDSL

這個結構體會在初始化時通過 view 屬性持有 ConstraintView。

internal let view: ConstraintView

internal init(view: ConstraintView) {
    self.view = view

}

同時還提供了那些我們必調用的 makeConstraints,contentHuggingHorizontalPriority 等等函數。這樣我們就可以在 UIView 中直接調用這些函數來進行視圖的約束設置了。

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

public var contentHuggingHorizontalPriority: Float {
    get {
        return self.view.contentHuggingPriority(for: .horizontal).rawValue
    }
    set {
        self.view.setContentHuggingPriority(LayoutPriority(rawValue: newValue), for: .horizontal)
    }
}
//還有 remakeConstraints,contentCompressionResistanceHorizontalPriority 等等就不一一列出了
...

ConstraintViewDSL 是繼承自 ConstraintAttributesDSL。

ConstraintAttributesDSL

ConstraintAttributesDSL 是個協議,繼承于 ConstraintBasicAttributesDSL 這個協議,為什么要多這一層呢,因為 ConstraintAttributesDSL 這個里面定了 iOS 8 系統出現的新的屬性,比如 lastBaseline,firstBaseline,leftMargin 等。而 ConstraintBasicAttributesDSL 里定義的是一開始就有的那些屬性比如 left,top,centerX,size 等。

Masonry

接下來我們看看 Masonry 是給誰做的約束。

View+MASAdditions

View+MASAdditions 就是 Masonry 的一個外部的入口,實質上就是 UIView 的一個 Category 作用就是用來設置 MASViewAttribute 的屬性,并實例化,并且指定當前的 UIView 對應的 LayoutAttribute。和 SnapKit 一樣, Masonry 也對 iOS 和 macOS 做了兼容,在 macOS 里就是 NSView,相關代碼在 MASUtilities.h 文件里,這里除了平臺相關代碼外,還有些宏的定義和靜態方法。這里我們可以看看靜態方法 static inline id _MASBoxValue(const char *type, …) 的作用:

static inline id _MASBoxValue(const char *type, ...) {
    va_list v;
    va_start(v, type);
    id obj = nil;
    if (strcmp(type, @encode(id)) == 0) {
        id actual = va_arg(v, id);
        obj = actual;
    } else if (strcmp(type, @encode(CGPoint)) == 0) {
        CGPoint actual = (CGPoint)va_arg(v, CGPoint);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(CGSize)) == 0) {
        CGSize actual = (CGSize)va_arg(v, CGSize);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(MASEdgeInsets)) == 0) {
        MASEdgeInsets actual = (MASEdgeInsets)va_arg(v, MASEdgeInsets);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(double)) == 0) {
        double actual = (double)va_arg(v, double);
        obj = [NSNumber numberWithDouble:actual];
    } else if (strcmp(type, @encode(float)) == 0) {
        float actual = (float)va_arg(v, double);
        obj = [NSNumber numberWithFloat:actual];
    } else if (strcmp(type, @encode(int)) == 0) {
        int actual = (int)va_arg(v, int);
        obj = [NSNumber numberWithInt:actual];
    } else if (strcmp(type, @encode(long)) == 0) {
        long actual = (long)va_arg(v, long);
        obj = [NSNumber numberWithLong:actual];
    } else if (strcmp(type, @encode(long long)) == 0) {
        long long actual = (long long)va_arg(v, long long);
        obj = [NSNumber numberWithLongLong:actual];
    } else if (strcmp(type, @encode(short)) == 0) {
        short actual = (short)va_arg(v, int);
        obj = [NSNumber numberWithShort:actual];
    } else if (strcmp(type, @encode(char)) == 0) {
        char actual = (char)va_arg(v, int);
        obj = [NSNumber numberWithChar:actual];
    } else if (strcmp(type, @encode(bool)) == 0) {
        bool actual = (bool)va_arg(v, int);
        obj = [NSNumber numberWithBool:actual];
    } else if (strcmp(type, @encode(unsigned char)) == 0) {
        unsigned char actual = (unsigned char)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedChar:actual];
    } else if (strcmp(type, @encode(unsigned int)) == 0) {
        unsigned int actual = (unsigned int)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedInt:actual];
    } else if (strcmp(type, @encode(unsigned long)) == 0) {
        unsigned long actual = (unsigned long)va_arg(v, unsigned long);
        obj = [NSNumber numberWithUnsignedLong:actual];
    } else if (strcmp(type, @encode(unsigned long long)) == 0) {
        unsigned long long actual = (unsigned long long)va_arg(v, unsigned long long);
        obj = [NSNumber numberWithUnsignedLongLong:actual];
    } else if (strcmp(type, @encode(unsigned short)) == 0) {
        unsigned short actual = (unsigned short)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedShort:actual];
    }
    va_end(v);
    return obj;
}

看這段代碼是不是就能猜出來是做什么的了,對,它就是我們經常使用的 mas_equalTo 這個方法,這里可以看到它是如何支持變參和如何將 float,double,int 這樣的值類型數據轉換成和 equalTo 一樣的對象 NSNumber 數據的。這個寫法靈感來自 GitHub - specta/expecta: A Matcher Framework for Objective-C/Cocoa 。 mas_equalTo 和 equalTo 都是宏定義的。

#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))

#define equalTo(...) mas_equalTo(__VA_ARGS__)

#define MASBoxValue(value) _MASBoxValue(@encode(__typeof__((value))), (value))

MASBoxValue 這個宏定義就是上面的 _MASBoxValue 這個方法。細心同學會發現這兩個 equal 的宏對應的方法是不同的,一個是 equalTo(MASBoxValue(( VA_ARGS ))) 另一個是 mas_equalTo( VA_ARGS ) 但是這兩個方法的實現是一樣的。

- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}

- (MASConstraint * (^)(id))mas_equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}

這樣寫就是避免宏定義沖突的一種方式。

這個 Category 還有那些我們總是調用的 mas_makeConstraints,mas_updateConstraints,mas_remakeConstraint 等方法。

mas_makeConstraints 的 block 參數會將創建的 MASConstraintMaker 這個工廠類對象暴露出去,讓我們去設置這個類對象中的 MASConstraint 屬性,然后通過該對象的 install 方法將當前視圖中所有添加的約束添加到一個數組里。該數組里存儲是 MASViewConstraint 對象,對應的就是 NSLayoutConstraint。具體代碼如下:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

這種設計模式和 SnapKit 的一樣使用了閉包來獲取用戶設置的數據,在設計模式里叫做好萊塢原則。

mas_updateConstraints 和 mas_makeConstraints 差不多,不過里面多了一行:

constraintMaker.updateExisting = YES;

這樣當添加約束時會通過這個屬性是否為真來檢查約束是否 intall 了,是的話就更新,沒有就添加。

if (self.updateExisting) {
    existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
}
if (existingConstraint) {
    // just update the constant
    existingConstraint.constant = layoutConstraint.constant;
    self.layoutConstraint = existingConstraint;
} else {
    [self.installedView addConstraint:layoutConstraint];
    self.layoutConstraint = layoutConstraint;
    [firstLayoutItem.mas_installedConstraints addObject:self];
}

mas_remakeConstraints 的話是添加了這一句:

constraintMaker.removeExisting = YES;

設置為 YES 后會將以前設置的約束 uninstall 掉,后面再把新設置的約束添加上。

if (self.removeExisting) {
    NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
    for (MASConstraint *constraint in installedConstraints) {
        [constraint uninstall];
    }
}

最后還有個方法 mas_closestCommonSuperview,這個方法一般我們都不會主動調用,所以很多人應該都太熟悉,不過這個斷言報錯大家應該會有很深刻的印象 couldn’t find a common superview for … 。所以這個方法如其名就是去找共同的父視圖,還是最近的。框架內部也就在 MASViewConstraint 的 install 方法里用了一次。

- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view {
    MAS_VIEW *closestCommonSuperview = nil;

    MAS_VIEW *secondViewSuperview = view;
    while (!closestCommonSuperview && secondViewSuperview) {
        MAS_VIEW *firstViewSuperview = self;
        while (!closestCommonSuperview && firstViewSuperview) {
            if (secondViewSuperview == firstViewSuperview) {
                closestCommonSuperview = secondViewSuperview;
            }
            firstViewSuperview = firstViewSuperview.superview;
        }
        secondViewSuperview = secondViewSuperview.superview;
    }
    return closestCommonSuperview;
}

這個查找是 N 方的,誰有辦法能夠優化下么。

如何設置約束?

SnapKit

先看看這張圖,里面是我們使用框架時用的最多的設置 make 的過程,圖里將每個操作對應的不同 ConstraintMaker 做了說明。

下面來對這幾種 ConstraintMaker 來詳細說下。

ConstraintMaker

這個是設置的入口,makeConstraints 函數一個閉包參數可以提供外部去設置ConstraintMaker 自己的 left,right,top 等屬性來描述約束。這些屬性的 getter 方法會返回 ConstraintMakerExtendable 實例。

先看看 ConstraintMaker 的構造函數:

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

LayoutConstraintItem 會通過給擴展 ConstraintLayoutGuide 和 ConstraintView 來達到約束 item 類型的作用。下面看看 prepare 這個函數的作用。

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

看,禁用 AutoresizeMask 是在這里統一處理了。

接下來看看閉包參數設置屬性的 getter 方法。

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

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

ConstraintMaker 包含了一個 ConstraintDescription 數組,里面會記錄用戶設置的各個屬性,然后返回 ConstraintMakerExtendable。

OptionSet

這里的 ConstraintAttributes 是個 OptionSet,ConstraintAttributes 結構體來遵從 OptionSet 選項集合協議,為什么不用枚舉呢?因為在一次只有一個選項被選中是枚舉是 OK 的。但是在 Swift 里的枚舉是沒法將多個枚舉選項組成一個值的,比如 ConstraintAttributes 里的 edges,size 和 center 等就是組合而成的。而 OptionSet 結構體使用了高效的位域來表示的。還有,OptionSet 繼承于 ExpressibleByArrayLiteral,這樣還能夠使用數組字面量來生成選項的集合。下面看看這個 ConstraintAttributes 是如何定義的。

internal static var none: ConstraintAttributes { return self.init(0) }
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 bottom: ConstraintAttributes { return self.init(8) }
internal static var leading: ConstraintAttributes { return self.init(16) }
internal static var trailing: ConstraintAttributes { return self.init(32) }
internal static var width: ConstraintAttributes { return self.init(64) }
internal static var height: ConstraintAttributes { return self.init(128) }
internal static var centerX: ConstraintAttributes { return self.init(256) }
internal static var centerY: ConstraintAttributes { return self.init(512) }
//更多就不一一列出來
...
// 組合的
internal static var edges: ConstraintAttributes { return self.init(15) }
internal static var size: ConstraintAttributes { return self.init(192) }
internal static var center: ConstraintAttributes { return self.init(768) }

@available(iOS 8.0, *)
internal static var margins: ConstraintAttributes { return self.init(61440) }
//還有一些,先不列了
...

可以看到組合的 size 就是 width(64) + height(128)= size(192)。

重載和自定義操作符

ConstraintAttributes 重載了 +,+=,-= 和 == 這些操作符。我們先看看代碼

internal func + (left: ConstraintAttributes,right: ConstraintAttributes) -> ConstraintAttributes {
    return left.union(right)
}

internal func +=(left:inout ConstraintAttributes, right: ConstraintAttributes) {
    left.formUnion(right)
}

internal func -=(left:inout ConstraintAttributes, right: ConstraintAttributes) {
    left.subtract(right)
}

internal func ==(left: ConstraintAttributes,right: ConstraintAttributes) -> Bool {
    return left.rawValue == right.rawValue
}

這種重載很適合對自定義的結構體進行一些熟悉的簡化符號操作。

如果希望自定義一些操作符的話就需要先聲明下,讓編譯器知道這是個操作符,比如我們自定義一個操作符?

struct A{
    var v:Int = 0
}
infix operator ?
func ?(left: A,right: A) -> Bool {
    return left.v + 1 == right.v
}

這里的 infix 是中間運算符的意思,還有前置運算符 prefix 和后置運算符 postfix。自定義運算符之能是類似,/,=,-,+,*,%,<,>,!,&,|,^,。,~ 等這樣的符號組成,也能支持一些特殊的字符比如剛才的用的?,還有?,? 這樣的特殊符號。

自定義運算符還能夠指定優先級分組 precedencegroup,如下:

infix operator ? : CPrecedence
precedencegroup CPrecedence {
    associativity: left
    higherThan: AdditionPrecedence
    lowerThan: MultiplicationPrecedence
}

下面列下常用類型對應的group

// "Exponentiative"
infix operator  << : BitwiseShiftPrecedence
infix operator &<< : BitwiseShiftPrecedence
infix operator  >> : BitwiseShiftPrecedence
infix operator &>> : BitwiseShiftPrecedence

// "Multiplicative"
infix operator   * : MultiplicationPrecedence
infix operator  &* : MultiplicationPrecedence
infix operator   / : MultiplicationPrecedence
infix operator   % : MultiplicationPrecedence
infix operator   & : MultiplicationPrecedence

// "Additive"
infix operator   + : AdditionPrecedence
infix operator  &+ : AdditionPrecedence
infix operator   - : AdditionPrecedence
infix operator  &- : AdditionPrecedence
infix operator   | : AdditionPrecedence
infix operator   ^ : AdditionPrecedence

//FIXME:is this the right precedence level for "..." ?
infix operator  ... : RangeFormationPrecedence
infix operator ..< : RangeFormationPrecedence

完整的操作符的定義和 precedencegroup 之間的優先級關系在 Swift 源碼的 swift/stdlib/public/core/Policy.swift 文件里,在線看地址是: https://github.com/apple/swift/blob/a7ff0da33488b9050cf83df95f46e5b9aa2348d5/stdlib/public/core/Policy.swift 。那些操作符優先級高些或者低些在這個文件里是一目了然。

ConstraintMakerExtendable

ConstraintMakerExtendable 繼承 ConstraintMakerRelatable,它可以實現鏈式的多屬性,有left,right,top 等等這樣的屬性,用以產生一個 ConstraintMakerRelatable 類型的實例。

我們看看 left 屬性的 getter 定義:

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

這里可以看到通過重載的操作符 += 能夠將 .left 加到 ConstraintAttributes 里。

ConstraintMakerRelatable

用于指定約束關系比如常用的 equalTo。equalTo 函數里面是調用的 relatedTo 函數,返回 ConstraintMakerEditable 類型的實例。

@discardableResult
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? ConstraintView {
        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
}

這里的 ConstraintRelatableTarget 是約束,equalTo 這個方法里面能傳的參數類型比較多,可以通過這個協議來擴展下只支持的類型,達到限制類型的功能。ConstraintPriorityTarget,ConstraintInsetTarget,ConstraintOffsetTarget 和 ConstraintInsetTarget 也都有類似的作用,不過這幾個還有個作用就是將 Float,Double,Int 和 UInt 這幾種類型都轉成 CGFloat。我們拿 ConstraintInsetTarget 來看看實現如下:

extension ConstraintInsetTarget{

    internal var constraintInsetTargetValue: ConstraintInsets {
        if let amount = self as? ConstraintInsets {
            return amount
        } else if let amount = self as? Float {
            return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount))
        } else if let amount = self as? Double {
            return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount))
        } else if let amount = self as? CGFloat {
            return ConstraintInsets(top: amount, left: amount, bottom: amount, right: amount)
        } else if let amount = self as? Int {
            return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount))
        } else if let amount = self as? UInt {
            return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount))
        } else {
            return ConstraintInsets(top: 0, left: 0, bottom: 0, right: 0)
        }
    }

}

ConstraintMakerEditable

ConstraintMakerEditable 繼承 ConstraintMakerPriortizable,主要是設置約束的 offset 和 inset 還有 multipliedBy 和 dividedBy 函數。

public class ConstraintMakerEditable:ConstraintMakerPriortizable{

    @discardableResult
    public func multipliedBy(_amount: ConstraintMultiplierTarget) -> ConstraintMakerEditable {
        self.description.multiplier = amount
        return self
    }

    @discardableResult
    public func dividedBy(_amount: ConstraintMultiplierTarget) -> ConstraintMakerEditable {
        return self.multipliedBy(1.0 / amount.constraintMultiplierTargetValue)
    }

    @discardableResult
    public func offset(_amount: ConstraintOffsetTarget) -> ConstraintMakerEditable {
        self.description.constant = amount.constraintOffsetTargetValue
        return self
    }

    @discardableResult
    public func inset(_amount: ConstraintInsetTarget) -> ConstraintMakerEditable {
        self.description.constant = amount.constraintInsetTargetValue
        return self
    }

}

ConstraintMakerPriortizable

ConstraintMakerPriortizable 繼承 ConstraintMakerFinalizable,用來設置優先級,返回 ConstraintMakerFinalizable 類型的實例。

ConstraintMakerFinalizable

里面類型為 ConstraintDescription 的屬性的類是一個完整的約束描述,有了這個描述就可以做后面的處理了。里面的內容是完整的,這個類是一個描述類, 用于描述一條具體的約束, 包含了包括 ConstraintAttributes 在內的各種與約束有關的元素,一個 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? = {
        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
        )
    }()

    // MARK: Initialization

    internal init(item: LayoutConstraintItem, attributes: ConstraintAttributes) {
        self.item = item
        self.attributes = attributes
    }

}

Masonry

在 Masonry 也有對應的 ConstraintMaker。

MASConstraintMaker

MASConstraintMaker 是創建 MASConstraint 對象的。里面有個 constraints 數組專門用來存儲創建的這些對象。前面 mas_makeConstraints 的那個 Block 暴露出的就是 MASConstraintMaker 對象。

接下來看看 MASConstraint 屬性的 getter 方法:

- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}

會發現這些 getter 方法都會調用 addConstraintWithLayoutAttribute 這個方法。

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    if ([constraint isKindOfClass:MASViewConstraint.class]) {
        //replace with composite constraint
        NSArray *children = @[constraint, newConstraint];
        MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
        compositeConstraint.delegate = self;
        [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
        return compositeConstraint;
    }
    if (!constraint) {
        newConstraint.delegate = self;
        [self.constraints addObject:newConstraint];
    }
    return newConstraint;
}

這里會發現每次 getter 都會創建一個新的 MASViewConstraint 對象,這里通過將新的 MASViewConstraint 對象的 delegate 設置成自己的方式讓新對象也能夠調用相同的方法創建一個新的 MASViewConstraint 對象,使得能夠支持進行鏈式的調用。

設置完后如何處理?

SnapKit

下面通過 makeConstraints 我們來看看 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)
    }
}

這個閉包給叫做 maker 的 ConstraintMaker 實例寫入了信息,遍歷 maker 的 descriptions 之后(我們之前說一條約束語句最終得到一個 self.description,但往往會有多條約束,所以 ConstraintMakerFinalizable 里面的 self.description,在 ConstraintMaker 里被一個數組維護),我們得到了 Constraint 數組。

跟進 Constraint 里的 activateIfNeeded 這個函數看看約束是怎么寫出來的了

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
    }
    let layoutConstraints = self.layoutConstraints

    if updatingExisting {
        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)")
            }

            let updateLayoutAttribute = (updateLayoutConstraint.secondAttribute == .notAnAttribute) ? updateLayoutConstraint.firstAttribute : updateLayoutConstraint.secondAttribute
            updateLayoutConstraint.constant = self.constant.constraintConstantTargetValueFor(layoutAttribute: updateLayoutAttribute)
        }
    } else {
        NSLayoutConstraint.activate(layoutConstraints)
        item.add(constraints: [self])
    }
}

Masonry

MASViewConstraint

這個類是對 NSLayoutConstriant 的封裝。它的父類是 MASConstraint,MASConstraint 是一個抽象不可實例的類,里面有接口和協議。它的兄弟類是 MASCompositeConstraint,里面有個數組專門存儲 MASViewConstraint 對象。

MASViewConstraint 對象的 install 方法會將各個約束 install 到對應的視圖上。我們看看 MASConstraintMaker 的 install 方法:

- (NSArray *)install {
    if (self.removeExisting) {
        NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
        for (MASConstraint *constraint in installedConstraints) {
            [constraint uninstall];
        }
    }
    NSArray *constraints = self.constraints.copy;
    for (MASConstraint *constraint in constraints) {
        constraint.updateExisting = self.updateExisting;
        [constraint install];
    }
    [self.constraints removeAllObjects];
    return constraints;
}

這個方法會遍歷 constraints 里每個約束進行 install。在這個 install 方法里會創建 MASLayoutConstraint 對象,然后把這個對象添加到對應的的視圖上。

MASLayoutConstraint *layoutConstraint
    = [MASLayoutConstraint constraintWithItem:firstLayoutItem
        attribute:firstLayoutAttribute
        relatedBy:self.layoutRelation
        toItem:secondLayoutItem
        attribute:secondLayoutAttribute
        multiplier:self.layoutMultiplier
        constant:self.layoutConstant];

layoutConstraint.priority = self.layoutPriority;
layoutConstraint.mas_key = self.mas_key;

創建完 MASLayoutConstraint 對象后,會根據約束的設置判斷將約束添加到哪個視圖上。

if (self.secondViewAttribute.view) {
    MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
    NSAssert(closestCommonSuperview,
             @"couldn't find a common superview for %@ and %@",
              self.firstViewAttribute.view, self.secondViewAttribute.view);
    self.installedView = closestCommonSuperview;
} else if (self.firstViewAttribute.isSizeAttribute) {
    self.installedView = self.firstViewAttribute.view;
} else {
    self.installedView = self.firstViewAttribute.view.superview;
}


MASLayoutConstraint *existingConstraint = nil;
if (self.updateExisting) {
    existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
}
if (existingConstraint) {
    // just update the constant
    existingConstraint.constant = layoutConstraint.constant;
    self.layoutConstraint = existingConstraint;
} else {
    [self.installedView addConstraint:layoutConstraint];
    self.layoutConstraint = layoutConstraint;
    [firstLayoutItem.mas_installedConstraints addObject:self];
}

通過上面代碼里的條件判斷可以看出,如果有設置相對的那個視圖就用先前提到的那個 mas_closestCommonSuperview 方法去找兩視圖的共同父視圖,不然如果只設置了高寬,就把約束加到當前視圖上,其它情況就加到當前視圖的父視圖上。

 

來自:http://ming1016.github.io/2018/04/07/read-snapkit-and-masonry-source-code/

 

 本文由用戶 vip60402 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!