原始碼閱讀:Masonry(一)——從使用入手

堯少羽發表於2018-08-03

該文章閱讀的Masonry的版本為1.1.0。

0.原生實現

本來不想貼直接使用原生API的實現方式,但是文章寫到一半發現沒有對原生API的解釋,Masonry的實現也不太好解釋,於是就新增了這個第0節,對NSLayoutConstraint這個類稍微介紹一下。

如果我們直接用官方提供的NSLayoutConstraint類進行佈局,應該這樣寫:

UIView *redView = [UIView new];
redView.backgroundColor = [UIColor redColor];
redView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:redView];
    
NSLayoutConstraint *constraint1 = [NSLayoutConstraint constraintWithItem:redView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:100.0];
NSLayoutConstraint *constraint2 = [NSLayoutConstraint constraintWithItem:redView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:200.0];
NSLayoutConstraint *constraint3 = [NSLayoutConstraint constraintWithItem:redView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:200];
NSLayoutConstraint *constraint4 = [NSLayoutConstraint constraintWithItem:redView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0];
NSArray<NSLayoutConstraint *> *constraints = @[constraint1, constraint2, constraint3, constraint4];
[self.view addConstraints:constraints];
複製程式碼
  • 首先把要設定autolayout的控制元件的translatesAutoresizingMaskIntoConstraints屬性設定為NO,這個屬性預設是YES
  • 然後通過NSLayoutConstraint類提供的工廠方法constraintWithItem: attribute: relatedBy: toItem: attribute: multiplier: constant:建立約束物件。
  • 最後利用控制元件的addConstraints:方法,將約束物件新增到控制元件上。

其實直接使用NSLayoutConstraint新增約束並不難也很好理解,就是太冗雜了。我們重點來看NSLayoutConstraint類例項化物件的工廠方法:

+(instancetype)constraintWithItem:(id)view1
                        attribute:(NSLayoutAttribute)attr1
                        relatedBy:(NSLayoutRelation)relation
                           toItem:(nullable id)view2
                        attribute:(NSLayoutAttribute)attr2
                       multiplier:(CGFloat)multiplier
                         constant:(CGFloat)c;
複製程式碼

看這個方法的目的是理解其各個引數的意義,這樣在接下里看Masonry時,就能好理解的多:

  • 蘋果官方文件中給出的約束公式是:view1.attr1 <relation> multiplier × view2.attr2 + c
  • view1:是指要設定的約束的目標檢視。
  • attr1:是指view1要設定約束的屬性,是檢視的頂部、寬度,還是其他什麼的。
  • relation:是指兩個檢視屬性的關係,一共有三種,分別是不大於、等於和不小於。
  • view2:是指要設定約束的參考檢視。
  • attr2:是指view2要設定約束的屬性。
  • multiplier:是指約束要乘的倍率。
  • c:是指約束要加的大小

1.使用Masonry

使用Masonry佈局就簡潔很多,同樣的佈局如下:

UIView *redView = [UIView new];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
    
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.size.mas_equalTo(CGSizeMake(200.0, 100.0));
    make.top.equalTo(self.view).offset(50.0);
    make.centerX.equalTo(self.view);
}];
複製程式碼

2.建立佈局環境

通過上一節可以看到所有的佈局都是在mas_makeConstraints:方法中進行的,點選方法進入檢視實現:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    // 用autolayout佈局要設定為NO
    self.translatesAutoresizingMaskIntoConstraints = NO;
    // 建立約束建立者物件
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    // 通過block回撥約束建立者物件
    block(constraintMaker);
    // 返回所有新增的約束
    return [constraintMaker install];
}
複製程式碼

在這個方法的實現中,就是建立一個管理約束的物件,然後通過block回撥用以新增約束,新增完成後設定新增的約束。

這個方法更像是建立了一個用於設定約束的環境,使用者只需要通過block設定約束即可,其他的都不需要操心。

3.新增約束

我們可以通過MASConstraintMaker類物件提供的屬性為控制元件新增各種各樣的約束,我們選取一個來檢視其具體實現:

make.top.equalTo(self.view).offset(50.0);
複製程式碼

3.1 屬性

在上一節中,我們已經知道了物件makeMASConstraintMaker型別,所以直接進入MASConstraintMaker類中檢視其top屬性的實現:

- (MASConstraint *)top {
    // 呼叫了另一個方法,並把要設定約束的位置傳遞過去
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
複製程式碼

繼續點選檢視:

- (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];
    // 如果傳入的約束物件是MASViewConstraint類及其子類
    if ([constraint isKindOfClass:MASViewConstraint.class]) {
        // 利用已經新增的約束物件和新新增的約束物件建立多檢視約束封裝物件
        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;
}
複製程式碼

在這一步中,可以看到會返回一個MASViewConstraint類或MASCompositeConstraint類的物件,這兩個類都是MASConstraint的子類,可以說是“兄弟類”,它們儲存了約束屬性及其所在的檢視,也就是 view1attr1

3.2 關係

- (MASConstraint * (^)(id))equalTo {
    // 返回一個返回值型別為id,引數型別是id的block
    return ^id(id attribute) {
        // 呼叫下面的方法新增約束關係
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}
複製程式碼

在上面的程式碼中我們看到make.top返回的是MASConstraint的子類MASViewConstraintMASCompositeConstraint類,所以在equalTo這個方法中呼叫的其實是MASViewConstraint類或MASCompositeConstraint類的物件方法equalToWithRelation

先看MASViewConstraint類對這個方法的實現:

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    // 返回一個返回值為id型別,引數為id和NSLayoutRelation型別的block
    return ^id(id attribute, NSLayoutRelation relation) {
        if ([attribute isKindOfClass:NSArray.class]) {
            // 如果傳入的屬性是一個陣列
            // 不能重複設定約束關係
            NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
            // 建立變數儲存檢視約束封裝物件
            NSMutableArray *children = NSMutableArray.new;
            // 遍歷傳入的屬性
            for (id attr in attribute) {
                // 建立檢視約束封裝物件,設定約束屬性和約束關係
                MASViewConstraint *viewConstraint = [self copy];
                viewConstraint.layoutRelation = relation;
                viewConstraint.secondViewAttribute = attr;
                // 儲存檢視約束封裝物件
                [children addObject:viewConstraint];
            }
            // 利用上面建立的檢視約束封裝物件陣列建立多檢視約束封裝物件
            MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
            compositeConstraint.delegate = self.delegate;
            // 替換掉當前檢視約束物件
            [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
            // 返回多檢視約束封裝物件
            return compositeConstraint;
        } else {    
            // 如果傳入的屬性不是陣列型別的
            // 不能重複設定約束關係
            // 如果已經設定了約束關係,必須和原約束關係相同,並且屬性必須是NSValue型別的
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            // 儲存約束關係和約束屬性
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            // 返回當前類物件
            return self;
        }
    };
}
複製程式碼

接著看一下MASViewConstraint類中兩個setter的實現:

- (void)setLayoutRelation:(NSLayoutRelation)layoutRelation {
    // 除了儲存約束關係,還儲存了是否設定了約束關係
    _layoutRelation = layoutRelation;
    self.hasLayoutRelation = YES;
}
複製程式碼

這個方法沒啥好說的,就是儲存了一下。

- (void)setSecondViewAttribute:(id)secondViewAttribute {
    if ([secondViewAttribute isKindOfClass:NSValue.class]) {
        // 如果引數是NSValue型別的,根據值的型別設定不同的屬性
        [self setLayoutConstantWithValue:secondViewAttribute];
    } else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
        // 如果引數是UIView型別的,就生成 view2 的檢視屬性封裝物件,其中 attr2 和 view1 的 view1 相同,並儲存
        _secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
    } else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
        // 如果引數是MASViewAttribute型別的,就直接儲存
        _secondViewAttribute = secondViewAttribute;
    } else {
        // 只允許輸入 NSValue 、 UIView 和 MASViewAttribute 這三種型別的資料
        NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
    }
}
複製程式碼

這個方法中的三個條件其實分別對應下面的三種輸入情況:

  1. make.width.equalTo(@200);
  2. make.centerX.equalTo(self.view);
  3. make.left.equalTo(self.view.mas_left);

在這一步中,實際做的工作就是儲存佈局關係和約束的參考檢視,也就是 relationview2 以及 attr2。但有一點需要注意的是Masonry通過將方法的返回值設定成一個返回值是當前類型別的block,來實現鏈式程式設計的效果。

3.3 常數

  • 首先看父類MASConstraint的實現:
- (MASConstraint * (^)(CGFloat))offset {
    return ^id(CGFloat offset){
        self.offset = offset;
        return self;
    };
}
複製程式碼

是不是熟悉的配方?是不是熟悉的味道?和equalTo一樣,都是通過返回一個返回值是id型別的block來實現鏈式程式設計的效果。其中的實現也很簡單,就是儲存了一下傳入的引數。

  • 再看其子類MASViewConstraint中的實現:
- (void)setOffset:(CGFloat)offset {
    self.layoutConstant = offset;
}
複製程式碼
- (void)setLayoutConstant:(CGFloat)layoutConstant {
    _layoutConstant = layoutConstant;

#if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
    if (self.useAnimator) {
        [self.layoutConstraint.animator setConstant:layoutConstant];
    } else {
        self.layoutConstraint.constant = layoutConstant;
    }
#else
    self.layoutConstraint.constant = layoutConstant;
#endif
}
複製程式碼

在這個子類中就是儲存了一下傳入的常數。

  • 還有子類MASCompositeConstraint中的實現:
- (void)setOffset:(CGFloat)offset {
    // 遍歷所有檢視屬性封裝物件,設定引數
    for (MASConstraint *constraint in self.childConstraints) {
        constraint.offset = offset;
    }
}
複製程式碼

這一步就是設定常數,也就是 c


到此為止,約束所需的引數都設定完成,下面就是設定約束了。

4.設定約束

在第 2 節中 mas_makeConstraints: 方法的最後一句就是設定約束:

return [constraintMaker install];
複製程式碼

我們點進 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;
}
複製程式碼

邏輯很明白,就是先移除之前已經新增的約束,在設定要設定的約束。

然後看一下解除約束的方法實現:

- (void)uninstall {
    // 這個為了相容 iOS8 之前的版本,因為屬性 active 是iOS8 才開始生效的
    if ([self supportsActiveProperty]) {
        // 將約束的活動狀態設定為NO
        self.layoutConstraint.active = NO;
        // 從儲存集合中移除
        [self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
        // 返回
        return;
    }
    
    // 如果是 iOS8 之前的版本
    // 移除掉檢視的約束
    [self.installedView removeConstraint:self.layoutConstraint];
    // 屬性置空
    self.layoutConstraint = nil;
    self.installedView = nil;
    
    // 從儲存集合中移除
    [self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
}
複製程式碼

接著看設定約束的方法實現:

- (void)install {
    // 先判斷當前約束是否已被設定,如果設定了就不需要繼續向下執行了
    if (self.hasBeenInstalled) {
        return;
    }
    
    // 同樣是為了相容 iOS8 
    if ([self supportsActiveProperty] && self.layoutConstraint) {
        // 將約束的活動狀態設定為YES
        self.layoutConstraint.active = YES;
        // 將約束新增到集合中儲存
        [self.firstViewAttribute.view.mas_installedConstraints addObject:self];
        return;
    }
    
    // iOS7 之前版本的設定方式
    // 獲取設定的各個引數
    MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
    NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
    MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
    NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;

    // 如果設定了像 make.left.equalTo(@10) 這樣的約束
    if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
        // view2 就是 view1 的父檢視
        secondLayoutItem = self.firstViewAttribute.view.superview;
        // attr2 就是 attr1
        secondLayoutAttribute = firstLayoutAttribute;
    }
    
    // 建立約束物件
    MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];
    
    // 設定優先順序和key
    layoutConstraint.priority = self.layoutPriority;
    layoutConstraint.mas_key = self.mas_key;
    
    if (self.secondViewAttribute.view) {
        // 如果設定了 view2
        // 獲取 view1 和 view2 的公共父檢視
        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) {
        // 如果設定的屬性為 size 型別的, 要設定約束的檢視就是 view1
        self.installedView = self.firstViewAttribute.view;
    } else {
        // 否則,要設定約束的檢視就是 view1 的父檢視
        self.installedView = self.firstViewAttribute.view.superview;
    }

    // 建立變數儲存之前新增的約束
    MASLayoutConstraint *existingConstraint = nil;
    // 如果需要更新約束
    if (self.updateExisting) {
        // 獲取之前的約束
        existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
    }

    if (existingConstraint) {
        // 如果有之前的約束
        // 更新約束
        existingConstraint.constant = layoutConstraint.constant;
        // 儲存當前約束
        self.layoutConstraint = existingConstraint;
    } else {
        // 如果沒有之前的約束
        // 向檢視設定約束
        [self.installedView addConstraint:layoutConstraint];
        // 儲存當前約束
        self.layoutConstraint = layoutConstraint;
        // 將約束新增到集合中儲存
        [firstLayoutItem.mas_installedConstraints addObject:self];
    }
}
複製程式碼

這個方法裡面還有個比較兩個約束是否相似的方法:

- (MASLayoutConstraint *)layoutConstraintSimilarTo:(MASLayoutConstraint *)layoutConstraint {
    // 遍歷要設定約束檢視的所有已設定的約束
    for (NSLayoutConstraint *existingConstraint in self.installedView.constraints.reverseObjectEnumerator) {
        // 如果已設定的約束不是 MASLayoutConstraint 型別的,跳過
        if (![existingConstraint isKindOfClass:MASLayoutConstraint.class]) continue;
        // 如果已設定的約束的 view1 和要設定約束的 view1 不相同,跳過
        if (existingConstraint.firstItem != layoutConstraint.firstItem) continue;
        // 如果已設定的約束的 view2 和要設定約束的 view2 不相同,跳過
        if (existingConstraint.secondItem != layoutConstraint.secondItem) continue;
        // 如果已設定的約束的 attr1 和要設定約束的 attr1 不相同,跳過
        if (existingConstraint.firstAttribute != layoutConstraint.firstAttribute) continue;
        // 如果已設定的約束的 attr2 和要設定約束的 attr2 不相同,跳過
        if (existingConstraint.secondAttribute != layoutConstraint.secondAttribute) continue;
        // 如果已設定的約束的 relation 和要設定約束的 relation 不相同,跳過
        if (existingConstraint.relation != layoutConstraint.relation) continue;
        // 如果已設定的約束的 multiplier 和要設定約束的 multiplier 不相同,跳過
        if (existingConstraint.multiplier != layoutConstraint.multiplier) continue;
        // 如果已設定的約束的優先順序和要設定約束的優先順序不相同,跳過
        if (existingConstraint.priority != layoutConstraint.priority) continue;
        
        // 返回倖存者,也就是除了常數 c 其他引數都要相同
        return (id)existingConstraint;
    }
    // 如果沒有符合條件的就返回空物件
    return nil;
}
複製程式碼

5.總結

  • 首先我們會呼叫 View+MASAdditions.h 分類中的 mas_makeConstraints: 方法用來建立設定約束的環境,以及獲取約束工廠類 MASConstraintMaker 的物件 make
  • 接著,我們呼叫 make 的屬性設定約束的屬性 attr1。例如 make.top。這個時候在 make 物件內部:
    • 首先會建立一個檢視屬性封裝類 MASViewAttribute 物件,裡面封裝著 view1attr1
    • 再用上一步建立的物件建立檢視約束封裝類 MASViewConstraint 物件並返回,這個物件就負責管理著 NSLayoutConstraint 類物件。
  • 然後,我們呼叫上一步返回的物件方法來設定約束的關係、view2attr2。例如 make.top.equalTo(redView.mas_bottom)。這時在MASViewConstraint 物件內部:
    • 首先會建立一個檢視屬性封裝類 MASViewAttribute 物件,裡面封裝著 view1attr1
    • 然後將上一步建立的物件儲存到當前物件的屬性中,並返回當前物件。
  • 接著,再設定約束的常數。例如,make.top.equalTo(redView.mas_bottom).offset(30.0);。在這一步中,MASViewConstraint 物件只是儲存了一下傳入的引數。
  • 設定完約束後,在 mas_makeConstraints: 方法中,就會呼叫 make 物件的 install 方法。make 物件會呼叫剛才建立的 MASViewConstraint 物件的install 方法。
  • 最後,在 MASViewConstraint 物件的install 方法中,通過剛才設定的約束的引數建立 NSLayoutConstraint 物件,並新增到檢視上。

相關文章