Masonry 是 Objective-C 中用于自動(dòng)布局的第三方框架, 我們一般使用它來(lái)代替冗長(zhǎng), 繁瑣的 AutoLayout 代碼.
Masonry 的使用還是很簡(jiǎn)潔的:
[button mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view);
make.top.equalTo(self.view).with.offset(40);
make.width.equalTo(@185);
make.height.equalTo(@38);
}];
從 mas_makeConstraints: 開(kāi)始
其中最常用的方法就是
// View+MASAdditions.h
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block;
同樣, 也有用于更新和重新構(gòu)建約束的分類方法:
// View+MASAdditions.h
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;
Constraint Maker Block
我們以 mas_makeConstraints:
方法為入口來(lái)分析一下 Masonry 以及類似的框架(SnapKit)是如何工作的. mas_makeConstraints:
方法位于 UIView
的分類 MASAdditions
中.
Provides constraint maker block and convience methods for creating MASViewAttribute which are view + NSLayoutAttribute pairs.
這個(gè)分類為我們提供一種非常便捷的方法來(lái)配置 MASConstraintMaker
, 并為視圖添加 mas_left
mas_right
等屬性.
方法的實(shí)現(xiàn)如下:
// View+MASAdditions.m
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
因?yàn)?Masonry 是封裝的蘋(píng)果的 AutoLayout 框架, 所以我們要在為視圖添加約束前將 translatesAutoresizingMaskIntoConstraints
屬性設(shè)置為 NO
. 如果這個(gè)屬性沒(méi)有被正確設(shè)置, 那么視圖的約束不會(huì)被成功添加.
在設(shè)置 translatesAutoresizingMaskIntoConstraints
屬性之后,
- 我們會(huì)初始化一個(gè)
MASConstraintMaker
的實(shí)例. - 然后將 maker 傳入 block 配置其屬性.
- 最后調(diào)用 maker 的
install
方法為視圖添加約束.
MASConstraintMaker
MASConstraintMaker
為我們提供了工廠方法來(lái)創(chuàng)建 MASConstraint
. 所有的約束都會(huì)被收集直到它們最后調(diào)用 install
方法添加到視圖上.
Provides factory methods for creating MASConstraints. Constraints are collected until they are ready to be installed
在初始化 MASConstraintMaker
的實(shí)例時(shí), 它會(huì)持有一個(gè)對(duì)應(yīng) view 的弱引用, 并初始化一個(gè) constraints
的空可變數(shù)組用來(lái)之后配置屬性時(shí)持有所有的約束.
// MASConstraintMaker.m
- (id)initWithView:(MAS_VIEW *)view {
self = [super init];
if (!self) return nil;
self.view = view;
self.constraints = NSMutableArray.new;
return self;
}
這里的 MAS_VIEW
是一個(gè)宏, 是 UIView
的 alias.
// MASUtilities.h
#define MAS_VIEW UIView
Setup MASConstraintMaker
在調(diào)用 block(constraintMaker)
時(shí), 實(shí)際上是對(duì) constraintMaker
的配置.
make.centerX.equalTo(self.view);
make.top.equalTo(self.view).with.offset(40);
make.width.equalTo(@185);
make.height.equalTo(@38);
make.left
訪問(wèn) make
的 left
right
top
bottom
等屬性時(shí), 會(huì)調(diào)用 constraint:addConstraintWithLayoutAttribute:
方法.
// MASViewConstraint.m
- (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 isKindOfClass:MASViewConstraint.class]) { ... }
if (!constraint) {
newConstraint.delegate = self;
[self.constraints addObject:newConstraint];
}
return newConstraint;
}
在調(diào)用鏈上最終會(huì)達(dá)到 constraint:addConstraintWithLayoutAttribute:
這一方法, 在這里省略了一些暫時(shí)不需要了解的問(wèn)題. 因?yàn)樵谶@個(gè)類中傳入該方法的第一個(gè)參數(shù)一直為 nil
, 所以這里省略的代碼不會(huì)執(zhí)行.
這部分代碼會(huì)先以布局屬性 left
和視圖本身初始化一個(gè) MASViewAttribute
的實(shí)例, 之后使用 MASViewAttribute
的實(shí)例初始化一個(gè) constraint
并設(shè)置它的代理, 加入數(shù)組, 然后返回.
這些工作就是你在輸入 make.left
進(jìn)行的全部工作, 它會(huì)返回一個(gè) MASConstraint
, 用于之后的繼續(xù)配置.
make.left.equalTo(@80)
在 make.left
返回 MASConstraint
之后, 我們會(huì)繼續(xù)在這個(gè)鏈?zhǔn)降恼Z(yǔ)法中調(diào)用下一個(gè)方法來(lái)指定約束的關(guān)系.
// MASConstraint.h
- (MASConstraint * (^)(id attr))equalTo;
- (MASConstraint * (^)(id attr))greaterThanOrEqualTo;
- (MASConstraint * (^)(id attr))lessThanOrEqualTo;
這三個(gè)方法是在 MASViewConstraint
的父類, MASConstraint
中定義的.
MASConstraint
是一個(gè)抽象類, 其中有很多的方法都必須在子類中覆寫(xiě)的. Masonry 中有兩個(gè) MASConstraint
的子類, 分別是 MASViewConstraint
和 MASCompositeConstraint
. 后者實(shí)際上是一些約束的集合. 這么設(shè)計(jì)的原因我們會(huì)在 post 的最后解釋.
先來(lái)看一下這三個(gè)方法是怎么實(shí)現(xiàn)的:
// MASConstraint.m
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
該方法會(huì)導(dǎo)致 self.equalToWithRelation
的執(zhí)行, 而這個(gè)方法是定義在子類中的, 因?yàn)楦割愖鳛槌橄箢悰](méi)有提供這個(gè)方法的具體實(shí)現(xiàn).
// MASConstraint.m
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { MASMethodNotImplemented(); }
MASMethodNotImplemented
也是一個(gè)宏定義, 用于在子類未繼承這個(gè)方法或者直接使用這個(gè)類時(shí)拋出異常.
// MASConstraint.m
#define MASMethodNotImplemented() \
@throw [NSException exceptionWithName:NSInternalInconsistencyException \
reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
userInfo:nil]
因?yàn)槲覀優(yōu)?equalTo
提供了參數(shù) attribute
和布局關(guān)系 NSLayoutRelationEqual
, 這兩個(gè)參數(shù)會(huì)傳遞到 equalToWithRelation
中, 設(shè)置 constraint
的布局關(guān)系和 secondViewAttribute
屬性, 為即將 maker 的 install
做準(zhǔn)備.
// MASViewConstraint.m
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
if ([attribute isKindOfClass:NSArray.class]) { ... }
else {
...
self.layoutRelation = relation;
self.secondViewAttribute = attribute;
return self;
}
};
}
我們不得不提一下 setSecondViewAttribute:
方法, 它并不只是一個(gè)簡(jiǎn)單的 setter 方法, 它會(huì)根據(jù)你傳入的值的種類賦值.
// MASConstraintMaker.m
- (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);
}
}
第一種情況對(duì)應(yīng)的就是:
make.left.equalTo(@40);
傳入 NSValue
的時(shí), 會(huì)直接設(shè)置 constraint
的 offset
, centerOffset
, sizeOffset
, 或者 insets
第二種情況一般會(huì)直接傳入一個(gè)視圖:
make.left.equalTo(view);
這時(shí), 就會(huì)初始化一個(gè) layoutAttribute
屬性與 firstViewArribute
相同的 MASViewAttribute
, 上面的代碼就會(huì)使視圖與 view
左對(duì)齊.
第三種情況會(huì)傳入一個(gè)視圖的 MASViewAttribute
:
make.left.equalTo(view.mas_right);
使用這種寫(xiě)法時(shí), 一般是因?yàn)榧s束的方向不同. 這行代碼會(huì)使視圖的左側(cè)與 view
的右側(cè)對(duì)齊.
到這里我們就基本完成了對(duì)一個(gè)約束的配置, 接下來(lái)可以使用相同的語(yǔ)法完成對(duì)一個(gè)視圖上所有約束進(jìn)行配置, 然后進(jìn)入了最后一個(gè)環(huán)節(jié).
Install MASConstraintMaker
我們會(huì)在 mas_makeConstraints:
方法的最后調(diào)用 [constraintMaker install]
方法來(lái)安裝所有存儲(chǔ)在 self.constraints
數(shù)組中的所有約束.
// MASConstraintMaker.m
- (NSArray *)install {
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;
}
在這個(gè)方法會(huì)先判斷當(dāng)前的視圖的約束是否應(yīng)該要被 uninstall
, 如果我們?cè)谧铋_(kāi)始調(diào)用 mas_remakeConstraints:
方法時(shí), 視圖中原來(lái)的約束就會(huì)全部被 uninstall
.
然后就會(huì)遍歷 constraints
數(shù)組, 發(fā)送 install
消息.
MASViewConstraint install
MASViewConstraint 的 install
方法就是最后為當(dāng)前視圖添加約束的最后的方法, 首先這個(gè)方法會(huì)先獲取即將用于初始化 NSLayoutConstraint
的子類的幾個(gè)屬性.
// MASViewConstraint.m
MAS_VIEW *firstLayoutItem = self.firstViewAttribute.view;
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.view;
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
Masonry 之后會(huì)判斷當(dāng)前即將添加的約束是否是 size 類型的約束
// MASViewConstraint.m
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
secondLayoutItem = firstLayoutItem.superview;
secondLayoutAttribute = firstLayoutAttribute;
}
如果不是 size 類型并且沒(méi)有提供第二個(gè) viewAttribute
, (e.g. make.left.equalTo(@10);
) 會(huì)自動(dòng)將約束添加到 superview
上. 它等價(jià)于:
make.left.equalTo(superView.mas_left).with.offset(10);
然后就會(huì)初始化 NSLayoutConstraint
的子類 MASLayoutConstraint
:
// MASViewConstraint.m
MASLayoutConstraint *layoutConstraint
= [MASLayoutConstraint constraintWithItem:firstLayoutItem
attribute:firstLayoutAttribute
relatedBy:self.layoutRelation
toItem:secondLayoutItem
attribute:secondLayoutAttribute
multiplier:self.layoutMultiplier
constant:self.layoutConstant];
layoutConstraint.priority = self.layoutPriority;
接下來(lái)它會(huì)尋找 firstLayoutItem
和 secondLayoutItem
兩個(gè)視圖的公共 superview
, 相當(dāng)于求兩個(gè)數(shù)的最小公倍數(shù).
// View+MASAdditions.m
- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view {
MAS_VIEW *closestCommonSuperview = nil;
MAS_VIEW *secondViewSuperview = view;
while (!closestCommonSuperview && secondViewSuperview) {
MAS_VIEW *firstViewSuperview = self;
while (!closestCommonSuperview && firstViewSuperview) {
if (secondViewSuperview == firstViewSuperview) {
closestCommonSuperview = secondViewSuperview;
}
firstViewSuperview = firstViewSuperview.superview;
}
secondViewSuperview = secondViewSuperview.superview;
}
return closestCommonSuperview;
}
如果需要升級(jí)當(dāng)前的約束就會(huì)獲取原有的約束, 并替換為新的約束, 這樣就不需要再次為 view
安裝約束.
// MASViewConstraint.m
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];
如果原來(lái)的 view
中不存在可以升級(jí)的約束, 或者沒(méi)有調(diào)用 mas_updateConstraint:
方法, 那么就會(huì)在上一步尋找到的 installedView
上面添加約束.
[self.installedView addConstraint:layoutConstraint];
其他問(wèn)題
到現(xiàn)在為止整個(gè) Masonry 為視圖添加約束的過(guò)程就已經(jīng)完成了, 然而我們還有一些待解決的其它問(wèn)題.
make.left.equal(view).with.offset(30)
我們?cè)谇懊娴挠懻撝幸呀?jīng)討論了這個(gè)鏈?zhǔn)秸Z(yǔ)法的前半部分, 但是在使用中也會(huì)"延長(zhǎng)"這個(gè)鏈?zhǔn)秸Z(yǔ)句, 比如添加 with
offset
.
其實(shí)在 Masonry 中使用 with
并不是必須的, 它的作用僅僅是使代碼更加的易讀.
Optional semantic property which has no effect but improves the readability of constraint
// MASConstraint.m
- (MASConstraint *)with {
return self;
}
- (MASConstraint *)and {
return self;
}
與 with
有著相同作用的還有 and
, 這兩個(gè)方法都會(huì)直接返回 MASConstraint
, 方法本身不做任何的修改.
而 offset
方法其實(shí)是修改 layoutConstraint
中的常量, 因?yàn)?self.layoutConstant
在初始化時(shí)會(huì)被設(shè)置為 0, 我們可以通過(guò)修改 offset
屬性來(lái)改變它.
// MASViewConstraint.m
- (void)setOffset:(CGFloat)offset {
self.layoutConstant = offset;
}
MASCompositeConstraint
MASCompositeConstraint
是一些 MASConstraint
的集合, 它能夠提供一種更加便捷的方法同時(shí)為一個(gè)視圖來(lái)添加多個(gè)約束.
A group of MASConstraint objects
通過(guò) make
直接調(diào)用 edges
size
center
時(shí), 就會(huì)產(chǎn)生一個(gè) MASCompositeConstraint
的實(shí)例, 而這個(gè)實(shí)例會(huì)初始化所有對(duì)應(yīng)的單獨(dú)的約束.
// MASConstraintMaker.m
- (MASConstraint *)edges {
return [self addConstraintWithAttributes:MASAttributeTop | MASAttributeLeft | MASAttributeRight | MASAttributeBottom];
}
- (MASConstraint *)size {
return [self addConstraintWithAttributes:MASAttributeWidth | MASAttributeHeight];
}
- (MASConstraint *)center {
return [self addConstraintWithAttributes:MASAttributeCenterX | MASAttributeCenterY];
}
這些屬性都會(huì)調(diào)用 addConstraintWithAttributes:
方法, 生成多個(gè)屬于 MASCompositeConstraint
的實(shí)例.
// MASConstraintMaker.m
NSMutableArray *children = [NSMutableArray arrayWithCapacity:attributes.count];
for (MASViewAttribute *a in attributes) {
[children addObject:[[MASViewConstraint alloc] initWithFirstViewAttribute:a]];
}
MASCompositeConstraint *constraint = [[MASCompositeConstraint alloc] initWithChildren:children];
constraint.delegate = self;
[self.constraints addObject:constraint];
return constraint;
mas_equalTo
Masonry 中還有一個(gè)類似與 magic 的宏, 這個(gè)宏將 C 和 Objective-C 語(yǔ)言中的一些基本數(shù)據(jù)結(jié)構(gòu)比如說(shuō) double
CGPoint
CGSize
這些值用 NSValue
進(jìn)行包裝.
這是一種非常簡(jiǎn)潔的使用方式, 如果你對(duì)這個(gè)非常感興趣, 可以看一下 MASUtilities.h
中的源代碼, 在這里就不對(duì)這個(gè)做出解釋了.
Masonry 如何為視圖添加約束(面試回答)
Masonry 與其它的第三方開(kāi)源框架一樣選擇了使用分類的方式為 UIKit 添加一個(gè)方法 mas_makeConstraint
, 這個(gè)方法接受了一個(gè) block, 這個(gè) block 有一個(gè) MASConstraintMaker
類型的參數(shù), 這個(gè) maker 會(huì)持有一個(gè)約束的數(shù)組, 這里保存著所有將被加入到視圖中的約束.
我們通過(guò)鏈?zhǔn)降恼Z(yǔ)法配置 maker, 設(shè)置它的 left
right
等屬性, 比如說(shuō) make.left.equalTo(view)
, 其實(shí)這個(gè) left
equalTo
還有像 with
offset
之類的方法都會(huì)返回一個(gè) MASConstraint
的實(shí)例, 所以在這里才可以用類似 Ruby 中鏈?zhǔn)降恼Z(yǔ)法.
在配置結(jié)束后, 首先會(huì)調(diào)用 maker 的 install
方法, 而這個(gè) maker 的 install
方法會(huì)遍歷其持有的約束數(shù)組, 對(duì)其中的每一個(gè)約束發(fā)送 install
消息. 在這里就會(huì)使用到在上一步中配置的屬性, 初始化 NSLayoutConstraint
的子類 MASLayoutConstraint
并添加到合適的視圖上.
視圖的選擇會(huì)通過(guò)調(diào)用一個(gè)方法 mas_closestCommonSuperview:
來(lái)返回兩個(gè)視圖的最近公共父視圖.
總結(jié)
雖然 Masonry 這個(gè)框架中的代碼并不是非常的多, 只有 1,2 萬(wàn)行的代碼, 但是感覺(jué)這個(gè)項(xiàng)目閱讀起來(lái)十分的困難, 沒(méi)有 SDWebImage 清晰, 因?yàn)榇a中類的屬性非常的多, 而且有很多相似的屬性會(huì)干擾我們對(duì)這個(gè)項(xiàng)目的閱讀, 整個(gè)框架運(yùn)用了大量的 block 語(yǔ)法進(jìn)行回調(diào).
雖然代碼十分整潔不過(guò)我覺(jué)得卻降低了可讀性, 但是還是那句話, 把簡(jiǎn)潔留給別人復(fù)雜留給自己, 只要為開(kāi)發(fā)者提供簡(jiǎn)潔的接口就可以了.