1. 程式人生 > >Masonry 原始碼解讀

Masonry 原始碼解讀

架構劃分

我把 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 的鏈式呼叫是怎麼做到的?

喜歡本文的話,可以請我喝個可樂哦:

支付寶:

支付寶

微信:
微信