關於 Masonry 的一些思考(下)

前言

本篇文章是筆者對上篇文章《關於 Masonry 的一些思考》的一些本身的解答,哪裏有理解不到位的地方,請盡情拍磚。若是想先看無答案版,請前往上篇文章 《看完 Masonry 源碼後的幾點思考?》。git

圖片來自戴銘文章 《讀 SnapKit 和 Masonry 自動佈局框架源碼github

關於 Masonry 思考的解答

1. Masonry 都作了些什麼?

Masonry 是一個讓開發者用簡潔優雅的語法來調用原生 AutoLayout 進行佈局的輕量級框架。Masonry 擁有本身的 DSL 佈局語言,讓咱們能夠更具象地描述約束的增長與更新,讓約束的代碼也變得更加簡潔易讀、容易理解。編程

DSL 是一種基於特定領域的語言,它使工做更貼近於客戶的理解,而不是實現自己,這樣有利於開發過程當中,全部參與人員使用同一種語言進行交流。簡單來講,就是咱們只需描述出咱們想要什麼效果,而毋需涉及底層實現,這無疑下降了工做過程當中溝通協調的門檻。swift

語言過於蒼白,讓咱們 show code:api

原生 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 進行佈局:bash

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);
}];

複製代碼

代碼甚至能夠再精簡下:app

UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(superview).with.insets(padding);
}];
複製代碼

以上代碼來自 Masonry 的 github 介紹框架

通過上面的代碼比較,Masonry 語法的簡潔優雅效果是淺顯易見的。代碼不只變得精簡,並且閱讀成本也基本降到了最低。less

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 源碼的小夥伴帶來些不同的視角,若是對於文中有解讀不當的地方也請您不吝指出。

相關文章
相關標籤/搜索