從Masonry原理解析使用注意點

Masonry的原理解析以及使用

Masonry應該是目前使用最為廣泛的對于AutoLayout的封裝(Swift版本叫做SnapKit),但是大家對于Masonry的使用只是停留在基礎的方式,很少人會去理解Masonry內部去調用AutoLayout的具體原理,致使在UI上容易產生很多的沖突,導致Masonry的Crash等等情況;所以這篇文章主要是來解決上面提出的問題;

關于AutoLayout

要講Masonry必須從iOS的布局歷史開始,系統的UI布局大致分為3類:

  • Frame Layout
  • Auto Resizing
  • Auto Layout

所謂FrameLayout即通過設置view的frame屬性值從而控制view的位置以及大小;

Auto Resizing其實也是屬于FrameLayout的范疇,目的就是為了讓子view可以跟隨superview進行大小的調整;但是不足點就是Auto Resizing無法處理同級間的view布局以及無法讓superview根據子view進行反向的數據調整;

于是就出現了Autolayout,它是一種基于約束的布局系統;簡單來說Autolayout的本質其實就是解析一組多元一次方程,當要確定一個視圖的位置,也是需要確定視圖的橫縱坐標以及寬度和高度的,只是這個橫縱坐標和寬度高度不再是寫死的數值,而是根據約束計算得來,從而達到自動布局的效果;

view_formula_2x.png

約束的本質就是兩個view的線性關系,上圖就是一個基本的關系方程式RedView的位置其實是通過BlueView的位置來固定的;這里不做過多的講解有興趣的朋友可以去看官方文檔

AutoLayout的使用

官方對于AutoLayout的使用提供了3種方法;但是其中兩種本質都是其實都是使用NSLayoutConstraint對象進行約束;

  1. 使用xib以及storyboard進行布局,不過這種方式基本不用,因為不好用;
  2. 使用VFL語法進行約束,VFL簡直就是一種又臭又長的語法,非常不好用也很難記,需要掃盲的同學可以查看官方文檔,我就不多講了,因為我自己也不是很懂;
/* Create an array of constraints using an ASCII art-like visual format string.
 */
+ (NSArray<NSLayoutConstraint *> *)constraintsWithVisualFormat:(NSString *)format options:(NSLayoutFormatOptions)opts metrics:(nullable NSDictionary<NSString *, NSNumber *> *)metrics views:(NSDictionary<NSString *, id> *)views;

  1. 使用NSLayoutConstraint純代碼添加,這種方式的缺點就是會大大的增加代碼量,平均一個約束就需要寫大量的代碼,造成開發的效率大大降低;
/* Create constraints explicitly.  Constraints are of the form "view1.attr1 = view2.attr2 * multiplier + constant" 
 If your equation does not have a second view and attribute, use nil and NSLayoutAttributeNotAnAttribute.
 Use of this method is not recommended. Constraints should be created using anchor objects on views and layout guides.
 */
+ (instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(nullable id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c;

上面曾經說過三種Autolayout的布局方法有兩種的本質是一樣的,就是因為VFL的本質其實就是返回多個NSLayoutConstraint對象,而不需要直接一個一個的創建NSLayoutConstraint對象大量減少代碼量;個人認為要不是VFL的語法太過于變態,如果簡單好用也就沒有Masonry什么事了;

Masonry的本質其實就是通過鏈式的語法將一個一個約束關系記錄下來,然后通過創建一個一個NSLayoutConstraint對象進行布局約束,Masonry內部的本質其實這樣并不復雜,只是存在很多細節點,導致直接使用的它的人會存在許多疑惑點,約束間的關系理不清;

關于Masonry中的宏定義

一下有兩段代碼,其中一個使用了mas_定義另一個沒有使用mas_定義,我想絕大部分人在使用的過程中肯定會充滿疑惑,好像不管使用哪一個都沒有問題;

// 使用宏定義
[view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.mas_equalTo(self.mas_top).mas_offset(20.f);
    make.leading.mas_equalTo(self.mas_leading).mas_offset(20.f);
    make.size.mas_equalTo(CGSizeMake(100.f, 100.f));
}];

// 不使用宏定義
[view makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(self.top).offset(20.f);
    make.leading.equalTo(self.leading).offset(20.f);
    make.size.equalTo(CGSizeMake(100.f, 100.f));
}];

其實上述兩種布局最后的效果都是一樣的;原因就是非mas_定義的本質調用還是調用mas_定義的聲明;

- (NSArray *)makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
    return [self mas_makeConstraints:block];
}

- (NSArray *)updateConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
    return [self mas_updateConstraints:block];
}

- (NSArray *)remakeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
    return [self mas_remakeConstraints:block];
}

對于makeConstraints(updateConstraints && remakeConstraints)三個函數來說,本質還是調用了mas_makeConstraints(mas_updateConstraints && mas_remakeConstraints);

@property (nonatomic, strong, readonly) MASViewAttribute *mas_leading;

#define MAS_ATTR_FORWARD(attr)  \
- (MASViewAttribute *)attr {    \
    return [self mas_##attr];   \
}

MAS_ATTR_FORWARD(leading);

對于leading這些屬性來說,leading的調用實際調用的還是mas_leading;

#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

/**
 *  Sets the constraint relation to NSLayoutRelationEqual
 *  returns a block which accepts one of the following:
 *    MASViewAttribute, UIView, NSValue, NSArray
 *  see readme for more details.
 */
- (MASConstraint * (^)(id attr))equalTo;

/**
 *  Sets the constraint relation to NSLayoutRelationGreaterThanOrEqual
 *  returns a block which accepts one of the following:
 *    MASViewAttribute, UIView, NSValue, NSArray
 *  see readme for more details.
 */
- (MASConstraint * (^)(id attr))greaterThanOrEqualTo;

/**
 *  Sets the constraint relation to NSLayoutRelationLessThanOrEqual
 *  returns a block which accepts one of the following:
 *    MASViewAttribute, UIView, NSValue, NSArray
 *  see readme for more details.
 */
- (MASConstraint * (^)(id attr))lessThanOrEqualTo;

/**
 *  Modifies the NSLayoutConstraint constant based on a value type
 */
- (MASConstraint * (^)(NSValue *value))valueOffset;

我們可以發現對于equalTo函數默認接受的應該是一個NSObject的對象,但是我們卻可以傳入一個CGSize以及CGFloat這類參數,原因就是因為存在一個equalTo的宏定義調用了mas_equalTo,而mas_equalTo實際調用了equalTo函數只是將參數轉換成了NSObject對象;
所以我們可以得到以下結果:
無論傳入的參數是否是NSObject對象,equalTo和mas_equalTo最后調用的都是equalTo函數,需要注意的是equalTo宏定義和equalTo函數雖然本質調用一樣,但是是屬于不同函數;

關于Masonry的數據轉換

通過上面的宏定義我們存在一個疑惑點,為一個非NSObject對象可以被equalTo接受呢?原因就是下面這個函數噶會的作用;

/**
 *  Given a scalar or struct value, wraps it in NSValue
 *  Based on EXPObjectify: https://github.com/specta/expecta
 */
static inline id _MASBoxValue(const char *type, ...) {
    va_list v;
    va_start(v, type);
    id obj = nil;
    if (strcmp(type, @encode(id)) == 0) {
        id actual = va_arg(v, id);
        obj = actual;
    } else if (strcmp(type, @encode(CGPoint)) == 0) {
        CGPoint actual = (CGPoint)va_arg(v, CGPoint);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(CGSize)) == 0) {
        CGSize actual = (CGSize)va_arg(v, CGSize);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(MASEdgeInsets)) == 0) {
        MASEdgeInsets actual = (MASEdgeInsets)va_arg(v, MASEdgeInsets);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(double)) == 0) {
        double actual = (double)va_arg(v, double);
        obj = [NSNumber numberWithDouble:actual];
    } else if (strcmp(type, @encode(float)) == 0) {
        float actual = (float)va_arg(v, double);
        obj = [NSNumber numberWithFloat:actual];
    } else if (strcmp(type, @encode(int)) == 0) {
        int actual = (int)va_arg(v, int);
        obj = [NSNumber numberWithInt:actual];
    } else if (strcmp(type, @encode(long)) == 0) {
        long actual = (long)va_arg(v, long);
        obj = [NSNumber numberWithLong:actual];
    } else if (strcmp(type, @encode(long long)) == 0) {
        long long actual = (long long)va_arg(v, long long);
        obj = [NSNumber numberWithLongLong:actual];
    } else if (strcmp(type, @encode(short)) == 0) {
        short actual = (short)va_arg(v, int);
        obj = [NSNumber numberWithShort:actual];
    } else if (strcmp(type, @encode(char)) == 0) {
        char actual = (char)va_arg(v, int);
        obj = [NSNumber numberWithChar:actual];
    } else if (strcmp(type, @encode(bool)) == 0) {
        bool actual = (bool)va_arg(v, int);
        obj = [NSNumber numberWithBool:actual];
    } else if (strcmp(type, @encode(unsigned char)) == 0) {
        unsigned char actual = (unsigned char)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedChar:actual];
    } else if (strcmp(type, @encode(unsigned int)) == 0) {
        unsigned int actual = (unsigned int)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedInt:actual];
    } else if (strcmp(type, @encode(unsigned long)) == 0) {
        unsigned long actual = (unsigned long)va_arg(v, unsigned long);
        obj = [NSNumber numberWithUnsignedLong:actual];
    } else if (strcmp(type, @encode(unsigned long long)) == 0) {
        unsigned long long actual = (unsigned long long)va_arg(v, unsigned long long);
        obj = [NSNumber numberWithUnsignedLongLong:actual];
    } else if (strcmp(type, @encode(unsigned short)) == 0) {
        unsigned short actual = (unsigned short)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedShort:actual];
    }
    va_end(v);
    return obj;
}

對于所有傳入的參數無論是否是NSObject對象,都會通過_MASBoxValue這個函數進行數據的封裝,將所有參數都轉換成一個NSObject對象

- (void)setLayoutConstantWithValue:(NSValue *)value {
    if ([value isKindOfClass:NSNumber.class]) {
        self.offset = [(NSNumber *)value doubleValue];
    } else if (strcmp(value.objCType, @encode(CGPoint)) == 0) {
        CGPoint point;
        [value getValue:&point];
        self.centerOffset = point;
    } else if (strcmp(value.objCType, @encode(CGSize)) == 0) {
        CGSize size;
        [value getValue:&size];
        self.sizeOffset = size;
    } else if (strcmp(value.objCType, @encode(MASEdgeInsets)) == 0) {
        MASEdgeInsets insets;
        [value getValue:&insets];
        self.insets = insets;
    } else {
        NSAssert(NO, @"attempting to set layout constant with unsupported value: %@", value);
    }
}

最后對于那些原本非NSObject對象在進行反向解析,設置對應的值;

關于Masonry的結構

下圖是網上一張很詳細介紹Masonry結構的一張結構圖,這里引用一下,因為我不想很具體的去介紹每一句代碼,只把最核心的幾點告訴大家;


1829891-2157823aedb4e4dc.png

上圖的大致流程其實很通俗易懂,因為我們這樣想,我們最主要的目的無非就是講一個一個NSLayoutConstraint約束抽象成我們能夠簡單通俗的編寫方式,所以Masonry的主要流程其實就是每個view提供給用戶一個MASConstraintMaker對象,讓用戶不斷在MASConstraintMaker對象上添加一個一個MASConstraint的約束結構,最后將所有的MASConstraint轉化成一個一個NSLayoutConstraint對象添加在相應的view上面;

接下來的很多概念都需要用到上面的結構;

關于mas_makeConstraints,mas_updateConstraints,mas_remakeConstraints的區別理解

我相信上面三個函數大家一定不會陌生,而且應該也知道對應的使用場景;
mas_makeConstraints就是創建一個新的約束
mas_updateConstraints就是更新一個原有的約束
mas_remakeConstraints就是移除現有的約束,添加新的約束;

介紹一下主要的原理,每個MASConstraintMaker對象有兩個updateExisting && removeExisting屬性,用來保存當前的maker的約束方式

// MASConstraintMaker
/**
 *  Whether or not to check for an existing constraint instead of adding constraint
 */
@property (nonatomic, assign) BOOL updateExisting;

/**
 *  Whether or not to remove existing constraints prior to installing
 */
@property (nonatomic, assign) BOOL removeExisting;

所以當調用mas_makeConstraints && mas_updateConstraints && mas_remakeConstraints這三個函數的時候,最后都會去執行install這個操作,而install里面本身就會判斷如果是remakeConstraints那么它就會移除所有舊的約束,然后添加新的約束;對于updateConstraints && makeConstraints只是
添加新的約束,但是MASConstraint本身會保存當前的約束是更新約束還是新加約束;

// MASConstraintMaker
- (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;
}

那么對于updateConstraints && makeConstraints內部對于updateExisting的區別其實很簡單,如果updateExisting為true,那么就從當前的view去找是否存在和當前約束一樣的約束,然后更新約束的constant,我們可以從layoutConstraintSimilarTo函數可以看到,判斷約束是否存在的標準就是除了constant以外的所有屬性;如果updateExisting為false,那么就是直接添加新的約束;

// MASConstraint
/**
 *  Whether or not to check for an existing constraint instead of adding constraint
 */
@property (nonatomic, assign) BOOL updateExisting;

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

- (MASLayoutConstraint *)layoutConstraintSimilarTo:(MASLayoutConstraint *)layoutConstraint {
    // check if any constraints are the same apart from the only mutable property constant

    // go through constraints in reverse as we do not want to match auto-resizing or interface builder constraints
    // and they are likely to be added first.
    for (NSLayoutConstraint *existingConstraint in self.installedView.constraints.reverseObjectEnumerator) {
        if (![existingConstraint isKindOfClass:MASLayoutConstraint.class]) continue;
        if (existingConstraint.firstItem != layoutConstraint.firstItem) continue;
        if (existingConstraint.secondItem != layoutConstraint.secondItem) continue;
        if (existingConstraint.firstAttribute != layoutConstraint.firstAttribute) continue;
        if (existingConstraint.secondAttribute != layoutConstraint.secondAttribute) continue;
        if (existingConstraint.relation != layoutConstraint.relation) continue;
        if (existingConstraint.multiplier != layoutConstraint.multiplier) continue;
        if (existingConstraint.priority != layoutConstraint.priority) continue;

        return (id)existingConstraint;
    }
    return nil;
}

關于Masonry為什么可以鏈式調用

其實Masonry可以鏈式調用無非就是為了縮減代碼量,沒有其他任何原因;從我們之前的結構圖可以看到MASConstraintMaker對象包函了大量的MASConstraint屬性對象,而MASConstraint屬性對象里面還是定義大量的MASConstraint屬性,于是就可以不斷返回MASConstraint的對象;于是問題來了,它是怎么做到將每個MASConstraint對象都保存起來呢?

// MASConstraint
/**
 *  Usually MASConstraintMaker but could be a parent MASConstraint
 */
@property (nonatomic, weak) id<MASConstraintDelegate> delegate;

// MASConstraintMaker
@interface MASConstraintMaker () <MASConstraintDelegate>

@property (nonatomic, weak) MAS_VIEW *view;
@property (nonatomic, strong) NSMutableArray *constraints;

@end

- (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;
}

@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對象都有一個MASConstraintDelegate的代理,而MASConstraintMaker實現了這個代理,所以所有生成MASConstraint的任務其實最后都是通過MASConstraintMaker來實現的,并通過constraints進行保存;

關于Masonry如何正確添加對應的View關系

可能很多人存在一個疑惑點,Masonry是如何正確的添加每個約束關系到對應的View上呢?不理解我這句話的同學可以自行寫一個約束,然后打印對應的view的constraints可以發現,每個約束都有自己的對應關系,有的添加在superview上面,有些是添加在自己的view上面;那么Masonry是怎么做的呢?

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

我們發現對于存在secondView的情況,那么firstView和secondView的最近公共view就是約束需要添加的view,如果firstView是設置size的大小(包括單獨的寬高),那么需要添加約束的就是自身的view,其他情況一律都是firstView的superview(比如make.center.offset(10)這類操作);

關于Masonry的一些缺省寫法

很多人會在代碼中會寫如下的代碼,以下兩段代碼實現效果是一樣的,但是其中一段是缺省代碼,那么為什么缺省的寫法也是可以正確實現呢?

[view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.equalTo(self.view).offset(20.f);
    make.top.equalTo(self.view).offset(20.f);
    make.size.equalTo(CGSizeMake(120.f, 120.f));
}];

[view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.equalTo(self.view.left).offset(20.f);
    make.top.equalTo(self.view.top).offset(20.f);
    make.size.equalTo(CGSizeMake(120.f, 120.f));
}];

原因就在于Masonry會對系統的缺省值進行補充,如果在equalTo的時候傳入secondViewAttribute是UIView對象,那么使用的約束類型就是該firstView的約束屬性,如果傳入的secondViewAttribute是secondView的約束屬性,那么就直接使用;

- (void)setSecondViewAttribute:(id)secondViewAttribute {
    if ([secondViewAttribute isKindOfClass:NSValue.class]) {
        [self setLayoutConstantWithValue:secondViewAttribute];
    } else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
        _secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
    } else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
        MASViewAttribute *attr = secondViewAttribute;
        if (attr.layoutAttribute == NSLayoutAttributeNotAnAttribute) {
            _secondViewAttribute = [[MASViewAttribute alloc] initWithView:attr.view item:attr.item layoutAttribute:self.firstViewAttribute.layoutAttribute];;
        } else {
            _secondViewAttribute = secondViewAttribute;
        }
    } else {
        NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
    }
}

Masonry細節解析

大家想看Masonry具體細節原理具體看這篇文章,代碼上沒有什么難度只要理解思想基本就能看明白;

關于Masonry的屬性優先級注意點

這里要說的主要是關于View自身內容尺寸(Intrinsic Content Size),抗壓縮抗拉伸(Compression-Resistance and Content-Hugging)這兩種屬性的概念介紹,這點是需要好好理解的,這些主要影響到的就是關于約束的優先級關系,我這里不做多講有興趣可以去看這篇文章

關于Masonry的性能

其實AutoLayout的性能不是很好,想想也知道要在主線程解析多元一次方程,對于復雜的界面布局效率可想而知具體可以參見這篇文章;

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容