Masonry 原始碼解讀(下)

小橘爺發表於2018-05-04

前言

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

設定約束不相等性

Masonry 中為我們準備了設定約束不相等時的方法:

- (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 引數不同。

- (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
複製程式碼

以其中一種為例:

#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, 這是行內函數的定義。引入行內函數的目的是為了解決程式中函式呼叫的效率問題。

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

在程式編譯時,編譯器將程式中出現的行內函數的呼叫表示式用行內函數的函式體來進行替換。顯然,這種做法不會產生轉去轉回的問題,但是由於在編譯時將函式休中的程式碼被替代到程式中,因此會增加目標程式程式碼量,進而增加空間開銷,而在時間代銷上不象函式呼叫時那麼大,可見它是以目的碼的增加為代價來換取時間的節省。

因為每一個呼叫 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 原始碼解讀(下)

如果覺得我寫的還不錯,請關注我的微博@小橘爺,最新文章即時推送~

相關文章