作者:代培
地址: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:
這個方法,我刪去了其中暫時無關的代碼,不過刪去的代碼在後面還會提到。
因爲傳入的constraint
爲nil
,所以直接進入這個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
的兩個子類來實現MASViewConstraint
和MASCompositeConstraint
。
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
最重要的三個屬性就是:firstViewAttribute
、secondViewAttribute
、layoutRelation
。
這個secondViewAttribute
的setter
方法裏內容很多:
- (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
有三種類型,分別是NSValue
、MAS_VIEW
、MASViewAttribute
,我可以舉三個例子對應這裏的三種情況:
make.width.mas_equalTo(100);
make.left.equalTo(self.view);
make.left.equalTo(self.view.mas_left);
其中第二行和第三行是等價的,從setter
的代碼裏可以看出爲什麼第二個例子和第三個例子是等價的,因爲當傳入的secondViewAttribute
的類型是MAS_VIEW
類型時,首先會實例化一個MASViewAttribute
的對象,該對象使用傳入的View
和firstView
的layoutAttribute
進行配置,所以當傳入self.view
時會和當前view
的attribute
保持一致使用left
。
第三行傳入的self.view.mas_left
直接就是一個MASViewAttribute
對象,直接賦值即可。
MASViewAttribute
MASViewAttribute
保存三樣東西:MAS_VIEW
類型的view
、id
類型的item
、NSLayoutAttribute
類型的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
保存起來,同時將自身加入第一個view
的installedConstraints
的數組中
至此整個約束的添加邏輯就完成了。
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
對象,這個對象也有left
、right
、top
、bottom
、width
、height
等方法,當對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爲我們提供優雅的使用方式,並沒有帶來循環引用的弊端,真的是優秀。