Masonry源碼分析

語言: CN / TW / HK

Masonry源碼分析

版本:v1.1.0

github鏈接

引子

首先我們根據較為完整的使用,與NSLayoutConstraints作對比,然後再逐步分析Masonry實現自動佈局的過程。

objc UIView *superview = self.view;    UIView *view1 = [[UIView alloc] init];    view1.backgroundColor = UIColor.redColor;   [superview addSubview:view1];        //Masonry佈局    UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);   [view1 mas_makeConstraints:^(MASConstraintMaker *make) {        make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler        make.left.equalTo(superview.mas_left).with.offset(padding.left);        make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);        make.right.equalTo(superview.mas_right).with.offset(-padding.right);   }];        //NSLayoutConstraint佈局   [superview addConstraints:@[        //view1 constraints       [NSLayoutConstraint constraintWithItem:view1                                     attribute:NSLayoutAttributeTop                                     relatedBy:NSLayoutRelationEqual                                        toItem:superview                                     attribute:NSLayoutAttributeTop                                    multiplier:1.0                                      constant:padding.top], ​       [NSLayoutConstraint constraintWithItem:view1                                     attribute:NSLayoutAttributeLeft                                     relatedBy:NSLayoutRelationEqual                                        toItem:superview                                     attribute:NSLayoutAttributeLeft                                    multiplier:1.0                                      constant:padding.left], ​       [NSLayoutConstraint constraintWithItem:view1                                     attribute:NSLayoutAttributeBottom                                     relatedBy:NSLayoutRelationEqual                                        toItem:superview                                     attribute:NSLayoutAttributeBottom                                    multiplier:1.0                                      constant:-padding.bottom], ​       [NSLayoutConstraint constraintWithItem:view1                                     attribute:NSLayoutAttributeRight                                     relatedBy:NSLayoutRelationEqual                                        toItem:superview                                     attribute:NSLayoutAttributeRight                                    multiplier:1                                      constant:-padding.right],     ]];

從佈局代碼量可以看出,使用Masonry佈局是簡潔很多的,可讀性也較高。

當然,Masonry是通過鏈式調用的方式簡化了代碼,實際上最後也是轉化為NSLayoutConstraint佈局(下文會具體分析)。

分析過程

我們從上面的mas_makeConstraints佈局角度出發,分析Masonry如何做到如此簡潔地完成一個view的約束。主要分析步驟為

一、給誰做約束

二、如何組成約束

三、如何完成約束

一、給誰做約束

通過查看mas_makeConstraints方法的定義可知,約束對象類型為MAS_VIEW,在MASUtilities.h文件中可以找到其宏定義。

``` objc

if TARGET_OS_IPHONE || TARGET_OS_TV

#define MAS_VIEW UIView

elif TARGET_OS_MAC

#define MAS_VIEW NSView

endif

```

這裏我們僅針對iOS架構來分析,可以得出:是給UIView做約束

二、如何組成約束

我們先來分析這一句代碼,研究各個鏈式調用是如何最終組成一句約束的,我們可以先從make入手

objc make.left.equalTo(superview.mas_left).with.offset(padding.left);

(1)make是何物?

mas_makeConstraints調用得知,make為MASConstraintMaker類型對象,可以理解為協助創建約束的類。那其對應的topleftright等等屬性是什麼概念呢?

(2)MASConstraintMaker的topleft等屬性

① 查看定義得知,這些屬性定位在MASConstraintMaker類中,返回了MASConstraint類型

objc @property (nonatomic, strong, readonly) MASConstraint *left; - (MASConstraint *)left {      return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft]; }    - (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {    return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute]; }

②以left為例,實際的操作為,構造一個MASViewConstraint(繼承自MASConstraint)對象並返回,其firstViewAttributeitem為當前做約束的view,layoutAttribute為對應的NSLayoutAttributeLeft

objc - (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; }

如上我們得到MASConstraint類型對象,可以算是確認了需要創建約束的具體屬性,然後將該約束添加到constraints數組屬性中。

接下來就是equalTo(superview.mas_left).with.offset(padding.left);操作,我們先來研究MASConstraintequalTo(...) / mas_equalTo(...)操作。

(3)MASViewConstraint的equalTo(...) / mas_equalTo(...)方法

``` objc

define mas_equalTo(...)                 equalTo(MASBoxValue((VA_ARGS)))

define equalTo(...)                     mas_equalTo(VA_ARGS)

​ - (MASConstraint * (^)(id))mas_equalTo {    return ^id(id attribute) {        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);   }; } ​ //MASViewConstraint的equalToWithRelation方法 - (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;       }   }; } ```

該方法返回了一個block,傳入attribute返回MASConstraint類型對象。

此方法確認了MASViewConstraint的layoutRelation為NSLayoutRelationEqual, 而傳入的參數(superview.mas_left)即為secondViewAttribute

(4)MASViewConstraintwith/and方法

objc - (MASConstraint *)with {    return self; } ​ - (MASConstraint *)and {    return self; }

這兩個方法返回了self,其實的起到了連接語義的作用,一般可以省略。接下來我們看看offset方法

(5)MASViewConstraintoffset(...)/mas_offset(...)方法

``` objc

define mas_offset(...)                 valueOffset(MASBoxValue((VA_ARGS)))

define offset(...)                     mas_offset(VA_ARGS)

​ - (MASConstraint * (^)(NSValue value))valueOffset {    return ^id(NSValue offset) {        NSAssert([offset isKindOfClass:NSValue.class], @"expected an NSValue offset, got: %@", offset);       [self setLayoutConstantWithValue:offset];        return self;   }; } ​ - (void)setLayoutConstantWithValue:(NSValue )value {    if ([value isKindOfClass:NSNumber.class]) {        self.offset = [(NSNumber )value doubleValue];   } else if (strcmp(value.objCType, @encode(CGPoint)) == 0) {        CGPoint point;       [value getValue:&point];        self.centerOffset = point;   } else if (strcmp(value.objCType, @encode(CGSize)) == 0) {        CGSize size;       [value getValue:&size];        self.sizeOffset = size;   } else if (strcmp(value.objCType, @encode(MASEdgeInsets)) == 0) {        MASEdgeInsets insets;       [value getValue:&insets];        self.insets = insets;   } else {        NSAssert(NO, @"attempting to set layout constant with unsupported value: %@", value);   } } ​ - (void)setOffset:(CGFloat)offset {    self.layoutConstant = offset; } ```

可以看出offset方法會根據傳入的類型,去設置offset/centerOffset/sizeOffset/insets屬性。而設置這些屬性,最後都會確認對應約束屬性的layoutConstant

(6)MASViewConstraintmultipliedBy(...)方法

objc - (MASConstraint * (^)(CGFloat))multipliedBy {    return ^id(CGFloat multiplier) {        NSAssert(!self.hasBeenInstalled,                 @"Cannot modify constraint multiplier after it has been installed");                self.layoutMultiplier = multiplier;        return self;   }; }

因為MASViewConstraint對象初始化時layoutMultiplier為1.0時,所以layoutMultiplier為1.0時常常不寫。這裏也就是將layoutMultiplier屬性賦值。

(7)總結

objc make.left.equalTo(superview.mas_left).with.offset(padding.left);

至此,這一整句代碼的流程就可以總結為:

設置MASViewConstraint對象的以下屬性

objc firstViewAttribute.item = view1 firstViewAttribute.layoutAttribute = NSLayoutAttributeLeft layoutRelation = NSLayoutRelationEqual secondViewAttribute.item = superview secondViewAttribute.layoutAttribute = NSLayoutAttributeLeft layoutMultiplier = 1.0 layoutConstant = padding.left

對比以下代碼,有沒有感覺和NSLayoutConstraint寫約束時的參數一模一樣

objc [NSLayoutConstraint constraintWithItem:view1                             attribute:NSLayoutAttributeLeft                             relatedBy:NSLayoutRelationEqual                                toItem:superview                             attribute:NSLayoutAttributeLeft                            multiplier:1.0                              constant:padding.left],

有了這些屬性之後,Masonry自然就可以通過mas_makeConstraints/mas_updateConstraints/mas_remakeConstraints方法進行添加/更新/移除後添加相應約束。

接下來我們通過分析mas_makeConstraints方法,瞭解Masonry如何完成約束

三、如何完成約束

我們先來看mas_makeConstraints的實現

objc - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { //translatesAutoresizingMaskIntoConstraints 默認情況下,視圖上的自動調整掩碼會產生完全確定的約束視圖的位置。這允許自動佈局系統跟蹤視圖的幀。佈局是手動控制的(例如,通過-setFrame:)。當您選擇通過添加自己的約束來使用自動佈局來定位視圖時,您必須將此屬性設置為NO    self.translatesAutoresizingMaskIntoConstraints = NO;    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];    block(constraintMaker);    return [constraintMaker install]; }

可以看到該方法首先創建了constraintMaker,然後調用block,也就是設置並添加了block中的每一條約束到constraints中,然後返回[constraintMaker install],我們就來研究MASConstraintMaker的install方法

(1)MASConstraintMaker的install方法

objc - (NSArray *)install { //removeExisting在mas_remakeConstraints情況下為YES    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; }

①首先,如果調用的是mas_remakeConstraints會先卸載之前的約束

②將constraints中的每條約束執行install,然後清空constraints。此時我們應該查看MASConstraint的install方法

(2)MASConstraint的install方法

objc - (void)install {    //...        MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;    NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;    MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;    NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute; ​    // alignment attributes must have a secondViewAttribute    // therefore we assume that is refering to superview    // eg make.left.equalTo(@10)    if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {        secondLayoutItem = self.firstViewAttribute.view.superview;        secondLayoutAttribute = firstLayoutAttribute;   }        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;        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];   } }

代碼較多,這裏就簡單總結一下。

① 如果secondViewAttribute =nil,且約束屬性不為width和height時,默認設置secondLayoutItem為view.superview、secondLayoutAttribute為firstViewAttribute.layoutAttribute。eg make.left.equalTo(@10)

②用對應的屬性構造了MASLayoutConstraint約束對象

③確定要設置安裝約束的installedView為 (1)最近的公共祖先view (2)寬高屬性約束時,為firstViewAttribute.view (3)firstViewAttribute.view.superview

④判斷是否是更新約束,是則更新,不是則installedView添加該約束 ([self.installedView addConstraint:layoutConstraint];)

因為MASLayoutConstraint繼承自NSLayoutConstraint,所以這裏installedView (MAS_VIEW類型)的addConstraint方法,其實也就是UIView的addConstraint方法,也就成功地為installedView添加上了該約束。

//Adds a constraint on the layout of the receiving view or its subviews. - (void)addConstraint:(NSLayoutConstraint *)constraint API_AVAILABLE(ios(6.0)); // This method will be deprecated in a future release and should be avoided.  Instead, set NSLayoutConstraint's active property to YES.

總結

(1)Masonry添加約束流程總結

通過block中的每一行創建一個約束MASViewConstraint,添加到約束數組constraints中,通過install方法逐條添加到對應的installedView上。

(2)equalTo和mas_equalTo、offset和mas_offset功能基本相同,只不過mas_equalTomas_offset添加了對數字字面量的支持(即_MASBoxValue)。

objc       make.height.equalTo(123.0); //error: Passing 'double' to parameter of incompatible type '__strong id'       make.height.mas_equalTo(123.0);

(3)如果約束屬性不為寬高,且equalTo/mas_equalTo的對象是父view的相同屬性,可省略mas_equalTo。(即不寫equalTo/mas_equalTo,默認相對父view佈局)

objc   [self.view addSubview:view1];   [view1 mas_makeConstraints:^(MASConstraintMaker *make) {        //因為view1.superview = self.view, 所以以下兩句代碼效果相同        make.right.mas_equalTo(self.view.mas_right).mas_equalTo(-20);        make.right.mas_equalTo(-20);   }];

「其他文章」