關於 Masonry 的一些思考(下)

Swants發表於2018-06-11

前言

本篇文章是筆者對上篇文章《關於 Masonry 的一些思考》的一些自己的解答,哪裡有理解不到位的地方,請盡情拍磚。如果想先看無答案版,請前往上篇文章 《看完 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);
    }];
複製程式碼

不會造成迴圈引用,而 MJRefreshheader 初始化方法

self.scrollView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
        NSLog(@"%@", self);
    }];
複製程式碼

為什麼會造成迴圈引用?

MJRefreshheaderWithRefreshingBlock: 方法內部,返回的 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_SHORTHANDMAS_SHORTHAND_GLOBALS 巨集是做什麼用的?它的效果是如何實現的呢?

MAS_SHORTHAND 巨集可以在呼叫 Masonry api 的時候省去 mas_ 字首

MasonryView 定義了 一個 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. MasonrymakeConstraints: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 物件,需要注意的是:

  • 該物件已儲存了呼叫 viewFirstView) 和 leftFirstAttribute

  • 該物件已被新增到 makeconstraints 陣列內儲存

MASViewConstraint.right 生成並返回了 MASCompositeConstraint 物件,需要注意的是:

  • MASCompositeConstraint 物件儲存了包含 lefttop 的兩條 MASViewConstraint 物件
  • makeconstraints 陣列之前儲存的 MASViewConstraint 物件被替換為該 MASCompositeConstraint 物件

MASCompositeConstraint.top 返回之前的 MASCompositeConstraint 物件,需要注意的是:

  • MASCompositeConstraint 增加了一條 top 約束

MASCompositeConstraint .equalTo(self.view) 返回之前的 MASCompositeConstraint

  • 遍歷 MASCompositeConstraint 儲存的幾條約束, 為他們設定 layoutRelationsecondViewsecondAttribute
  • 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 型別呼叫。所以前一個方法的返回值正好作為下一個方法的呼叫者,而呼叫過的所有方法修改的約束都被 makerconstraints 所記錄下來。隨後在 [constraintMaker install]; 的時候遍歷 constraints 執行 [constraint install]

7.MASViewConstraint 為什麼要弱引用一個 MASLayoutConstraint 的例項物件,它又用這個物件做了什麼?

Masonry 庫最後都會生成一個 MASVIewConstraint 物件,Masonry 會根據這個物件生成系統原生 NSLayoutConstraint 約束的建立,而後期可能要對這個原生約束進行一些移除操作。需要記錄這個原生約束物件。

8.MASConstraintMaker 持有一個 constraints 陣列, 而 MASViewConstrint 類也有一個用來記錄約束的陣列,這兩個陣列都是用來記錄生成的約束,那這兩個陣列有什麼區別嗎?各自的作用又是什麼?

MASConstraintMaker 的 陣列是記錄 本次 Masonry API 呼叫生成的約束,最後 make 將這個陣列內的約束遍歷安裝 install。 陣列裡儲存的是 MASViewConstraintMASCompositeConstraint 物件

MASViewConstrint 類的陣列,記錄的是 Masonry 呼叫者 View 已經安裝了哪些約束,這個陣列在後期呼叫者呼叫 updateConstraints: 時判斷,更新的約束是否已經安裝了 ,remakeConstraints: 方法時,需要根據陣列將已經安裝過的約束移除。陣列裡儲存的都是 MASViewConstrint 物件。

後記

儘管筆者水平有限,但對這些問題的拙劣見解還是奉上,希望可以給讀 Masonry 原始碼的小夥伴帶來些不一樣的視角,如果對於文中有解讀不當的地方也請您不吝指出。

相關文章