Masonry is a light-weight layout framework which wraps AutoLayout with a nicer syntax. Masonry has its own layout DSL which provides a chainable way of describing your NSLayoutConstraints which results in layout code that is more concise and readable. Masonry supports iOS and Mac OS X.
翻譯:
Masonry是一個輕量級的布局框架,它使用更好的語法包裝AutoLayout。 Masonry有自己的布局DSL,它提供了一種鏈式調(diào)用的方式來描述NSLayoutConstraints,從而使布局代碼更簡潔,更易讀。 Masonry支持iOS和Mac OS X.
什么是DSL
DSL(Domain Specific Language) 翻譯成中文就是:“領域特定語言”。首先,從定義就可以看出,DSL 也是一種編程語言,只不過它主要是用來處理某個特定領域的問題。
下邊介紹iOS中如何實現(xiàn)鏈式調(diào)用的DSL。
為什么需要使用Masonry
首先看下直接用NSLayoutConstraints方式布局視圖需要什么操作:
例如:我們需要布局一個視圖view1,使他距離父視圖上下左右都為10,NSLayoutConstraints布局代碼如下:
公式:view1.top = superview.top * 1.0 + 10
UIView *superview = self.view;
UIView *view1 = [[UIView alloc] init];
view1.translatesAutoresizingMaskIntoConstraints = NO;
view1.backgroundColor = [UIColor greenColor];
[superview addSubview:view1];
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
[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代碼如下:
[view1 mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.mas_equalTo(superview).with.insets(padding);}];
由此可以看到使用NSLayoutConstraints方式布局代碼及其冗余且不易讀。
Masonry框架結構分析
主要的幾個類:
View+MASAdditions.h
MASConstraintMaker
MASViewConstraint
masonry_struct_chart
從調(diào)用mas_makeConstraints方法說起
首先我們看一個簡單調(diào)用的例子:
[view1 mas_makeConstraints:^(MASConstraintMaker *make) { make.left.mas_equalTo(superview).offset(10)}];
mas_makeConstraints:實現(xiàn)如下:首先將self.translatesAutoresizingMaskIntoConstraints置為NO,關閉自動添加約束,改為我們手動添加,接著創(chuàng)建一個MASConstraintMaker對象,通過block將constraintMaker對象回調(diào)給用戶,讓用戶對constraintMaker對象的屬性進行初始化,其中block(constraintMaker)就相當于我們直接在該方法內(nèi)部調(diào)用make.left.mas_equalTo(superview).offset(10),然后調(diào)用install方法對約束進行安裝,該方法返回一個數(shù)組,數(shù)組當中存放約束數(shù)組,成員類型為MASViewConstraint
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];}
緊接著看下block回調(diào)回來的操作是如何進行的,也就是下面的這些代碼:
make.left.mas_equalTo(superview).offset(10)
點擊去看下.left的調(diào)用實現(xiàn):
(MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];}- (MASConstraint *)left { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];}
最后調(diào)用:(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;}
其中有一個MASViewAttribute的類,該類其實是對UIView和NSLayoutAttribute的封裝
MASViewConstraint是對NSLayoutConstraint的封裝,最后將布局約束添加到一個數(shù)組當中
block回調(diào)執(zhí)行完畢之后,最后對布局進行安裝[constraintMaker install],該方法,最后會調(diào)用到MASViewConstraint當中的install方法,其中有一個比較重要的方法:
MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view {
MAS_VIEW *closestCommonSuperview = nil; // 定義一個公共父視圖
MAS_VIEW *secondViewSuperview = view;
while (!closestCommonSuperview && secondViewSuperview) {
// 遍歷secondView的父視圖
MAS_VIEW *firstViewSuperview = self;
while (!closestCommonSuperview && firstViewSuperview) { // 遍歷當前視圖的父視圖
if (secondViewSuperview == firstViewSuperview) {
closestCommonSuperview = secondViewSuperview; // 找到公共父視圖
}
firstViewSuperview = firstViewSuperview.superview;
}
secondViewSuperview = secondViewSuperview.superview;
}
return closestCommonSuperview;}
該方法是查找兩個視圖最近的公共父視圖,這個類似求兩個數(shù)字的最小公倍數(shù)。如果找到了就返回,如果找不到就返回nil。尋找兩個視圖的公共父視圖對于約束的添加來說是非常重要的,因為相對的約束是添加到其公共父視圖上的。比如舉個列子 viewA.left = viewB.right + 10, 因為是viewA與viewB的相對約束,那么約束是添加在viewA與viewB的公共父視圖上的,如果viewB是viewA的父視圖,那么約束就添加在viewB上從而對viewA起到約束作用。
遇到的問題
MAS_SHORTHAND 和 MAS_SHORTHAND_GLOBALS的含義
MAS_SHORTHAND: 如果我們用Masonry框架不想寫mas_前綴,需要在導入頭文件之前定義這個宏
MAS_SHORTHAND_GLOBALS: 定義這個宏,可以讓equalTo接受基本數(shù)據(jù)類型,內(nèi)部會對基本數(shù)據(jù)類型進行封裝
//define this constant if you want to use Masonry without the 'mas_' prefix#define MAS_SHORTHAND
//define this constant if you want to enable auto-boxing for default syntax#
define MAS_SHORTHAND_GLOBALS#import "Masonry.h"
Convenience initializer 和 designated initializer
在我們閱讀masonry源碼的過程中,我們發(fā)現(xiàn)有兩個初始化方法,注釋不太一樣,位于MASViewAttribute類下:
/** * Convenience initializer. */
- (id)initWithView:(MAS_VIEW )view layoutAttribute:(NSLayoutAttribute)layoutAttribute;
/* * The designated initializer. */ - (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute;
這兩種初始化的方式有什么區(qū)別呢,平時在我們開發(fā)當中,我們可能需要通過初始化來確定一些屬性的值,并不想由外界來修改它,于是我們可能會需要些很多個initWith方法,加入我的這個對象有姓名、性別、年齡等屬性,但是我初始化的時候,并不是所有地方都要知道這些信息,于是我們可能會有initWithUserName:、initWithSex:、initWithUserName:age:等方法,為了管理我們的initWith方法,Apple 將 init 方法分為兩種類型:designated initializer 和 convenience initializer (又叫 secondary initializer)
designated initializer 只有一個,它會為 class當中每個 property 都提供一個初始值,是最完整的 initWith 方法,例如我們在masonry當中也可以看到:
- (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;}
最終都會調(diào)用到- (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute方法
convenience initializer 則可以有很多個,它可以選擇只初始化部分的 property。convenience initializer 最后到會調(diào)用到 designated initializer,所以 designated initializer 也可以叫做 final initializer
NS_NOESCAPE修飾符
在masonry源碼當中,我們看到在修飾block的時候用到了NS_NOESCAPE
- (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;
NS_NOESCAPE用于修飾方法中的block類型參數(shù),作用是告訴編譯器,這個block在mas_makeConstraints:方法返回之前就會執(zhí)行完畢,而不是被保存起來在之后的某個時候再執(zhí)行
masonry為什么不會引起循環(huán)引用
比如我們可能經(jīng)常會寫如下代碼:
[self.view mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view.superview).offset(10);
}];
這里為什么不需要寫@weakify(self),接著看mas_makeConstraints:是如何實現(xiàn)的:
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
從代碼中可以看到,block強引用了self,但是在mas_makeConstraints:方法中self并沒有直接或間接持有block,而是直接調(diào)用block(constraintMaker),所以不會引起強引用
鏈式調(diào)用實戰(zhàn)應用
在我們開發(fā)過程中,我們會經(jīng)常用到UILabel,每次初始化都要設置一堆的屬性,比較麻煩,當然我們也可以采取類似如下方法:+ (UILabel *)createLabelWithFont:(UIFont *)font andTextColor:(UIColor *)color andDefaultContent:(NSString *)content,但是一旦我們所需要初始化的參數(shù)比較多時,就會造成方法參數(shù)非常多,甚至我們有些參數(shù)根本不需要初始化,用鏈式編程該如何實現(xiàn)呢??
首先為UILabel創(chuàng)建一個category,#import “UILabel+zjLabel.h”,代碼如下:
import "UILabel+zjLabel.h"
@implementation UILabel (zjLabel)
- (UILabel *)zj_createLabel:(void (^)(UILabel * _Nonnull))block{
UILabel *label = [UILabel new];
block(label);
return label;
}
(UILabel *(^)(NSString *))zj_text{
return ^(NSString *str){
self.text = str;
return self;
};
}(UILabel *(^)(UIFont *))zj_font{
return ^(UIFont *font){
self.font = font;
return self;
};
}(UILabel *(^)(UIColor *))zj_textColor{
return ^(UIColor *color){
self.textColor = color;
return self;
};
}(UILabel *(^)(NSTextAlignment))zj_textAlignment{
return ^(NSTextAlignment aligment){
self.textAlignment = aligment;
return self;
};
}
在需要的地方調(diào)用方式如下:
UILabel *label = [UILabel zj_createLabel:^(UILabel * _Nonnull label) {
label.zj_text(@"haha").zj_font([UIFont systemFontOfSize:24]).zj_textColor(UIColor.redColor);
}];
[superview addSubview:label];
不需要初始化的參數(shù)可以直接不寫,只初始化我們需要的
總結
另外很多人擔心自動布局的性能問題,事實上蘋果已經(jīng)在iOS12中對auto layout進行優(yōu)化:
WWDC2018講解了iOS12優(yōu)化后的表現(xiàn)
auto_layout_ optimize
可以看到在iOS12之前auto layout性能會隨著嵌套視圖的增加呈指數(shù)增長,但是在iOS12上蘋果官方已經(jīng)對此進行了優(yōu)化,隨著嵌套視圖的增加性能問題得到了大幅的提升。
鏈式編程的特點:方法返回值是block,而且該block必須有返回值,返回值就是對象本身,block也可以輸入?yún)?shù)