Masonry實現原理並沒有那麼可怕

Mcyboy發表於2018-03-01

Masonry實現原理並沒有那麼可怕

想必在AFNetworking之後,Masonry成了廣大iOS開發者日常開發不可或缺的三方庫之一。它的使用真的非常簡單,例如:

[greenView makeConstraints:^(MASConstraintMaker *make) {
        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);
        
    }];
複製程式碼

從這段短小精悍的程式碼中,我們已經能夠挖掘出它背後的原理。本文假設你對Masonry已經有基本的瞭解和使用。

首先我們提取一些“關鍵詞”,他們究竟是啥意思?

1.makeConstraints:

2.make

3.left、right、height...

4.equalTo()

1.makeConstraints:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}
複製程式碼
  1. MASConstraintMaker被傳入了block()中,即MASConstraintMaker例項就是我們的第二個“關鍵詞”---make。從[constraintMaker install]還可看出,make負責了約束的新增。後文詳細介紹make

  2. translatesAutoresizingMaskIntoConstraints,官方解釋大致為:預設情況下它是YES,即viewautoresizing mask會自動成為它的佈局。如果我們希望手動佈局,需要將它設為NO

2.make(MASConstraintMaker)

從鏈式呼叫make.top.equalTo...,我們可以看出,make中定義了這些約束屬性,它們是這樣實現的:

// step 1
- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}

// step 2
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}

// step 3
- (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;
}
複製程式碼
  1. MASViewAttribute 從實現檔案中得知,MASViewAttribute = View + NSLayoutAttribute + item,這是一個可變元組,儲存了View和與它相關的約束資訊。

  2. MASViewConstraint 這就是一個約束,它包含firstViewAttributesecondViewAttribute

  3. 由step2可知,單純的約束屬性在該方法下的第一個引數都是nil,所以我們先直接看這種情況下的step3的執行情況。它被加入了一個約束陣列中。

if (!constraint) {
        newConstraint.delegate = self;
        [self.constraints addObject:newConstraint];
    }
複製程式碼

4)鏈式呼叫的實現 我們知道Masonry鏈式呼叫是它廣受好評的優點之一。那麼,是如何做到make.top.left這樣的操作呢? 由step3得知,make.top的返回型別是MASViewConstraint,那MASViewConstraint中是如何呼叫到left的呢? 從MASViewConstraint的父類MASConstraint可以看到,這裡也定義了所有的佈局屬性,而這些佈局屬性的實現方式,如下,例:

// MASConstraint.m
- (MASConstraint *)top {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}

// MASViewConstraint.m
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");

    return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}
複製程式碼

MASViewConstraint把這個方法的具體實現委託給了代理方法。而在step3中,這個代理正是MASConstraintMaker。不同的是,此時,step2方法的引數不在是nil了,而是當前約束屬性。這也是的step3的處理邏輯不一樣了。(回到step3中再看看!)此時,MASCompositeConstraint登場了,它是一個約束組合。以make.top.left...為例,這個約束組合包含了topleft,並且呼叫了shouldBeReplacedWithConstraint:方法,如下:

- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint {
    NSUInteger index = [self.constraints indexOfObject:constraint];
    NSAssert(index != NSNotFound, @"Could not find constraint %@", constraint);
    [self.constraints replaceObjectAtIndex:index withObject:replacementConstraint];
}
複製程式碼

make.top.left...為例,這個方法找到了之前儲存top約束的位置,並替換成了約束組合。最終,topleft就一起被加入和make的約束陣列中。

3.install

從上文看,我們已經拿到了所有的約束。這些約束是如何加入到檢視上的?來看看install方法。 摘錄一下MASConstraintMaker中的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;
}
複製程式碼

make的處理相對簡單,再看看MASViewConstraint本身是如何install的。由於方法太長,我不貼上完整的程式碼了(分開來會比較容易看懂)。大致的流程如下: 1.如何約束是設定widthheight,這是檢視自身的屬性,則把當前檢視的父檢視作為關聯檢視(程式碼中為secondLayoutItem)。即這兩個約束是相對於父檢視設定的。

2.如果佈局上存在相對檢視,即通常寫法中的equalTo(someView.mas_top)這樣,則找到這2個檢視最近的公共父檢視,並把約束新增在這個父檢視上。程式碼如下:

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;
    }
複製程式碼

3.最後,如果是更新約束操作,則找出需要更新的約束單獨修改;否則,為檢視新增約束,並記錄在mas_installedConstraints中。

到這裡,約束的新增已經完全梳理明白了。

4.equalTo()

還是得看具體實現方法:

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    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 {
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            return self;
        }
    };
}
複製程式碼

這裡就比較好理解了,如果傳入的是約束陣列,則把他包裝成MASCompositeConstraint,以同樣的方式加入到makeconstraints中。如果是單個約束,則將其設為secondViewAttribute,用於install的時候使用。

總結

1.MASConstraintMaker作為工廠,生產一個個MASViewConstraint約束物件。 2.MASViewConstraintMASCompositeConstraint繼承於抽象類MASConstraint,為我們提供了高度封裝的約束物件 3.View+MASAdditions這個UIView的擴充套件是Masonry與外界互動的介面類,這樣很好的把複雜的約束邏輯封裝在內部管理,又提供了簡單的API供使用者使用。

你看懂了嘛~

相關文章