Masonry源碼分析

作者:代培
地址:http://daipei.me/posts/source_code_analysis_of_masonry/
轉載請註明出處
我的博客搬家了,新博客地址:daipei.me

AutoLayout是個好東西,但是官方的API實在不好用,Masonry應時而生爲AutoLayout提供了簡潔的接口,我們的項目中的佈局全部都是用Masonry,可以說離了它有些寸步難行。

Masonry使用起來是十分簡單的:

[self.aView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view);
        make.top.equalTo(self.view.mas_top).offset(100);
        make.width.height.mas_equalTo(200);
}];

從mas_makeConstraints開始

Masonry中使用最多的就是mas_makeConstraints:這個方法,這是用於第一次添加約束時使用的方法,關於設置約束,一共有三種方法:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;

從方法名可以很容易看出這三個方法分別是什麼作用,第二個方法是更新約束時使用的,第三個方法是重新添加約束時使用的,也就是以前的約束不需要時完全重新設置約束,需要注意的是如果要重新設置約束一定要用第三個方法,連續調用第一個方法容易引起約束的衝突,雖然程序不一定會crash。

這三個方法會返回一個數組,這個數組中是新添加的約束,不過我從來沒有用到過這個返回值,如果不是看源碼,其實都不知道這些方法是有返回值的。

下面看一下mas_makeConstraints:的實現:

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

首先將translatesAutoresizingMaskIntoConstraints這個屬性設置爲NO

By default, the autoresizing mask on a view gives rise to constraints that fully determine

the view’s position. This allows the auto layout system to track the frames of views whose

layout is controlled manually (through -setFrame:, for example).

When you elect to position the view using auto layout by adding your own constraints,

you must set this property to NO. IB will do this for you.

這句話的意思大致就是最終系統都是用constraints的方式來組織視圖,但是如果這個設置爲YES系統會將你設置的Frame之類的屬性轉換爲constraints,但是如果你要自己添加約束,也就是如果你要使用AutoLayout的話,就必須將這個屬性設置爲NO。如果你用InterfaceBuilder的AutoLayout,會自動將這個屬性設置爲NO

第二步是用當前View來實例化一個MASConstraintMaker類型的maker,這裏的self是調用mas_makeConstraints:的view。

第三步執行傳入的block中的代碼,將剛剛實例化的maker傳入block,用於配置這個maker

Note:曾經產生過一個疑惑,就是我們在使用Masonry進行佈局的時候,在block中都是直接引用self的,爲什麼不會產生循環引用?看完源碼就明白了其中的原因,首先這個block肯定是強引用了self的,假設我們是在一個VC中進行的佈局(大多數情況下是這樣),這個self就是VC,然後這個VC強引用了調用Masonry接口的View,但是這個view沒有引用這個block,事實上這個block沒有被任何對象引用,所以這個block在執行完以後就會被釋放了,block引用了self,但是self沒有直接或間接引用block,所以不會存在循環引用的問題。

最後向這個maker發送install的消息,將用戶設置的約束添加到view上。

MASConstraintMaker

首先看看它的初始化方法:

- (id)initWithView:(MAS_VIEW *)view {
    self = [super init];
    if (!self) return nil;

    self.view = view;
    self.constraints = NSMutableArray.new;

    return self;
}

這裏的MAS_VIEW是一個宏:

#if TARGET_OS_IPHONE || TARGET_OS_TV
    #define MAS_VIEW UIView
#elif TARGET_OS_MAC
    #define MAS_VIEW NSView
#endif

這個裏使用宏的意圖比較明顯,Masonry希望不僅僅支持iOS,同時也支持tvOS和macOS

maker保持了當前view的引用,當然這裏的引用是弱引用,雖然強引用也不會引起循環引用,但是這裏弱引用其實和合理,因爲如果view都不存在了,這個maker也沒有存在的必要了,view不應該因爲maker的引用而引用計數加1。

同時maker實例化了一個可變數組constraints,這個數組中保存的就是要添加到當前view的約束。

我們看一下當調用make.left時會發生什麼:

- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    ...
    if (!constraint) {
        newConstraint.delegate = self;
        [self.constraints addObject:newConstraint];
    }
    return newConstraint;
}

我們看的最終調用的是-constraint: addConstraintWithLayoutAttribute:這個方法,我刪去了其中暫時無關的代碼,不過刪去的代碼在後面還會提到。

因爲傳入的constraintnil,所以直接進入這個if判斷,在這個判斷中將新生成的constraint的代理設爲maker,並將其加入self.constraints這個數組中。

最後將新生成的constraint返回。

MASConstraint

在上一節中make.left就是返回了一個MASConstraint對象,下面看一下make.left.equalTo(self.view)這句話是怎樣調用的:

// MASConstraint.h
- (MASConstraint * (^)(id attr))equalTo;

在MASConstraint.h中有這樣一個接口,我看了半天才搞明白這是一個什麼函數,這是一個返回值爲block的函數,返回的這個block的返回值是MASConstraint,接受一個id類型的參數,我們看它的調用方式:.equalTo(self.view),這其實比較奇怪,因爲我們知道OC中方法是不能用點語法調用的,只有屬性纔可以,所以其實這裏可以把equalTo理解爲一個block類型的屬性,讓這個方法實際上就是這個block的getter方法。

這個方法中的實現是這樣的:

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

這裏直接進入了-equalToWithRelation這個方法,這是一個抽象方法,由MASConstraint的兩個子類來實現MASViewConstraintMASCompositeConstraint

Note:這裏的抽象方法用一種比較有趣的方法來實現,Masonry定義了一個宏叫做MASMethodNotImplemented(),這個宏會拋出一個異常,如果錯誤的調用了這個抽象方法在運行時就是導致crash,OC不支持抽象方法,但是這裏用了一種獨特的方式實現抽象方法,還是挺值得學習的。

在這個方法中傳入了一個relation的參數比如上面代碼中傳入的NSLayoutRelationEqual,這個參數在後面的佈局中是會用到的。

調用不同的方法傳入的參數就不一樣,比如-greaterThanOrEqualTo傳入的就是NSLayoutRelationGreaterThanOrEqual,而lessThanOrEqualTo傳入的就是NSLayoutRelationLessThanOrEqual

MASViewConstraint

先看一下MASConstraint這個相對簡單的子類,我們關注這個子類是如何實現上述的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;
        }
    };
}

我暫時省略了第一個判斷中的內容,在else分支中,首先斷言這個constraint沒有被重定義。

然後設置layoutRelation,在setter方法中將上面的self.hasLayoutRelation標記爲YES,這裏的relation在前面說過

最後設置secondViewAttribute,看到second自然會想到會不會有first,確實是有的,first就是當前view的attribute,其實這個理解起來不難,一個約束就是描述兩個view之間的關係(尺寸約束除外),所以這個MASViewConstraint最重要的三個屬性就是:firstViewAttributesecondViewAttributelayoutRelation

這個secondViewAttributesetter方法裏內容很多:

- (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]) {
        _secondViewAttribute = secondViewAttribute;
    } else {
        NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
    }
}

這裏的secondViewAttribute有三種類型,分別是NSValueMAS_VIEWMASViewAttribute,我可以舉三個例子對應這裏的三種情況:

make.width.mas_equalTo(100);
make.left.equalTo(self.view);
make.left.equalTo(self.view.mas_left);

其中第二行和第三行是等價的,從setter的代碼裏可以看出爲什麼第二個例子和第三個例子是等價的,因爲當傳入的secondViewAttribute的類型是MAS_VIEW類型時,首先會實例化一個MASViewAttribute的對象,該對象使用傳入的ViewfirstViewlayoutAttribute進行配置,所以當傳入self.view時會和當前viewattribute保持一致使用left

第三行傳入的self.view.mas_left直接就是一個MASViewAttribute對象,直接賦值即可。

MASViewAttribute

MASViewAttribute保存三樣東西:MAS_VIEW類型的viewid類型的itemNSLayoutAttribute類型的layoutAttribute

其初始化方法有兩個:

- (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute {
    self = [self initWithView:view item:view layoutAttribute:layoutAttribute];
    return self;
}

- (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute {
    self = [super init];
    if (!self) return nil;

    _view = view;
    _item = item;
    _layoutAttribute = layoutAttribute;

    return self;
}

第二個方法中的item在一般情況下和第一個view是同一個對象,當使用Masonry的VC相關的接口時是指id<UILayoutSupport>

最後就是使用兩個view的MASViewAttribute來構建constraint並添加到相關view上。

Install

當配置好maker後,就是install的步驟,直接看install中的部分源代碼:

- (void)install {
    ...
    MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
    NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
    MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
    NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
    ...
    MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];
    ...
    MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
    self.installedView = closestCommonSuperview;
    ...
    [self.installedView addConstraint:layoutConstraint];
    self.layoutConstraint = layoutConstraint;
    [firstLayoutItem.mas_installedConstraints addObject:self];
}

這裏只關注其主要的邏輯,首先根據兩個viewAttribute用系統API生成一個layoutConstraint對象,然後調用-mas_closestCommonSuperview:方法獲取兩個view的最近父view,最後在這個父view添加剛纔生成的約束。

Note:mas_closestCommonSuperview:的邏輯是先固定一個view,然後向上遍歷另一個view的父view,如果找到相同view就退出,沒找到再固定第一個view的父view,繼續遍歷第二個view的父view,直到找到或是遍歷完全部。

這其中有很多判斷,會分成很多種情況,我這裏講的是最通常的那一種情況。

最後會將該constraint保存起來,同時將自身加入第一個viewinstalledConstraints的數組中

至此整個約束的添加邏輯就完成了。

MASCompositeConstraint

前面在說到make.left這句話的執行情況時省略了一部分代碼,這裏就將其補回來。

在最開始的使用示例中有一句話make.width.height.mas_equalTo(200);,這句話最終會進入下面這個方法:

- (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;
    }
    ...
    return newConstraint;
}

當我們調用make.width時返回一個MASConstraint對象,這個對象也有leftrighttopbottomwidthheight等方法,當對make.width調用.height時,就會生成一個MASCompositeConstraint對象compositeConstraint,這個對象持有一個MASConstraint類型對象的數組,同時用compositeConstraint替換原來的constraint,Masonry使用MASCompositeConstraint來支持在一句話中同時設置多個約束的行爲。

我們再來看另一種情況make.top.left.bottom.right.equalTo(self.view);,在這句話中make.top.left返回的已經是一個MASCompositeConstraint對象了,這時調用.bottom時會進入MASCompositeConstraint-constraint: addConstraintWithLayoutAttribute:方法,這個方法的實現如下:

- (MASConstraint *)constraint:(MASConstraint __unused *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    id<MASConstraintDelegate> strongDelegate = self.delegate;
    MASConstraint *newConstraint = [strongDelegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
    newConstraint.delegate = self;
    [self.childConstraints addObject:newConstraint];
    return newConstraint;
}

它首先拿到自己的代理,這個代理實際上就是maker,我們看前面生成MASCompositeConstraint的代碼就可知道,然後調用maker-constraint: addConstraintWithLayoutAttribute:方法,這個方法的作用在此刻就十分單純,就是生成一個newConstraint

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    ...
    return newConstraint;
}

省略去的代碼都是在此情況下不會執行的部分。

然後將newConstraint代理設爲self,同時將其加入到self.childConstraints數組中,在後面安裝時,對這個數組中每個約束都發送install消息即可。

總結

第一次閱讀源代碼選擇了Masonry,因爲其代碼量不是很大,但其實跳坑裏去了。Masonry的源碼閱讀起來真的很吃力,各種擁有類似名字的變量,各種block的嵌套,各種抽象方法給閱讀帶來了困難。不過這絲毫不影響這個庫的優秀,它提供的接口如此簡潔,使用起來是如此的絲滑,完美的闡釋了那句:把複雜留給自己,把簡單留給別人。

鏈式語法

Masonry通過使用大量的block提供了簡潔的鏈式語法。MASConstraint這個類中的大部分方法的都返回一個block,而block的返回值都是MASConstraint,返回的MASConstraint對象又可以調用返回block的方法,正是通過這樣的方式使鏈式語法能夠工作。

抽象方法

通過定義宏:

#define MASMethodNotImplemented() \
    @throw [NSException exceptionWithName:NSInternalInconsistencyException \
                                   reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
                                 userInfo:nil]

來實現抽象方法真的很有創意。

宏的自動補全

我們看下面這段代碼:

#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__)))

@interface MASConstraint (AutoboxingSupport)

/**
 *  Aliases to corresponding relation methods (for shorthand macros)
 *  Also needed to aid autocompletion
 */
- (MASConstraint * (^)(id attr))mas_equalTo;
- (MASConstraint * (^)(id attr))mas_greaterThanOrEqualTo;
- (MASConstraint * (^)(id attr))mas_lessThanOrEqualTo;

/**
 *  A dummy method to aid autocompletion
 */
- (MASConstraint * (^)(id offset))mas_offset;

@end

當我們在使用mas_equalTo()這個方法時,實際上使用的是上面的宏,但是Masonry仍然提供了方法,這樣做的目的在註釋中寫的很清楚,爲了使宏能夠自動補全。

沒有循環引用

使用block時最讓人心煩的就是循環引用,Masonry使用block爲我們提供優雅的使用方式,並沒有帶來循環引用的弊端,真的是優秀。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章