Masonry 源碼解讀(下)

前言

書接上文,咱們在上一篇文章中已經解解讀了 Masonry 框架中最核心的功能是如何實現的,接下來再看一下另外的一些點。git

設置約束不相等性

Masonry 中爲咱們準備了設置約束不相等時的方法:github

- (MASConstraint * (^)(id attr))greaterThanOrEqualTo;

- (MASConstraint * (^)(id attr))lessThanOrEqualTo;
複製代碼

greaterThanOrEqualTo 爲例,看一下它和 equalTo 的區別:數組

- (MASConstraint * (^)(id))greaterThanOrEqualTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationGreaterThanOrEqual);
    };
}
複製代碼

equalTo 別無二致,一樣是調用了 equalToWithRelation 方法,只不過 relation 參數不一樣。bash

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        if ([attribute isKindOfClass:NSArray.class]) {
            ...
        } else {
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            self.layoutRelation = relation; // 在這裏設置的
            self.secondViewAttribute = attribute;
            return self;
        }
    };
}
複製代碼

autoboxing

由上面的代碼咱們能夠看到,傳遞給 equalToWithRelation 方法的 attribute 參數是一個 id 類型的對象,這意味每次調用 .equalTo 方法時,須要對純數字的參數進行包裝。如果簡單的數字約束還好,可是針對 size 等須要傳遞結構體才能解決的約束,就顯得很繁瑣了。Masonry 爲咱們提供了一些宏用來解決這個問題:框架

#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
#define mas_greaterThanOrEqualTo(...) greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_lessThanOrEqualTo(...) lessThanOrEqualTo(MASBoxValue((__VA_ARGS__)))

#define mas_offset(...) valueOffset(MASBoxValue((__VA_ARGS__)))


#ifdef MAS_SHORTHAND_GLOBALS

#define equalTo(...) mas_equalTo(__VA_ARGS__)
#define greaterThanOrEqualTo(...) mas_greaterThanOrEqualTo(__VA_ARGS__)
#define lessThanOrEqualTo(...) mas_lessThanOrEqualTo(__VA_ARGS__)

#define offset(...) mas_offset(__VA_ARGS__)

#endif
複製代碼

以其中一種爲例:less

#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
複製代碼

能夠看出,mas_equalTo 是經過每次調用 equalTo 方法時,對參數調用 MASBoxValue() 這個宏來解決 autoboxing 問題的。而 MASBoxValue() 宏的定義是:函數

#define MASBoxValue(value) _MASBoxValue(@encode(__typeof__((value))), (value))
複製代碼

轉而調用了一個名爲 _MASBoxValue 的函數:性能

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;
}
複製代碼

在函數定義的最開始使用了 static inline, 這是內聯函數的定義。引入內聯函數的目的是爲了解決程序中函數調用的效率問題。ui

函數調用會帶來下降效率的問題,由於調用函數實際上將程序執行順序轉移到函數所存放在內存中某個地址,將函數的程序內容執行完後,再返回到轉去執行該函數前的地方。這種轉移操做要求在轉去前要保護現場並記憶執行的地址,轉回後先要恢復現場,並按原來保存地址繼續執行。所以,函數調用要有必定的時間和空間方面的開銷,因而將影響其效率。特別是對於一些函數體代碼不是很大,但又頻繁地被調用的函數來說,解決其效率問題更爲重要。引入內聯函數實際上就是爲了解決這一問題。編碼

在程序編譯時,編譯器將程序中出現的內聯函數的調用表達式用內聯函數的函數體來進行替換。顯然,這種作法不會產生轉去轉回的問題,可是因爲在編譯時將函數休中的代碼被替代到程序中,所以會增長目標程序代碼量,進而增長空間開銷,而在時間代銷上不象函數調用時那麼大,可見它是以目標代碼的增長爲代價來換取時間的節省。

由於每個調用 mas_ 前綴開頭的宏都會調用 _MASBoxValue 函數,因此在這裏進行了 static inline 的處理。

_MASBoxValue 函數的參數聲明爲 const char *type, ... 說明其接受的是可變參數。在使用 MASBoxValue(value)_MASBoxValue 函數進行調用時,傳入的參數只有兩個:值的類型編碼(@encode(__typeof__((value))))和值(value)。

@encode@編譯器指令之一,返回一個給定類型編碼爲一種內部表示的字符串(例如,@encode(int) → i),相似於 ANSI Ctypeof 操做。蘋果的 Objective-C 運行時庫內部利用類型編碼幫助加快消息分發。詳細內容能夠參考 NSHipster 這篇博客:Type Encodings

Objective-C 中對於可變參數的處理是依賴一組宏來實現的,經過代碼來看更直觀一點:

static inline id _MASBoxValue(const char *type, ...) {
    va_list v; // 指向變參的指針
    va_start(v, type); // 使用第一個參數來初使化 v 指針
    id obj = nil; // 聲明一個 id 類型的指針(用於存儲返回值)
    if (strcmp(type, @encode(id)) == 0) { // strcmp() 函數是用來比較字符串的,若是相同則返回 0
        id actual = va_arg(v, id); // 返回可變參數,va_arg 第二個參數爲可變參數類型,若是有多個可變參數,依次調用可獲取各個參數
        obj = actual; // 因爲傳入的自己就是 id 類型,因此不須要類型轉換
    } else if (strcmp(type, @encode(CGPoint)) == 0) { // 若是匹配 CGPoint 類型
        CGPoint actual = (CGPoint)va_arg(v, CGPoint); // 取出可變參數
        obj = [NSValue value:&actual withObjCType:type]; // 經過 NSValue 對基本數據類型作一次包裝
    } ... // 以後的分支同理。
    ...
    va_end(v); // 結束可變參數的獲取
    return obj; // 返回轉換後的結果
}
複製代碼

NSArray

傳入的參數不只能夠是單個值,也能夠是數組:

make.height.equalTo(@[view1.mas_height, view2.mas_height]);
複製代碼

內部實現爲:

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        if ([attribute isKindOfClass:NSArray.class]) {
            NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
            NSMutableArray *children = NSMutableArray.new;
            for (id attr in attribute) {
                MASViewConstraint *viewConstraint = [self copy];
                viewConstraint.layoutRelation = relation;
                viewConstraint.secondViewAttribute = attr;
                [children addObject:viewConstraint];
            }
            MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
            compositeConstraint.delegate = self.delegate;
            [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
            return compositeConstraint;
        } else {
            ...
        }
    };
}
複製代碼

對於數組類型的參數 attribute 參數,會落入第一個分支,將數據中的參數一一拆出來,分別生成 MASViewConstraint 類型的對象,再經過這些對象來初始化一個 MASCompositeConstraint 類型的對象(compositeConstraint),接下來咱們看一下 MASCompositeConstraint 的初始化方法:

- (id)initWithChildren:(NSArray *)children;
複製代碼
- (id)initWithChildren:(NSArray *)children {
    self = [super init];
    if (!self) return nil;

    _childConstraints = [children mutableCopy];
    for (MASConstraint *constraint in _childConstraints) {
        constraint.delegate = self;
    }

    return self;
}
複製代碼

MASViewConstraint 類型相似,都是繼承與 MASConstraint 類的模型類,用 @property (nonatomic, strong) NSMutableArray *childConstraints; 屬性來保存一組約束,這其中的每一個約束的 delegate 都是 self

再以後,調用 [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint]; 方法:

- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint {
    NSUInteger index = [self.constraints indexOfObject:constraint];
    NSAssert(index != NSNotFound, @"Could not find constraint %@", constraint);
    [self.constraints replaceObjectAtIndex:index withObject:replacementConstraint];
}
複製代碼

去替換數組中對應存儲的約束。

在對視圖施加約束的時候也有所不一樣,MASCompositeConstraint 是經過遍歷 childConstraints 中全部的實例挨個 install

- (void)install {
    for (MASConstraint *constraint in self.childConstraints) {
        constraint.updateExisting = self.updateExisting;
        [constraint install];
    }
}
複製代碼

優先級

約束是能夠設置優先級的,從 0-1000,不過一般狀況下也不須要這麼多個等級,讓咱們先來看一下 Masonry 中是如何實現這一功能的:

make.left.greaterThanOrEqualTo(label.mas_left).with.priorityLow();
複製代碼

經過調用 .priorityLow() 方法,就爲這條約束設置了低優先級,下面是該方法的聲明和實現:

- (MASConstraint * (^)(void))priorityLow;
複製代碼
- (MASConstraint * (^)(void))priorityLow {
    return ^id{
        self.priority(MASLayoutPriorityDefaultLow);
        return self;
    };
}
複製代碼

MASLayoutPriorityDefaultLow 是預先定義好的一組常量之一,定義以下,是經過預編譯宏對跨平臺的優先級作了一層封裝:

#if TARGET_OS_IPHONE || TARGET_OS_TV

    #import <UIKit/UIKit.h>
    #define MAS_VIEW UIView
    #define MAS_VIEW_CONTROLLER UIViewController
    #define MASEdgeInsets UIEdgeInsets

    typedef UILayoutPriority MASLayoutPriority;
    static const MASLayoutPriority MASLayoutPriorityRequired = UILayoutPriorityRequired;
    static const MASLayoutPriority MASLayoutPriorityDefaultHigh = UILayoutPriorityDefaultHigh;
    static const MASLayoutPriority MASLayoutPriorityDefaultMedium = 500;
    static const MASLayoutPriority MASLayoutPriorityDefaultLow = UILayoutPriorityDefaultLow;
    static const MASLayoutPriority MASLayoutPriorityFittingSizeLevel = UILayoutPriorityFittingSizeLevel;

#elif TARGET_OS_MAC

    #import <AppKit/AppKit.h>
    #define MAS_VIEW NSView
    #define MASEdgeInsets NSEdgeInsets

    typedef NSLayoutPriority MASLayoutPriority;
    static const MASLayoutPriority MASLayoutPriorityRequired = NSLayoutPriorityRequired;
    static const MASLayoutPriority MASLayoutPriorityDefaultHigh = NSLayoutPriorityDefaultHigh;
    static const MASLayoutPriority MASLayoutPriorityDragThatCanResizeWindow = NSLayoutPriorityDragThatCanResizeWindow;
    static const MASLayoutPriority MASLayoutPriorityDefaultMedium = 501;
    static const MASLayoutPriority MASLayoutPriorityWindowSizeStayPut = NSLayoutPriorityWindowSizeStayPut;
    static const MASLayoutPriority MASLayoutPriorityDragThatCannotResizeWindow = NSLayoutPriorityDragThatCannotResizeWindow;
    static const MASLayoutPriority MASLayoutPriorityDefaultLow = NSLayoutPriorityDefaultLow;
    static const MASLayoutPriority MASLayoutPriorityFittingSizeCompression = NSLayoutPriorityFittingSizeCompression;

#endif
複製代碼

self.priority() 就是簡單的賦值:

- (MASConstraint * (^)(MASLayoutPriority))priority {
    return ^id(MASLayoutPriority priority) {
        NSAssert(!self.hasBeenInstalled,
                 @"Cannot modify constraint priority after it has been installed");
        
        self.layoutPriority = priority;
        return self;
    };
}
複製代碼

固然咱們也能夠爲每一條約束設置咱們想要的優先級,以下所示,原理相同就不在贅述了:

make.top.equalTo(label.mas_top).with.priority(600);
複製代碼

經過另外一個視圖爲當前視圖設置約束

設置約束不必定每次都要傳入數值,也能夠依據別的視圖來設置約束:

make.edges.equalTo(view2);
複製代碼

因爲傳入的並不是 NSArray 類型的參數,因而一樣會落入 equalToWithRelationelse 分支:

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        if ([attribute isKindOfClass:NSArray.class]) {
            ...
        } else {
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute; // 在這裏將 view2 傳入
            return self;
        }
    };
}
複製代碼

不一樣類型的 attribute 是在 set 方法裏處理的,讓咱們看一下 self.secondViewAttribute 屬性的 set 方法:

- (void)setSecondViewAttribute:(id)secondViewAttribute {
    if ([secondViewAttribute isKindOfClass:NSValue.class]) {
        [self setLayoutConstantWithValue:secondViewAttribute];
    } else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
        _secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute]; // 重點在這行
    } else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
        MASViewAttribute *attr = secondViewAttribute;
        if (attr.layoutAttribute == NSLayoutAttributeNotAnAttribute) {
            _secondViewAttribute = [[MASViewAttribute alloc] initWithView:attr.view item:attr.item layoutAttribute:self.firstViewAttribute.layoutAttribute];;
        } else {
            _secondViewAttribute = secondViewAttribute;
        }
    } else {
        NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
    }
}
複製代碼

在傳入的 secondViewAttributeview 的情形下,會經過 self.firstViewAttribute.layoutAttribute 來從 view2 中提取出對應的約束數值,建立新的 MASViewAttribute 賦值給 _secondViewAttribute

更新約束

在我看來 Masonry 相對於原生和其餘大多數 AutoLayout 框架最大的優勢在於,當你想更新約束的時候,不須要持有對應約束的引用,而是調用 mas_updateConstraints(用於更新約束) 或 mas_remakeConstraints(用於重設約束)便可,下面看一下這兩個方法的聲明和實現:

mas_updateConstraints

- (NSArray *)mas_updateConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;
複製代碼
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    constraintMaker.updateExisting = YES;
    block(constraintMaker);
    return [constraintMaker install];
}
複製代碼

相較於 mas_makeConstraints 區別在於 constraintMaker.updateExisting = YES; ,下面看一下 updateExisting 這個標誌位對於施加約束的影響:

- (NSArray *)install {
    if (self.removeExisting) {
        ...
    }
    NSArray *constraints = self.constraints.copy;
    for (MASConstraint *constraint in constraints) {
        constraint.updateExisting = self.updateExisting;
        [constraint install];
    }
    [self.constraints removeAllObjects];
    return constraints;
}
複製代碼

install 方法中,會對 constraints 中的每個 constraintupdateExisting 的標誌位都設爲 YES,再來看一下 MASConstraintinstall 方法:

- (void)install {
    ...

    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];
    }
}
複製代碼

若是 updateExistingYES,就經過 layoutConstraintSimilarTo 方法來嘗試拿到以前已經 install 過的相似約束,若是能拿到,那麼就只是更改了原有約束的 constant,若是沒有,就正常添加約束。下面是 layoutConstraintSimilarTo 方法的實現:

- (MASLayoutConstraint *)layoutConstraintSimilarTo:(MASLayoutConstraint *)layoutConstraint {
    // check if any constraints are the same apart from the only mutable property constant

    // go through constraints in reverse as we do not want to match auto-resizing or interface builder constraints
    // and they are likely to be added first.
    for (NSLayoutConstraint *existingConstraint in self.installedView.constraints.reverseObjectEnumerator) {
        if (![existingConstraint isKindOfClass:MASLayoutConstraint.class]) continue;
        if (existingConstraint.firstItem != layoutConstraint.firstItem) continue;
        if (existingConstraint.secondItem != layoutConstraint.secondItem) continue;
        if (existingConstraint.firstAttribute != layoutConstraint.firstAttribute) continue;
        if (existingConstraint.secondAttribute != layoutConstraint.secondAttribute) continue;
        if (existingConstraint.relation != layoutConstraint.relation) continue;
        if (existingConstraint.multiplier != layoutConstraint.multiplier) continue;
        if (existingConstraint.priority != layoutConstraint.priority) continue;

        return (id)existingConstraint;
    }
    return nil;
}
複製代碼

簡單的遍歷 constraints 數組裏的全部約束,並比較屬性是否相同。

mas_remakeConstraints

- (NSArray *)mas_remakeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;
複製代碼
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    constraintMaker.removeExisting = YES;
    block(constraintMaker);
    return [constraintMaker install];
}
複製代碼

相較於 mas_makeConstraints 區別在於會簡單粗暴的把全部視圖上試駕過的約束挨個拿出來 uninstall,因而可知這個方法對於性能的影響是蠻大的,要慎用:

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

installedConstraintsForView 方法就是簡單的返回 mas_installedConstraints 中的全部對象,在此再也不贅述:

+ (NSArray *)installedConstraintsForView:(MAS_VIEW *)view {
    return [view.mas_installedConstraints allObjects];
}
複製代碼

原文地址:Masonry 源碼解讀(下)

若是以爲我寫的還不錯,請關注個人微博@小橘爺,最新文章即時推送~

相關文章
相關標籤/搜索