傳送門:鏈式程式設計小Demo
這篇文章是 Masonry 框架原始碼的解析和筆記。學習Masonry之前,先了解這個框架設計的初衷—傳統的利用系統API進行純程式碼佈局的不足。然後,根據Masonry常見的幾個鏈式語法中,順藤摸瓜地瞭解Masonry的呼叫棧。最後,學習並思考這個框架用到的設計模式和鏈式程式設計思想。
1. 之前的不足:系統API純程式碼佈局
- 系統給的自動佈局(AutoLayout)的API
+(instancetype)constraintWithItem:(id)view1
attribute:(NSLayoutAttribute)attr1
relatedBy:(NSLayoutRelation)relation
toItem:(nullable id)view2
attribute:(NSLayoutAttribute)attr2
multiplier:(CGFloat)multiplier
constant:(CGFloat)c;
複製程式碼
- 傳統程式碼中使用系統API進行佈局
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.view.backgroundColor = [UIColor yellowColor];
UIView *subView = [[UIView alloc] init];
subView.backgroundColor = [UIColor redColor];
// 在設定約束前,先將子檢視新增進來
[self.view addSubview:subView];
// 使用autoLayout約束,禁止將AutoresizingMask轉換為約束
[subView setTranslatesAutoresizingMaskIntoConstraints:NO];
// 設定subView相對於VIEW的上左下右各40畫素
NSLayoutConstraint *constraintTop = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:40];
NSLayoutConstraint *constraintLeft = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:40];
// 由於iOS座標系的原點在左上角,所以設定下,右邊距使用負值
NSLayoutConstraint *constraintBottom = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-40];
NSLayoutConstraint *constraintRight = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeRight multiplier:1.0 constant:-40];
// 將四條約束加進陣列中
NSArray *array = [NSArray arrayWithObjects:constraintTop, constraintLeft, constraintBottom, constraintRight, nil];
// 把約束條件設定到父檢視的Contraints中
[self.view addConstraints:array];
}
複製程式碼
可見,系統傳統的程式碼佈局有點繁瑣。為了簡化上述傳統佈局程式碼,被廣泛應用的第三方框架 Masonry 對AutoLayout 進行了封裝,Swift版則是 SnapKit。這篇文章就是針對 Masonry 原始碼的解析與學習筆記。在這之前,如下圖所示,是 Masonry 原始碼的結構圖:
2. 順藤摸瓜:Masonry鏈式語法的呼叫棧解析
2.1 mas_makeConstraints
:外部呼叫
- 呼叫例子
#import "Masonry.h"
複製程式碼
[self.containerView addSubview:self.bannerView];
[self.bannerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(self.containerView.mas_leading);
make.top.equalTo(self.containerView.mas_top);
make.trailing.equalTo(self.containerView.mas_trailing);
make.height.equalTo(@(kViewWidth(131.0)));
}];
複製程式碼
2.2 mas_makeConstraints
:實現原理,通過匯入的標頭檔案分析
- Masonry.h
#import <Foundation/Foundation.h>
//! Project version number for Masonry.
FOUNDATION_EXPORT double MasonryVersionNumber;
//! Project version string for Masonry.
FOUNDATION_EXPORT const unsigned char MasonryVersionString[];
#import "MASUtilities.h"
#import "View+MASAdditions.h"
#import "View+MASShorthandAdditions.h"
#import "ViewController+MASAdditions.h"
#import "NSArray+MASAdditions.h"
#import "NSArray+MASShorthandAdditions.h"
#import "MASConstraint.h"
#import "MASCompositeConstraint.h"
#import "MASViewAttribute.h"
#import "MASViewConstraint.h"
#import "MASConstraintMaker.h"
#import "MASLayoutConstraint.h"
#import "NSLayoutConstraint+MASDebugAdditions.h"
複製程式碼
其中
View+MASAdditions
分類為UIView
新增了mas_makeConstraints
方法
- View+MASAdditions.m
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
複製程式碼
- MASConstraintMaker.m
@interface MASConstraintMaker () <MASConstraintDelegate>
@property (nonatomic, weak) MAS_VIEW *view;
@property (nonatomic, strong) NSMutableArray *constraints;
@end
複製程式碼
- (id)initWithView:(MAS_VIEW *)view {
self = [super init];
if (!self) return nil;
self.view = view;
self.constraints = NSMutableArray.new;
return self;
}
複製程式碼
2.3 .top
:通過MASConstraintMaker
類原始碼分析
先分析設定 第一個約束屬性 的情況(且唯一一個):例如
make.top.equalTo(self.containerView.mas_top);
複製程式碼
2.3.1 MASConstraintMaker的分析
- MASConstraintMaker.m
- (MASConstraint *)top {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
- (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]) {
//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;
}
複製程式碼
該方法返回的newConstraint
是一個MASViewConstraint
類的示例,而MASViewConstraint
類又是MASConstraint
的子類,返回型別寫成MASConstraint
沒毛病。
程式碼較多,暫時可以只先看if (!constraint)
裡面的程式碼。可見,最後設定 newConstraint
物件代理為self
(即 MASConstraintMaker
),並新增到一開始準備好的 self.constraints 陣列中,返回。
其中,設定 MASViewConstraint
類 newConstraint
物件的 MASConstraintDelegate
代理為self
(即 MASConstraintMaker
),其作用就是為了能夠同時設定多個約束屬性!即鏈式語法。
- MASConstraint+Private.h
@protocol MASConstraintDelegate <NSObject>
/**
* Notifies the delegate when the constraint needs to be replaced with another constraint. For example
* A MASViewConstraint may turn into a MASCompositeConstraint when an array is passed to one of the equality blocks
*/
- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint;
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute;
@end
複製程式碼
2.3.2 MASConstraintMaker的繼續分析
第2.3.1節的MASConstraintMaker.m
程式碼中,先是初始化了 MASViewAttribute
物件並儲存了 view、item以及 NSLayoutAttribute
三個屬性。
- MASViewAttribute.m
- (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;
}
複製程式碼
然後又初始化了 MASViewConstraint
物件,內部配置了些預設引數並儲存瞭如上的第一個約束引數 MASViewAttribute
。
- MASViewConstraint.m
- (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute {
self = [super init];
if (!self) return nil;
_firstViewAttribute = firstViewAttribute;
self.layoutPriority = MASLayoutPriorityRequired;
self.layoutMultiplier = 1;
return self;
}
複製程式碼
2.4 .equalTo
:通過基類MASConstraint
及其子類MASViewConstraint
分析
第一個約束屬性 設定完後,走到.equalTo
時,前面返回已經是一個 MASViewConstraint
(繼承自MASConstraint
) 物件了,因而呼叫的是在基類MASConstraint
中宣告並實現的block屬性getter方法。
- MASConstraint.m
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
複製程式碼
其中,基類 MASConstraint
僅僅宣告,並沒有實現equalToWithRelation
抽象方法。但是,如2.3節中的鏈式語法.top
,該方法返回的newConstraint
實際是其子類–MASViewConstraint
類的例項,故而可呼叫子類MASViewConstraint
實現的equalToWithRelation
方法:
- MASViewConstraint.m
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
if ([attribute isKindOfClass:NSArray.class]) {
NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
NSMutableArray *children = NSMutableArray.new;
for (id attr in attribute) {
MASViewConstraint *viewConstraint = [self copy];
viewConstraint.layoutRelation = relation;
viewConstraint.secondViewAttribute = attr;
[children addObject:viewConstraint];
}
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self.delegate;
[self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;
} else {
NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
self.layoutRelation = relation;
self.secondViewAttribute = attribute;
return self;
}
};
}
複製程式碼
程式碼較多,暫時可先看else {
裡面的程式碼。
(1) self.layoutRelation = relation;
首先是 self.layoutRelation
儲存了約束關係且重寫了 set
方法,在裡面用 self.hasLayoutRelation
這個 BOOL
標識已經有約束關係。
- MASViewConstraint.m
- (void)setLayoutRelation:(NSLayoutRelation)layoutRelation {
_layoutRelation = layoutRelation;
self.hasLayoutRelation = YES;
}
複製程式碼
(2) self.secondViewAttribute = attribute;
然後同樣是重寫了 self.secondViewAttribute
的 set
方法,這裡會根據不同的情況做不同的操作。
- (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]) {
MASViewAttribute *attr = secondViewAttribute;
if (attr.layoutAttribute == NSLayoutAttributeNotAnAttribute) {
_secondViewAttribute = [[MASViewAttribute alloc] initWithView:attr.view item:attr.item layoutAttribute:self.firstViewAttribute.layoutAttribute];;
} else {
_secondViewAttribute = secondViewAttribute;
}
} else {
NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
}
}
複製程式碼
其中,第1種情況對應的是:
make.height.equalTo(@20.0f)
複製程式碼
傳入 NSValue
的時, 會直接設定 constraint
的 offset
, centerOffset
, sizeOffset
, 或者 insets
。呼叫棧如下:
//MASViewConstraint.m
if ([secondViewAttribute isKindOfClass:NSValue.class]) {
[self setLayoutConstantWithValue:secondViewAttribute];
}
//MASConstraint.m
- (void)setLayoutConstantWithValue:(NSValue *)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);
}
}
//MASViewConstraint.m
- (void)setOffset:(CGFloat)offset {
self.layoutConstant = offset;
}
//MASViewConstraint.m
- (void)setLayoutConstant:(CGFloat)layoutConstant {
_layoutConstant = layoutConstant;
#if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
if (self.useAnimator) {
[self.layoutConstraint.animator setConstant:layoutConstant];
} else {
self.layoutConstraint.constant = layoutConstant;
}
#else
self.layoutConstraint.constant = layoutConstant;
#endif
}
複製程式碼
第2種情況,一般是直接傳入一個檢視:
make.top.equalTo(self)
複製程式碼
這時, 就會初始化一個 layoutAttribute
屬性與 firstViewArribute
相同的 MASViewAttribute
, 上面的程式碼就會使檢視與 view 頂部對齊。
第3種情況,會傳入一個檢視的 MASViewAttribute
:
make.top.equalTo(view.mas_bottom);
複製程式碼
使用這種寫法時, 一般是因為約束的方向不同. 這行程式碼會使檢視的頂部與 view 的底部對齊。
2.5 .height.width
:Masonry的鏈式語法特性
- 呼叫例子
make.height.width.equalTo(@20);
複製程式碼
其中,.height
設定第一個約束屬性時,呼叫的是 MASConstraintMaker.m
中的 .height
, addConstraintWithLayoutAttribute
,以及- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute
。
- MASConstraintMaker.m
- (MASConstraint *)height {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeHeight];
}
- (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]) {
//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;
}
複製程式碼
該方法呼叫棧返回的是一個MASViewConstraint
(父類是 MASConstraint
) 物件。
因此,通過 .width
設定第二個約束屬性的時候,呼叫的先是基類 MASConstraint.m
中的.width
,然後呼叫由子類MASViewConstraint
實現的addConstraintWithLayoutAttribute
方法。這時候的呼叫棧為:
- MASConstraint.m
- (MASConstraint *)width {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeWidth];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute __unused)layoutAttribute {
MASMethodNotImplemented();
}
複製程式碼
- MASViewConstraint.m
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}
複製程式碼
這其中,self.delegate
是什麼呢?如2.3.1節所述,MASConstraintMaker.m 中設定了 MASViewConstraint
類 newConstraint
物件的 MASConstraintDelegate
代理為“self”
(即 MASConstraintMaker
),其作用就是為了能夠同時設定多個約束屬性,即鏈式語法。所以,第二個設定約束屬性跟第一個設定約束屬性最終 呼叫的方法一樣(都是MASConstraintMaker.m中實現的addConstraintWithLayoutAttribute
)。
- MASConstraintMaker.m
- (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;
}
複製程式碼
當設定 第二次約束屬性 並執行完之後,我們還可以發現 constraint
不為 nil
,而是一個 MASViewConstraint
物件 ,所以該方法呼叫棧返回的不是 MASViewConstraint
物件,而是 MASCompositeConstraint
這個物件了,下面我們來看看這個類。
2.6 約束的集合: MASCompositeConstraint
MASCompositeConstraint
是約束的集合,它裡面有個私有的陣列用來存放多個 MASViewAttribute
物件。
make.height.width.equalTo(@20)
複製程式碼
當設定 第二個約束屬性,走到 .width
時,最終走的是:
- MASConstraintMaker.m
- (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
判讀裡面,將 .height
.wight
兩條約束 MASViewConstraint
物件塞到陣列裡,建立 MASCompositeConstraint
物件,並且同樣設定了 delegate
,最後還把 self.constraints
裡面事先新增好的約束 MASViewConstraint
物件替換成了 MASCompositeConstraint
物件。
#pragma mark - MASConstraintDelegate
- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint {
NSUInteger index = [self.childConstraints indexOfObject:constraint];
NSAssert(index != NSNotFound, @"Could not find constraint %@", constraint);
[self.childConstraints replaceObjectAtIndex:index withObject:replacementConstraint];
}
複製程式碼
另外,我們可以點選 MASCompositeConstraint
初始化方法裡看看,它內部會通過 for
迴圈,把陣列裡面的所有 MASViewConstraint
物件同樣設定了 delegate
。
- (id)initWithChildren:(NSArray *)children {
self = [super init];
if (!self) return nil;
_childConstraints = [children mutableCopy];
for (MASConstraint *constraint in _childConstraints) {
constraint.delegate = self;
}
return self;
}
複製程式碼
這麼做的目的同時是為了能夠繼續鏈式呼叫,比如我們再設定第三個約束屬性 .left
make.height.width.left.equalTo(@20);
複製程式碼
這時候的呼叫棧如下:
- MASConstraint.m
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
複製程式碼
- MASCompositeConstraint.m
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
[self constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
return self;
}
- (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;
}
複製程式碼
可以發現,這裡又是通過 delegate 方式,呼叫 MASConstraintMaker
工廠類中的:
- MASConstraintMaker.m
- (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;
}
複製程式碼
此時,注意到兩個 if
體都沒有走進去,既不像第一次,也不像第二次約束設定的時候。所以,這次僅僅是初始化了個 MASViewConstraint
物件就直接返回了,然後回到上個方法中新增到 MASCompositeConstraint
的私有陣列 self.childConstraints
中返回備用。
關於三次 約束設定之後的 .equalTo(@20)
,因為執行完 .left
時,返回的是 MASCompositeConstraint
物件,到這一步的時候會有點變化,呼叫棧如下:
- MASConstraint.m
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
複製程式碼
- MASCompositeConstraint.m
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attr, NSLayoutRelation relation) {
for (MASConstraint *constraint in self.childConstraints.copy) {
constraint.equalToWithRelation(attr, relation);
}
return self;
};
}
複製程式碼
可以發現,這裡會迴圈之前準備好的私有陣列 self.childConstraints
,呼叫 MASViewConstraint.m 的 equalToWithRelation
方法,和上面講的一樣了。
2.7 新增約束到檢視
mas_makeConstraints
方法的最後會呼叫 [constraintMaker install]
方法來新增所有儲存在 self.constraints
陣列中的所有約束。
- 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;
}
複製程式碼
(1). 如果需要重新構建約束,也就是 呼叫 mas_remakeConstraints:
方法,會先取出檢視的所有約束,然後通過一個 for
迴圈,呼叫 uninstall
來清空所有約束:
(2). 如果不需要重新構建約束,會取出 self.constraints
陣列中準備好的約束,通過 for
迴圈,呼叫 install
來把約束新增到檢視上。
關於 install
,是基類 MASConstraint
的抽象方法,方法體由MASViewConstraint
或 MASCompositeConstraint
實現。而 MASCompositeConstraint
的 install
方法體中其實也是呼叫的由MASViewConstraint
類實現的install
。
- MASConstraint.m
- (void)install { MASMethodNotImplemented(); }
複製程式碼
- MASCompositeConstraint.m
- (void)install {
for (MASConstraint *constraint in self.childConstraints) {
constraint.updateExisting = self.updateExisting;
[constraint install];
}
}
複製程式碼
- MASViewConstraint.m
這裡程式碼較多,就不分開解析了,直接分為7步寫到原始碼的註釋中,如下所示:
- (void)install {
//【1】如果約束以及存在並是 active 會直接返回。
if (self.hasBeenInstalled) {
return;
}
//【2】如果 self.layoutConstraint 響應了 isActive 方法並且不為空,會啟用這條約束並新增到 mas_installedConstraints 陣列中,最後返回。
if ([self supportsActiveProperty] && self.layoutConstraint) {
self.layoutConstraint.active = YES;
[self.firstViewAttribute.view.mas_installedConstraints addObject:self];
return;
}
//【3】這邊是獲取即將用於初始化 NSLayoutConstraint 的子類 MASLayoutConstraint 的幾個屬性。
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)
//【4】這邊是判斷當前即將新增的約束是否是 size 型別的並且 self.secondViewAttribute 也就是約束的第二個引數是 nil,(eg make.left.equalTo(@10))會自動將約束新增到約束的第一個引數檢視的 superview 上。
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
secondLayoutItem = self.firstViewAttribute.view.superview;
secondLayoutAttribute = firstLayoutAttribute;
}
//【5】然後就會初始化 NSLayoutConstraint 的子類 MASLayoutConstraint。
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;
//【6】這段程式碼會先判斷是否有約束第二個引數的檢視,有的話會尋找約束第一個和第二引數檢視的公共 Superview,相當於求兩個數的最小公倍數;如果不滿足第一個條件,會判斷約束第一個引數是否是 size 型別的,是的話直接取到它的檢視;最後都不滿足會直接取到約束第一個引數檢視父檢視。
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) {
self.installedView = self.firstViewAttribute.view;
} else {
self.installedView = self.firstViewAttribute.view.superview;
}
//【7】如果需要升級當前的約束就會獲取原有的約束,並替換為新的約束,這樣就不需要再次為 view 安裝約束。如果原來的 view 中不存在可以升級的約束,那麼就會在上一步尋找到的 installedView 上面新增約束。
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];
}
}
複製程式碼
其中第【6】步中的mas_closestCommonSuperview
方法,它會尋找 firstLayoutItem 和 secondLayoutItem 兩個檢視的公共 superview, 相當於求兩個數的最小公倍數.
- 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;
}
複製程式碼
3. 順藤再摸瓜:Masonry其它鏈式語法的呼叫棧解析(選讀)
3.1 make.edges.equalTo(view)
- 例子
make.edges.equalTo(view)
複製程式碼
我們再來看看這種寫法,呼叫棧如下:
- MASConstraintMaker.m
- (MASConstraint *)edges {
return [self addConstraintWithAttributes:MASAttributeTop | MASAttributeLeft | MASAttributeRight | MASAttributeBottom];
}
- (MASConstraint *)addConstraintWithAttributes:(MASAttribute)attrs {
__unused MASAttribute anyAttribute = (MASAttributeLeft | MASAttributeRight | MASAttributeTop | MASAttributeBottom | MASAttributeLeading
| MASAttributeTrailing | MASAttributeWidth | MASAttributeHeight | MASAttributeCenterX
| MASAttributeCenterY |
......
NSMutableArray *attributes = [NSMutableArray array];
if (attrs & MASAttributeLeft) [attributes addObject:self.view.mas_left];
if (attrs & MASAttributeRight) [attributes addObject:self.view.mas_right];
if (attrs & MASAttributeTop) [attributes addObject:self.view.mas_top];
......
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;
}
複製程式碼
程式碼太多省略了一部分,可以發現這段程式碼作用就是返回一個包含多條約束的 MASCompositeConstraint
物件,接著後面的操作也都是一樣的了。
3.2 make.edges.equalTo(UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f));
上面3.1中例子的寫法還可以改成這樣:
make.edges.equalTo(UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f));
複製程式碼
這裡的 equalTo
需要注意下,它是一個巨集,定義在 MASConstraint.h 中:
- MASConstraint.h
#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__)))
#ifdef MAS_SHORTHAND_GLOBALS
#define equalTo(...) mas_equalTo(__VA_ARGS__)
#define greaterThanOrEqualTo(...) mas_greaterThanOrEqualTo(__VA_ARGS__)
#define lessThanOrEqualTo(...) mas_lessThanOrEqualTo(__VA_ARGS__)
#define offset(...) mas_offset(__VA_ARGS__)
複製程式碼
代入上述巨集定義,前面的程式碼等效成:
make.edges.equalTo(MASBoxValue(UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f)));
複製程式碼
可以發現,其實裡面呼叫的是 MASBoxValue
這個巨集,它將 C 和 Objective-C 語言中的一些基本資料結構比如說 double
CGPoint
CGSize
這些值用 NSValue
進行包裝。
這裡還支援直接呼叫 size、center 等,具體實現都差不多,就不熬述了:
make.center.equalTo(CGPointMake(0, 50));
make.size.equalTo(CGSizeMake(200, 100));
複製程式碼
3.3 make.height.equalTo(@[redView, blueView])
make.height.equalTo(@[redView, blueView])
複製程式碼
再來看看這種傳陣列的,在走到 .equalTo
時,最終會呼叫 MASViewConstraint.m 裡面的 equalToWithRelation
方法
- MASConstraint.m
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
複製程式碼
- MASViewConstraint.m
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
if ([attribute isKindOfClass:NSArray.class]) {
NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
NSMutableArray *children = NSMutableArray.new;
for (id attr in attribute) {
MASViewConstraint *viewConstraint = [self copy];
viewConstraint.layoutRelation = relation;
viewConstraint.secondViewAttribute = attr;
[children addObject:viewConstraint];
}
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self.delegate;
[self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;
} else { .... }
};
}
複製程式碼
這邊還是遍歷陣列,並且 MASViewConstraint
實現 NSCopying 協議
,呼叫 [self copy]
會建立 MASViewConstraint
物件
- (id)copyWithZone:(NSZone __unused *)zone {
MASViewConstraint *constraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:self.firstViewAttribute];
constraint.layoutConstant = self.layoutConstant;
constraint.layoutRelation = self.layoutRelation;
constraint.layoutPriority = self.layoutPriority;
constraint.layoutMultiplier = self.layoutMultiplier;
constraint.delegate = self.delegate;
return constraint;
}
複製程式碼
然後會根據傳的陣列裡面的 Value 型別來做不同的操作,前面講過就不熬述了:
- (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);
}
}
複製程式碼
最後便是生成 MASCompositeConstraint
物件,並通過 delegate
方式,呼叫 MASConstraintMaker
的方法,替換 self.constraints
陣列裡的約束:
- (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];
}
複製程式碼
4. 舉一反三:框架原始碼的學習啟示
4.1 簡化的設計模式:工廠類&工廠方法
MASConstraintMaker
類就是一個工廠類,負責建立MASConstraint
型別的物件(依賴於MASConstraint
介面,而不依賴於具體實現)。在UIView的View+MASAdditions
分類中就是呼叫的MASConstraintMaker
類中的一些方法。上述我們在使用Masonry給subView新增約束時,mas_makeConstraints
方法中的Block的引數就是MASConstraintMaker
的物件。使用者可以通過該Block回撥過來的MASConstraintMaker
物件給View指定要新增的約束以及該約束的值。該工廠中的constraints
屬性陣列就記錄了該工廠建立的所有MASConstraint
物件。
MASConstraintMaker
之所以成為約束工廠類,因為MASConstraintMaker
賦值建立NSLayoutConstraint
物件,因為Masonry將NSLayoutConstraint
類進一步封裝成了MASViewConstraint
,所以MASConstraintMaker
是負責建立MASViewConstraint
的物件,並呼叫MASViewConstraint
物件的Install
方法將該約束新增到相應的檢視中。
說了這麼多,總結一下,如果你呼叫maker.top
, maker.left
等等這些方法都會呼叫下方的工廠方法來建立相應的MASViewConstraint
物件,並記錄在工廠物件的約束陣列中。之所以能鏈式呼叫,就是講當前的工廠物件(MASConstraintMaker
)指定為MASViewConstraint
物件的代理,所以一個MASViewConstraint
物件就可以通過代理來呼叫工廠方法來建立另一個新的MASViewConstraint
物件了,此處用到了代理模式。
角色分析
-
Client:
UIView
,通過分類View+MASAdditions
來扮演 -
工廠類:
MASConstraintMaker
-
抽象產品:
MASConstraint
-
具體產品:
MASViewConstraint
,MASCompositeConstraint
4.2 真正的設計模式:組合模式
換一種角度看,Masonry 並非單純的工廠模式,而是採用了經典的 Composite 設計模式,中文可譯作組合模式。
4.2.1 經典 組合模式 中的參與者:
Client
- 通過 Component 介面操縱組合部件的物件。
Component
- 為組合中的物件宣告介面。
- 在適當的情況下,實現所有類共有介面的預設行為
- 宣告一個介面用於訪問和管理 Component 的子元件。
- 在遞迴結構中定義一個介面,用於訪問一個父部件,並在合適的情況下實現它。
Leaf
- 在組合中表示葉節點物件,葉節點沒有子節點。
- 在組合中定義圖元物件的行為。
Composite
- 定義有子部件的那些部件的行為。
- 在 Composite 介面中實現與子部件有關的操作。
4.2.2 從 組合模式 的角度看,Masonry 框架中的角色分析:
UIView
,通過分類View+MASAdditions
來呼叫Masonry
Client
MASConstraintMaker
Component
MASConstraint
Leaf
MASViewConstraint
Composite
MASCompositeConstraint
4.3 程式設計思想:鏈式程式設計
Objective-C是一門動態語言,它使用了一種動態的訊息傳送機制,即物件(object)或類(class)呼叫方法。而OC中的點語法則只能通過setter和getter方法作用於類的屬性,而不能作用於某個方法。想實現鏈式語法,只能通過類似block屬性的getter方法。
鏈式程式設計思想:核心思想為將block作為方法的返回值,且返回值的型別為呼叫者本身,並將該方法以setter的形式返回,這樣就可以實現了連續呼叫,即為鏈式程式設計。
【舉例】簡單使用鏈式程式設計思想實現一個簡單計算器的功能:
4.3.1 新建一個名為CaculateMaker的類,用於運算。
4.3.2 在CaculateMaker.h檔案中宣告一個方法add:
- CaculateMaker.h
// CaculateMaker.h
// ChainBlockTestApp
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface CaculateMaker : NSObject
@property (nonatomic, assign) CGFloat result;
- (CaculateMaker *(^)(CGFloat num))add;
@end
複製程式碼
4.3.3 在CaculateMaker.m檔案中實現這個方法:
- CaculateMaker.m
// CaculateMaker.m
// ChainBlockTestApp
#import "CaculateMaker.h"
@implementation CaculateMaker
- (CaculateMaker *(^)(CGFloat num))add;{
return ^CaculateMaker *(CGFloat num){
_result += num;
return self;
};
}
@end
複製程式碼
4.3.4 在viewController裡面匯入CaculateMaker.h檔案,然後呼叫add方法就完成了鏈式語法:
- ViewController.m
CaculateMaker *maker = [[CaculateMaker alloc] init];
maker.add(20).add(30);
複製程式碼
通過上面Masonry佈局可以看出,它為UIView寫了一個category,擴充了mas_makeConstraints
方法,並將MASConstraintMaker
物件作為block的引數傳遞,在block的實現裡完成UIView的佈局,提現了函數語言程式設計思想。
4.3.5 同樣,我們也可以給NSObject新增一個NSObject+Caculate的分類,完成加法操作:
- NSObject+Caculate.h
// NSObject+Caculate.h
// ChainBlockTestApp
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "CaculateMaker.h"
@interface NSObject (Caculate)
- (CGFloat)caculate:(void (^)(CaculateMaker *make))block;
@end
複製程式碼
- NSObject+Caculate.m
// NSObject+Caculate.m
// ChainBlockTestApp
#import "NSObject+Caculate.h"
@implementation NSObject (Caculate)
- (CGFloat)caculate:(void (^)(CaculateMaker *make))block;{
CaculateMaker *make = [[CaculateMaker alloc] init];
block(make);
return make.result;
}
@end
複製程式碼
4.3.6 最後在viewController裡面呼叫,就很輕鬆的實現了鏈式語法:
- ViewController.m
CGFloat result = [NSObject caculate:^(CaculateMaker *maker) {
maker.add(10).add(20).add(30);
}];
NSLog(@"結果為:%.2f",result);
複製程式碼
5. 參考閱讀
-
Masonry解析
- http://qiufeng.me/masonry
- https://www.cnblogs.com/ludashi/p/5591572.html
-
工廠模式
- https://www.jianshu.com/p/7b89b7f587f9
- https://www.jianshu.com/p/847af218b1f0
-
組合模式
- http://www.runoob.com/design-pattern/composite-pattern.html
- http://www.cnblogs.com/gaochundong/p/design_pattern_composite.html
- http://www.cnblogs.com/peida/archive/2008/09/09/1284686.html
-
鏈式程式設計
- https://www.jianshu.com/p/cb9252f5105b
- https://www.jianshu.com/p/ac8bdd3430e7