在瞭解`Masonry`框架之前,有必要先瞭解一下**自動佈局**的概念。在`iOS6`之前,`UI`佈局的方式是通過`frame`屬性和`Autoresizing`來完成的,而在`iOS6`之後,蘋果公司推出了`AutoLayout`的佈局方式,它是一種基於約束性的、描述性的佈局系統,尤其是蘋果的... ...
在瞭解Masonry
框架之前,有必要先瞭解一下自動佈局的概念。在iOS6
之前,UI
佈局的方式是通過frame
屬性和Autoresizing
來完成的,而在iOS6
之後,蘋果公司推出了AutoLayout
的佈局方式,它是一種基於約束性的、描述性的佈局系統,尤其是蘋果的手機屏幕尺寸變多之後,AutoLayout
的應用也越來越廣泛。
但是,手寫AutoLayout
佈局代碼是十分繁瑣的工作(不熟悉的話,可以找資料體驗一下,保證讓你爽到想哭,^_^);鑒於此,蘋果又開發了VFL
的佈局方式,雖然簡化了許多,但是依然需要手寫很多代碼;如果,你希望不要手寫代碼,那麼可以用xib
來佈局UI
,可以圖形化添加約束,只是xib
的方式不太適合多人協作開發。綜合以上的各種問題,Masonry
出現了,這是一款輕量級的佈局框架,採用閉包、鏈式編程的技術,通過封裝系統的NSLayoutConstraints
,最大程度地簡化了UI
佈局工作。
本文主要分析一下Masonry
的源碼結構、佈局方式和實現原理等等。
框架結構
Masonry
框架的源碼其實並不複雜,利用自己的描述語言,採用優雅的鏈式語法,使得自動佈局方法簡潔明瞭,並且同時支持iOS
和MacOS
兩個系統。Masonry
框架的核心就是MASConstraintMaker
類,它是一個工廠類,根據約束的類型會創建不同的約束對象;單個約束會創建MASViewConstraint
對象,而多個約束則會創建MAXCompositeConstraint
對象,然後再把約束統一添加到視圖上面。
佈局方式
Masonry
的佈局方式比較靈活,有mas_makeConstraints
(創建佈局)、mas_updateConstraints
(更新佈局)、mas_remakeConstraints
(重新創建佈局)三種:
1.mas_makeConstraints:
給視圖(視圖本身或父視圖)添加新的約束
// 給view1添加約束,frame和superView一樣
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superview);
}];
2.mas_updateConstraints:
更新視圖的約束,從視圖中查找相同的約束,如果找到,就更新,會設置maker
的updateExisting
為YES
// 更新view1的上邊距離superView為60,寬度為100
[view1 mas_updateConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(@60);
make.width.equalTo(@100);
}];
3.mas_remakeConstraints:
給視圖添加約束,如果視圖之前已經添加了約束,則會刪除之前的約束,會設置maker
的removeExisting
為YES
// 重新設置view1的約束為:頂部距離父視圖為300,左邊距離父視圖為100,寬為100,高為50
[view1 mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superview).offset(300);
make.left.equalTo(superview).offset(100);
make.width.equalTo(@100);
make.height.equalTo(@50);
}];
Masonry
的佈局相對關係也有三種:.equalTo
(==)、.lessThanOrEqualTo
(<=)、.greaterThanOrEqualTo
(>=)。
Masonry
的佈局關係的參數也有三種:
1. @100 --> 表示指定具體值
2. view --> 表示參考視圖的相同約束屬性,如view1的left參考view2的left等
3. view.mas_left --> 表示參考視圖的特定約束屬性,如view1的left參考view2的right等
實現原理
Masonry
是利用閉包和鏈式編程的技術實現簡化操作的,所以需要對閉包和鏈式編程有一定的基礎。下麵會根據案例來具體分析一下Masonry
的實現細節,代碼實現的功能是設置view1
的frame
是CGRectMake(100, 100, 100, 100)
;其中,mas_equalTo(...)
是巨集,會被替換成equalTo(MASBoxValue((...)))
,功能是把基本類型包裝成對象類型:
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.top.equalTo(@100);
make.size.mas_equalTo(CGSizeMake(50, 50));
}];
1.創建maker
首先調用mas_makeConstraints:
,這是一個UIView
的分類方法,參數是一個設置約束的block
,會把調用視圖作為參數創業一個maker
:
// View+MASAdditions.h
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
// 創建maker,並保存調用該方法的視圖view
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
2.生成約束
接下來,開始利用maker
產生約束,即調用block(constraintMaker)
2.1 設置坐標x
make.left
調用過程:
// MASConstraintMaker.h
- (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]) {
// 傳入的參數是nil,所以此處代碼不會執行
...
}
if (!constraint) {
// 設置newConstraint的代理為maker
newConstraint.delegate = self;
// 把約束加入到數組中
[self.constraints addObject:newConstraint];
}
// 返回MASViewConstraint類型的約束對象
return newConstraint;
}
其中,上述代碼根據maker
保存的view
和傳入的約束屬性layoutAttribute
創建了一個MASViewAttribute
對象,然後根據viewAttribute
對象創建了一個MASViewConstraint
約束對象,代碼如下:
// MASViewAttribute.h
- (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 = view;
_item = item;
// 保存約束屬性:NSLayoutAttributeLeft
_layoutAttribute = layoutAttribute;
return self;
}
// MASViewConstraint.h
- (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute {
self = [super init];
if (!self) return nil;
// 保存第一個屬性(封裝了視圖view和約束屬性NSLayoutAttributeLeft)
_firstViewAttribute = firstViewAttribute;
self.layoutPriority = MASLayoutPriorityRequired;
self.layoutMultiplier = 1;
return self;
}
2.2 設置坐標y
make.left.top
由於make.left
返回的是MASViewConstraint
對象,所以調用的top
應該是MASViewConstraint
類中的方法(該方法繼承自父類MASConstraint
),調用過程如下:
// MASConstraint.h
- (MASConstraint *)top {
// self是MASViewConstraint對象
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
// MASViewConstraint.h
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
// self.delegate是maker對象
return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}
// MASConstraintMaker.h
- (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]) {
// 由於參數constraint不為nil,所以進入此處執行
//replace with composite constraint
NSArray *children = @[constraint, newConstraint];
// 創建約束集合對象,並把先前的約束對象和本次新創建的約束對象保存到數組中
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
// 設置約束集合對象的代理為maker
compositeConstraint.delegate = self;
// 用約束集合對象替換maker中已經保存的約束對象,因為我們同一個maker設置了2個以上的約束
[self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
// 返回MASCompositeConstraint約束集合對象
return compositeConstraint;
}
if (!constraint) {
...
}
return newConstraint;
}
如果一個maker
添加多個約束後,就會創建MASCompositeConstraint
對象,創建約束集合的過程如下:
- (id)initWithChildren:(NSArray *)children {
self = [super init];
if (!self) return nil;
// 保存約束數組
_childConstraints = [children mutableCopy];
for (MASConstraint *constraint in _childConstraints) {
// 設置數組中所有的約束對象的代理為MASCompositeConstraint對象
constraint.delegate = self;
}
return self;
}
在創建了MASCompositeConstraint
對象後,就會更新maker
中的約束數組,在最後添加約束的時候,就會是全部的約束對象,代碼如下:
- (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];
}
2.3 設置x、y的值
make.left.top.equalTo(@100)
make.left.top
返回的對象是MASCompositeConstraint
類型,調用過程如下:
// MASConstraint.h
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
// MASCompositeConstraint.h
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attr, NSLayoutRelation relation) {
for (MASConstraint *constraint in self.childConstraints.copy) {
// 遍曆數組,把每個MASViewConstraint對象都調用該方法
constraint.equalToWithRelation(attr, relation);
}
return self;
};
}
// MASViewConstraint.h
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
if ([attribute isKindOfClass:NSArray.class]) {
// 由於attribute是@100的包裝類型,不是數組,此處代碼不會執行
...
} else {
NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
// 設置約束類別為NSLayoutRelationEqual
self.layoutRelation = relation;
// 設置第二個屬性
self.secondViewAttribute = attribute;
return self;
}
};
}
- (void)setLayoutRelation:(NSLayoutRelation)layoutRelation {
_layoutRelation = layoutRelation;
// 表明已經有了約束關係
self.hasLayoutRelation = YES;
}
下麵分析一下設置第二個屬性secondViewAttribute
的過程,因為Masonry
重寫了setter
方法,過程如下:
// MASViewConstraint.h
- (void)setSecondViewAttribute:(id)secondViewAttribute {
if ([secondViewAttribute isKindOfClass:NSValue.class]) {
// secondViewAttribute是@100類型
[self setLayoutConstantWithValue:secondViewAttribute];
} else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
// secondViewAttribute是視圖UIView類型
_secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
} else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
// secondViewAttribute是view.mas_left類型
_secondViewAttribute = secondViewAttribute;
} else {
NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
}
}
// MASConstraint.h @100類型
- (void)setLayoutConstantWithValue:(NSValue *)value {
// 根據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);
}
}
由於@100
是NSNumber
類型,所以執行self.offset
來設置偏移量,代碼如下:
// MASViewConstraint.h
- (void)setOffset:(CGFloat)offset {
// 設置layoutConstant屬性值,在最後添加屬性時作為方法參數傳入
self.layoutConstant = offset;
}
- (void)setLayoutConstant:(CGFloat)layoutConstant {
_layoutConstant = layoutConstant;
#if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
...
#else
self.layoutConstraint.constant = layoutConstant;
#endif
}
這裡有網友疑惑,因為self.layoutConstraint
在上面的方法中一直是nil
,設置它的constant
屬性是沒有意義的,不知道這麼寫有何意義?其實,我也有同樣的疑問!!!
2.4 設置size
另外,make.size
的實現過程和上面的分析類似,有興趣的可以自行參考,看一看具體的實現過程,在此不做分析。
3.安裝約束
下麵分析一下約束的安裝過程
[constraintMaker install]
調用過程如下:
// MASConstraintMaker.h
- (NSArray *)install {
if (self.removeExisting) {
// 是remake,所以要先刪除已經視圖中存在的約束
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;
}
// MASViewConstraint.h
- (void)uninstall {
if ([self supportsActiveProperty]) {
// 如果 self.layoutConstraint 響應了 isActive 方法並且不為空,會激活這條約束並添加到 mas_installedConstraints 數組中,最後返回
self.layoutConstraint.active = NO;
[self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
return;
}
[self.installedView removeConstraint:self.layoutConstraint];
self.layoutConstraint = nil;
self.installedView = nil;
[self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
}
下麵分析一下install
的過程:
- (void)install {
// 如果已經安裝過約束,直接返回
if (self.hasBeenInstalled) {
return;
}
// 如果 self.layoutConstraint 響應了 isActive 方法並且不為空,會激活這條約束並添加到 mas_installedConstraints 數組中,最後返回
if ([self supportsActiveProperty] && self.layoutConstraint) {
self.layoutConstraint.active = YES;
[self.firstViewAttribute.view.mas_installedConstraints addObject:self];
return;
}
// 取出約束的兩個視圖及約束屬性
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) {
// 如果第一個屬性不是size屬性,並且第二個屬性為nil,就把第二個視圖設置為view的父視圖,約束屬性設置為view的約束屬性
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) {
// 如果第一個屬性是設置size的,就把第一個視圖賦值給installedView
self.installedView = self.firstViewAttribute.view;
} else {
// 否則就取第一個視圖的父視圖賦值給installedView
self.installedView = self.firstViewAttribute.view.superview;
}
MASLayoutConstraint *existingConstraint = nil;
if (self.updateExisting) {
// 如果是更新屬性,就根據layoutConstraint查看視圖中是否存在該約束
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];
}
}
求兩個視圖的最小父視圖的代碼如下:
- (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) {
// 如果first和second的視圖一樣,就設置closestCommonSuperview,並返回
closestCommonSuperview = secondViewSuperview;
}
firstViewSuperview = firstViewSuperview.superview;
}
secondViewSuperview = secondViewSuperview.superview;
}
return closestCommonSuperview;
}
其實,上述代碼是先判斷first
和second
的視圖是否一樣,如果一樣,直接返回;如果不一樣,就判斷fisrt
的父視圖和second
是否一樣,如果一樣,就返回;不一樣,繼續判斷first
的父視圖和second
的父視圖是否一樣,如果一樣,就返回;不一樣,重覆迭代。
結束語
Masonry
的源碼分析完結,如果文中有不足之處,希望指出,互相學習。
參考資料