Masonry 原始碼解讀
架構劃分
我把 Masonry 的架構大致劃分如下:
- Public 公開介面,如 make.mas_left, view.mas_width 這些方法。
- Core 包括 Constraint 建模和 Constraints Maker.
- Attribute 這裡放的是約束的一部分屬性模型。
- Utils 工具,如 Debug 和 Box Value.
Public
View + MASAdditions, NSArray + MASAdditions, ViewController + MASAdditions 這三個分類提供了 View / View Array / ViewController 的一些屬性的存取方法,如 mas_left, mas_topLayoutGuide 等,區分 iOS 和 Mac 平臺。
這裡的獲取 Views 之間的 Common Super View 和 make/update/remake constraints 的區別都是我很喜歡問的問題。
View + MASShorthandAdditions, NSArray + MASShorthandAdditions 這兩個分類為以上方法提供了 shorthand 介面,其中用巨集來簡化程式碼量的寫法很好玩,雖然也很常見。
Attribute
個人覺得 Masonry 中比較不好的是,類之間的命名有點意義不明,因為太相似了。所以必須首先在概念上區分它們:
MASAttribute
其實就是 NSLayoutAttribute 這個 enum 的 Masonry NS_OPTIONS 版本,宣告如下:
typedef NS_OPTIONS(NSInteger, MASAttribute) {
MASAttributeLeft = 1 << NSLayoutAttributeLeft,
MASAttributeRight = 1 << NSLayoutAttributeRight,
MASAttributeTop = 1 << NSLayoutAttributeTop,
MASAttributeBottom = 1 << NSLayoutAttributeBottom,
MASAttributeLeading = 1 << NSLayoutAttributeLeading,
MASAttributeTrailing = 1 << NSLayoutAttributeTrailing,
MASAttributeWidth = 1 << NSLayoutAttributeWidth,
MASAttributeHeight = 1 << NSLayoutAttributeHeight,
MASAttributeCenterX = 1 << NSLayoutAttributeCenterX,
MASAttributeCenterY = 1 << NSLayoutAttributeCenterY,
MASAttributeBaseline = 1 << NSLayoutAttributeBaseline,
#if TARGET_OS_IPHONE
MASAttributeLeftMargin = 1 << NSLayoutAttributeLeftMargin,
MASAttributeRightMargin = 1 << NSLayoutAttributeRightMargin,
MASAttributeTopMargin = 1 << NSLayoutAttributeTopMargin,
MASAttributeBottomMargin = 1 << NSLayoutAttributeBottomMargin,
MASAttributeLeadingMargin = 1 << NSLayoutAttributeLeadingMargin,
MASAttributeTrailingMargin = 1 << NSLayoutAttributeTrailingMargin,
MASAttributeCenterXWithinMargins = 1 << NSLayoutAttributeCenterXWithinMargins,
MASAttributeCenterYWithinMargins = 1 << NSLayoutAttributeCenterYWithinMargins,
#endif
};
MASViewAttribute
這是一個類,它將 MAS_VIEW (其實就是 UIView / NSView) 和 NSLayoutAttribute 封裝在一起了。舉一個 Masonry 方程式的例子:
make.left.equalTo(secondView.right);
注意這裡的 secondView.right ,就是一個 MASViewAttribute ,它可以用來描述 View 的屬性,如 left, right, bottom, top 等。
MASLayoutConstraint
這是一個類,它是 NSLayoutConstraint 的子類,唯一多出的是一個 mas_key ,用於 Debug.
其實它是下文要提到的 MASViewConstraint 的一個屬性,這個更加容易混淆。
Utils
MASUtilities
定義了區分 iOS 和 Mac 的巨集,如 MAS_VIEW, MAS_VIEW_CONTROLLER, MASEdgeInsets 等,還有重新定義了 UILayoutPriority.
MASAttachKeys 這個巨集把 View 和用於 Debug 的 keys 自動關聯起來了,非常便捷。實現必看,核心是 NSDictionaryOfVariableBindings.
最精彩的當然是 MASBoxValue 這個巨集:
#define MASBoxValue(value) _MASBoxValue(@encode(__typeof__((value))), (value))
涉及的知識點有:
* 可變引數讀取
* 讀取一個 value 的型別
* Box Value: 將基本資料型別如 int, long 等 wrap 成 NSNumber、將結構體如 CGPoint, CGize, MASEdgeInsets 等 wrap 成 NSValue.
NSLayoutConstraint + MASDebugAdditions
這個 Category 重寫了 NSLayoutConstraint 的 description 方法,為 NSLayoutConstraint 加上了開發者繫結的標識 key ,然後把一些 Relation, Attribute, Priority 等字串化,在除錯的時候看起來更加清晰明瞭。
注意結合上面的 MASAttachKeys 這個巨集來用。
Core
有了上面的積累,最後我們來看 Core 部分。 Core 部分做的工作就是 Make and Install Constraints to View.
MASConstraint / MASViewConstraint / MASCompositeConstraint
MASConstraint 是 NSObject 的子類,是一個抽象基類,它的初始化方法加了斷言機制,如果該方法被直接呼叫將會 crash,程式碼:
- (id)init {
NSAssert(![self isMemberOfClass:[MASConstraint class]], @"MASConstraint is an abstract class, you should not instantiate it directly.");
return [super init];
}
其中很多方法都是需要子類去實現的,如果子類沒實現這裡會主動拋異常。
MASViewConstraint 是 MASConstraint 的子類,它是單個 NSLayoutConstraint 的封裝,通過 MASViewAttribute 來初始化。
MASCompositeConstraint 是 MASConstraint 的子類,它是一組 NSLayoutConstraint 的封裝,可以通過一組 MASConstraint 來初始化。
mas_equalTo 和 mas_offset
下面的巨集是由 MASConstraint 來呼叫的:
/**
* Convenience auto-boxing macros for MASConstraint methods.
*
* Defining MAS_SHORTHAND_GLOBALS will turn on auto-boxing for default syntax.
* A potential drawback of this is that the unprefixed macros will appear in global scope.
*/
#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__)
#endif
@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_SHORTHAND_GLOBALS ,那麼呼叫 equalTo (如 make.left.equalTo(view.right); )呼叫的是 equalTo(…) 巨集,傳入引數的可以是 MASViewAttribute, UIView, NSValue, NSArray 等物件,然後呼叫 mas_equalTo(__VA_ARGS__
) 這個方法,實現程式碼如下:
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
這裡最終返回的是一個 Block ,Block 中更新當前約束的 attribute 和 relation 並最終返回新的 MASConstraint. 而 equalTo(view.right) 已經呼叫了這個 Block ,並返回了更新後的 MASConstraint. 鏈式語法就是這樣傳遞下去的。
如果定義了 MAS_SHORTHAND_GLOBALS 並呼叫 offset (如 make.left.equalTo(view.right).offset(10); ) 呼叫的將是 mas_offset(…) ,也就是 valueOffset 方法,所以 MASConstraint 中定義的 mas_offset 方法將永遠得不到呼叫,因為它總是被 mas_offset 這個巨集覆蓋了,實際呼叫的是 valueOffset 方法,該方法返回一個 Block ,在 Block 中改變了當前約束的 Layout Constant ,並返回 self ,也是一個 MASConstraint. 而 valueOffset(10) 已經呼叫了這個 Block ,並返回了更新後的 MASConstraint.
return type 和鏈式呼叫
注意 MASConstraint 的每一個 getter 方法呼叫返回的都是 (MASConstraint * (^)(…)) ,注意是一個 Block ,在後面加上引數後呼叫這個 Block 了,Block 的返回值就是更新後的 MASConstraint ,從而讓鏈式呼叫一直傳遞下去。
而 with 和 and 內部則是什麼都不做。
MASConstraintDelegate protocol
@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
當 MASConstraint 呼叫 attribtue 的時候,就會觸發約束的建立或更新,然後通過 MASConstraintDelegate 傳遞回去給 MASConstraintMaker 的 constraints 陣列,並通過 constraint:shouldBeReplacedWithConstraint: 方法更新該約束或通過 constraint:addConstraintWithLayoutAttribute: 方法來新增約束。
MASViewConstraint 類的 install 方法
最後看看 MASViewConstraint 中比較核心的 install 方法,程式碼如下:
- (void)install {
if (self.hasBeenInstalled) {
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)
// 如果 secondViewAttribute 為空,則預設為 superview
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
secondLayoutItem = self.firstViewAttribute.view.superview;
secondLayoutAttribute = firstLayoutAttribute;
}
#warning - Core Method
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;
// 獲取要裝配的 view
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;
}
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];
}
}
其實很簡單,就是建立 NSLayoutConstraint ,然後獲取要裝配約束的 view:
- 如果 secondView 存在則為 firstView 和 secondView 的 closest common super view
- 如果 secondView 不存在且新增的約束是 NSLayoutAttributeWidth / NSLayoutAttributeHeight 這種針對 firstView 自身的,則為 firstView 自身
- 如果 secondView 不存在且新增的約束不是針對 firstView 自身的,則為 firstView 的 super view
接著判斷是否約束已存在,存在就直接更新約束的 constant,不存在就把 NSLayoutConstraint 新增到要裝配的 view 上。
MASConstraintMaker
如果上面的都明白了,那這裡就很簡單了,顧名思義就是製造約束的。
在 make.left.equalTo(xxx); 這種呼叫中,開頭的總是 MASConstraintMaker *make ,而 left/right/top/bottom/… 這些方法呼叫返回的總是 MASConstraint ,目的很簡單,鏈式呼叫的連線點都是 MASConstraint 類啊。
MASConstraintMaker 類中有一個數組 NSMutableArray<__kindof MASConstraint *> *constraints
,對於每一行 make.xxx.xxx.xxx 這種鏈式呼叫最後都是建立一個新的 MASConstraint 並新增到 maker 的 constraints 陣列中。
最後我們看一下 MAS_VIEW 的 make/update/remake 的實現程式碼:
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
constraintMaker.updateExisting = YES;
block(constraintMaker);
return [constraintMaker install];
}
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
constraintMaker.removeExisting = YES;
block(constraintMaker);
return [constraintMaker install];
}
其中 block(constraintMaker) 的內容就是建立了各種關於 self (一個 view) 的約束,例如:
make.top.greaterThanOrEqualTo(superview.top).offset(padding);
make.left.equalTo(superview.left).offset(padding);
make.bottom.equalTo(blueView.top).offset(-padding);
make.right.equalTo(redView.left).offset(-padding);
make.width.equalTo(redView.width);
make.height.equalTo(redView.height);
make.height.equalTo(blueView.height);
以 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 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;
}
一句話,根據 NSLayoutAttribute 建立對應的 MASConstraint.
然後將其新增到 maker 的 constraints 陣列中。最後呼叫的是 [maker install] 方法,程式碼如下:
- (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;
}
其中 remakeConstraints 會將 maker.view 的所有已安裝約束先移除。然後遍歷 constraints 陣列中的 MASConstraint 元素,如果是 updateConstraints 還要設定約束的 updateExisting 標誌位,如果是 makeConstraints 則直接 install 對應的約束,詳細程式碼見上文中 MASViewConstraint 的 install 方法。
小結
最後請告訴我:
1.下面的程式碼背後發生了什麼事?
[view1 makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(view2.left).offset(10);
make.width.equalTo(100);
...
}];
2.Masonry 的鏈式呼叫是怎麼做到的?
喜歡本文的話,可以請我喝個可樂哦:
支付寶:
微信: