前言
本篇文章是筆者對上篇文章《關於 Masonry 的一些思考》的一些自己的解答,哪裡有理解不到位的地方,請盡情拍磚。如果想先看無答案版,請前往上篇文章 《看完 Masonry
原始碼後的幾點思考?》。
圖片來自戴銘文章 《讀 SnapKit 和 Masonry 自動佈局框架原始碼》
關於 Masonry 思考的解答
1. Masonry
都做了些什麼?
Masonry
是一個讓開發者用簡潔優雅的語法來呼叫原生 AutoLayout
進行佈局的輕量級框架。Masonry
擁有自己的 DSL
佈局語言,讓我們可以更具象地描述約束的增加與更新,讓約束的程式碼也變得更加簡潔易讀、容易理解。
DSL 是一種基於特定領域的語言,它使工作更貼近於客戶的理解,而不是實現本身,這樣有利於開發過程中,所有參與人員使用同一種語言進行交流。簡單來說,就是我們只需描述出我們想要什麼效果,而毋需涉及底層實現,這無疑降低了工作過程中溝通協調的門檻。
語言過於蒼白,讓我們 show code:
原生 AutoLayout
實現一個紅色 view 佈局
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 進行佈局:
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
make.left.equalTo(superview.mas_left).with.offset(padding.left);
make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];
複製程式碼
程式碼甚至可以再精簡下:
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superview).with.insets(padding);
}];
複製程式碼
以上程式碼來自 Masonry 的 github 介紹
經過上面的程式碼比較,Masonry
語法的簡潔優雅效果是淺顯易見的。程式碼不僅變得精簡,而且閱讀成本也基本降到了最低。
2. 下面程式碼會發生迴圈引用嗎,為什麼?
[self.view addSubview:btn];
[btn makeConstrants:^(MASLayoutConstraint *make){
make.left.equalTo(self.view).offset(12);
}];
複製程式碼
答: 不會發生迴圈引用,方法中 block
引數雖然引用 self.view
,間接持有了 btn
,但是 block
引數是個匿名 block,並且在方法實現裡未額外引用這個 block
引數, block
並未被 btn
所持有,也就不存在兩者相互持有、迴圈引用。block -> self.view -> btn -(未引用)- block
而上述方法定義中也明確使用 NS_NOESCAPE
修飾 block
引數, 這個修飾符表明 block
在方法執行完前就會被執行釋放,而不會對 block
進行額外的引用儲存。
- (NSArray *)mas_updateConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block
在程式碼中 Masonry
也確實是這麼做的:
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
複製程式碼
從上面程式碼中,可以清除地看到,block
引數在 return
之前就被執行,並未被其他物件引用。
更多關於 NS_NOESCAPE 的介紹
額外擴充,很多同學對
block
的迴圈引用都不太瞭解:同樣是匿名 block 引數,系統動畫
[UIView animateWithDuration:100 animations:^{
NSLog(@"%@",self);
}];
複製程式碼
不會造成迴圈引用,而 MJRefresh
的 header
初始化方法
self.scrollView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
NSLog(@"%@", self);
}];
複製程式碼
為什麼會造成迴圈引用?
MJRefresh
的 headerWithRefreshingBlock:
方法內部,返回的 MJRefreshNormalHeader
物件強引用了這個 block
,而這個返回物件最後又被 self.scrollView.mj_header
強引用了,也就造成了 self -> scrollView -> mj_header -> block -> self
的強引用閉環,因此會造成迴圈引用。
headerWithRefreshingBlock:
實現程式碼:
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
MJRefreshHeader *cmp = [[self alloc] init];
cmp.refreshingBlock = refreshingBlock;
return cmp;
}
複製程式碼
系統的動畫實現方法中,self
並未和這個 block
產生關聯,但 block
確實持有了 self
,但筆者猜測 block
對self 並不是強引用,因為如果在這個動畫時間內控制器執行 POP
操作,self
會立即被釋放掉,也就是說除了導航控制器棧,self
並未被額外的強引用,否則 self 不會被釋放。
self 未引用 block (弱)-> self 。因此也不存在迴圈引用
想一想,當方法中使用匿名
block
、匿名物件作為引數,這些匿名物件是被誰持有?會在什麼時候釋放呢?歡迎在評論中探討。
3. MAS_SHORTHAND
、MAS_SHORTHAND_GLOBALS
巨集是做什麼用的?它的效果是如何實現的呢?
MAS_SHORTHAND
巨集可以在呼叫 Masonry
api
的時候省去 mas_
字首
Masonry
為 View
定義了 一個 View+MASAdditions
分類。在這個分類中,所有的成員屬性和方法都是帶有 mas_
字首的。Masonry 還另外定義了 View+MASShorthandAdditions
分類,在這個分類中所有的所有屬性和成員變數都不帶 mas_ 字首。但這個分類被 #ifdef MAS_SHORTHAND #endif
所包裹。 效果如下:
//MASShorthandAdditions 分類
#ifdef MAS_SHORTHAND
...
...(不帶有 mas_ 字首的成員變數和方法)
...
#endif
複製程式碼
這樣只有定義了 MAS_SHORTHAND
之後這個分類才會被編譯,而這個分類內部所有屬性的 get
方法、對外的介面方法實現還是呼叫的帶有 mas_
字首的方法,對於我們開發者來說,只是在 mas_
屬性與方法外面包裹上了一層語法糖。
不帶有 mas_
字首方法的實現:
//屬性的 get 方法巨集,在屬性前拼接 mas_ 字首,呼叫帶有字首的屬性 get 方法
#define MAS_ATTR_FORWARD(attr) \
- (MASViewAttribute *)attr { \
return [self mas_##attr]; \
}
複製程式碼
//不帶有 mas_ 字首的 API,內部會呼叫帶有 mas_ 字首的 API
- (NSArray *)makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
return [self mas_makeConstraints:block];
}
複製程式碼
而MAS_SHORTHAND_GLOBALS
巨集會將 equalTo()
、 greaterThanOrEqualTo()
、 offset()
巨集定義為 mas_equalTo()
、 mas_greaterThanOrEqualTo()
、 mas_offset()
。
而帶有 mas_
字首的方法會將括號內的 block
引數從基本資料型別轉化為 NSValue
物件型別
#define mas_equalTo(...) equalTo(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
複製程式碼
4. Masonry
的 makeConstraints:
、updateConstraints:
、 remakeConstraints:
有什麼區別,分別適合那些場景?
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
複製程式碼
remakeConstraints:
和上面程式碼的唯一區別就是增加了constraintMaker.removeExisting = YES;
當 [constraintMaker install]
時,如果 removeExisting
判斷為 true
,會將已安裝的約束全部執行 [constraint uninstall]
解除安裝;
而 updateConstraints:
和上面程式碼的唯一區別就是在呼叫 block
之前增加了一句 constraintMaker.updateExisting = YES
標示。
當 [constraint install]
執行時,會判斷 updateExisting
的值, 如果為 true
會接著判斷約束和已安裝的約束是否相似,(判斷是否相似的規則是,兩條約束只有 constant
常量值不一樣,其它諸如 firstItem
secondItem
firstAttribute
secondAttribute
relation
multiplier
priority
必須和之前約束完全一致,才為相似約束。),如果存在相似約束,則進行約束更新,否則就新增這條約束。因此我們要十分注意 updateConstraints:
新更新的約束會不會和已有的約束衝突, 例如當我們之前約束為 make.right.equalTo(self.view).offset(-12);
更新後為 make.right.equalTo(self.view.mas_centerX).offset(-15);
這是兩條不相似的約束(secondAttribute
不一樣),如果更新約束,會造成約束衝突。
5. 描述下程式碼 make.left.right.top.equalTo(self.view).offset(0)
都做了些什麼?
make.left
生成並返回 MASViewConstraint
物件,需要注意的是:
-
該物件已儲存了呼叫
view
(FirstView
) 和left
(FirstAttribute
) -
該物件已被新增到
make
的constraints
陣列內儲存
MASViewConstraint.right
生成並返回了 MASCompositeConstraint
物件,需要注意的是:
MASCompositeConstraint
物件儲存了包含left
和top
的兩條MASViewConstraint
物件make
的constraints
陣列之前儲存的MASViewConstraint
物件被替換為該MASCompositeConstraint
物件
MASCompositeConstraint.top
返回之前的 MASCompositeConstraint
物件,需要注意的是:
MASCompositeConstraint
增加了一條top
約束
MASCompositeConstraint .equalTo(self.view)
返回之前的 MASCompositeConstraint
,
- 遍歷
MASCompositeConstraint
儲存的幾條約束, 為他們設定layoutRelation
、secondView
和secondAttribute
equalTo()
引數是view
型別,secondAttribute
依舊是nil
,會在最後約束安裝時如果判斷為nil
則值初始化為FirstAttribute
MASCompositeConstraint.offset
無返回值
- 遍歷
MASCompositeConstraint
儲存的幾條約束,為他們設定layoutConstant
最後約束安裝時 執行 [constraintMaker install]
; 就會根據 firstView
FirstAttribute
layoutRelation
secondView
secondAttribute
layoutConstant
來生成原生的約束 NSLayoutConstraint
,並將原生約束新增到 firstView
secondView
最近的公共父檢視上生效。
6. Masonry
是如何做到鏈式優雅呼叫的?
鏈式程式設計思想:簡單來說,是將多個操作(多行程式碼)通過點號(.)連結在一起成為一句程式碼,使程式碼可讀性好。a(1).b(2).c(3)
鏈式程式設計特點:方法的返回值是 block , block 必須有返回值(本身物件),block 引數就是需要操作的值。
那 make.left.right.top.bottom.equalTo(self.view).offset(12)
鏈式呼叫的具體過程是什麼樣的呢?
首先 Masonry
定義了一個 MASConstraint
抽象類 上面所有的方法返回值都是 MASConstraint
型別,而所有的呼叫者除了第一個為 MASConstraintMake
型別,其它都是 MASConstraint
型別呼叫。所以前一個方法的返回值正好作為下一個方法的呼叫者,而呼叫過的所有方法修改的約束都被 maker
的 constraints
所記錄下來。隨後在 [constraintMaker install]
; 的時候遍歷 constraints
執行 [constraint install]
7.MASViewConstraint
為什麼要弱引用一個 MASLayoutConstraint 的例項物件,它又用這個物件做了什麼?
Masonry
庫最後都會生成一個 MASVIewConstraint
物件,Masonry
會根據這個物件生成系統原生 NSLayoutConstraint
約束的建立,而後期可能要對這個原生約束進行一些移除操作。需要記錄這個原生約束物件。
8.MASConstraintMaker
持有一個 constraints
陣列, 而 MASViewConstrint
類也有一個用來記錄約束的陣列,這兩個陣列都是用來記錄生成的約束,那這兩個陣列有什麼區別嗎?各自的作用又是什麼?
MASConstraintMaker
的 陣列是記錄 本次 Masonry
API
呼叫生成的約束,最後 make
將這個陣列內的約束遍歷安裝 install
。
陣列裡儲存的是 MASViewConstraint
和 MASCompositeConstraint
物件
而 MASViewConstrint
類的陣列,記錄的是 Masonry
呼叫者 View
已經安裝了哪些約束,這個陣列在後期呼叫者呼叫 updateConstraints:
時判斷,更新的約束是否已經安裝了 ,remakeConstraints:
方法時,需要根據陣列將已經安裝過的約束移除。陣列裡儲存的都是 MASViewConstrint
物件。
後記
儘管筆者水平有限,但對這些問題的拙劣見解還是奉上,希望可以給讀 Masonry
原始碼的小夥伴帶來些不一樣的視角,如果對於文中有解讀不當的地方也請您不吝指出。