Mansonry原始碼解析

yehot發表於2019-05-08

NSLayoutConstraint、Mansonry 對比

NSLayoutConstraint 新增 AutoLayout 約束

首先,給一個 view 新增 AutoLayout 的原生方法:

  • 使用 NSLayoutAttribute 建立一個 NSLayoutConstraint 物件;
  • 然後 [view addConstraint:]
UIView *superView = self.view;
UIView *view1 = [UIView new];
[superView addSubView:view1];

// 禁用自動約束
view1.translatesAutoresizingMaskIntoConstraints = NO;

// 建立一個 view1 相對於 superView 的 left 約束
NSLayoutConstraint *leftConstraint
= [NSLayoutConstraint constraintWithItem:view1
                               attribute:NSLayoutAttributeLeft
                               relatedBy:NSLayoutRelationEqual
                                  toItem:superView
                               attribute:NSLayoutAttributeLeft
                              multiplier:1.0
                                constant:10];
// 省略 30 行
NSLayoutConstraint *rightConstraint;
NSLayoutConstraint *topConstraint;
NSLayoutConstraint *bottomConstraint;
    
[view1 addConstraint:leftConstraint];
[view1 addConstraint:rightConstraint];
// 省略...
複製程式碼

使用 Masonry 新增 AutoLayout 約束

上文新增的約束,使用 Masonry 如下,簡化了大概 35 行程式碼:

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
     make.edges.equalTo(superView).offset(10);
}];
複製程式碼

Masonry 新增約束流程解析

mas_makeConstraints 內部邏輯展開如下:

    [view1 mas_makeConstraints:^(...) {
        view1.translatesAutoresizingMaskIntoConstraints = NO;
        MASConstraintMaker *maker = [[MASConstraintMaker alloc] initWithView:view1];
 
        // 呼叫 block():
        maker.left.equalTo(0);
        maker.width.and.top.equalTo().offset(2);
 
        [maker install];
    }]
複製程式碼

可以從上述例子中看到,Masonry 新增約束呼叫的 [view1 mas_makeConstraints:] 裡,主要做了以下事情:

  1. 禁用 translatesAutoresizingMaskIntoConstraints 自動約束;
  2. 初始化一個 ConstraintMaker (約束工廠類),並給 maker 新增 上下左右/寬高 的約束(注意這一步只是將約束記錄到 maker 的 constraint array 中);
  3. 在 maker install 時,將 constraint array 中的約束逐個 add 到 view 上;

1:生成約束(並 record)

1.1 make.left

    // 語法分析:
    MASConstraintMaker *make = [MASConstraintMaker new];
    // make.left 只是 get 方法( MASConstraintMaker 的 MASConstraint* left 屬性)
    MASConstraint *mas = make.left;
複製程式碼
  • 可以看到 make.left 是呼叫 .left 屬性;
  • 但由於重寫了 left 屬性的 get 方法,所以,make.left 會呼叫 [make addConstraint:] 新增一條 NSLayoutAttributeLeft 型別的約束;
  • left 屬性的 get 方法,最終 return 了一個 MASConstraint 物件,所以後續才能鏈式呼叫;
// MASConstraintMaker.m

- (MASConstraint *)left {
    return [self constraint:nil addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    
    // 用 view 和 NSLayoutAttribute 構建 MASViewAttribute
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    // 用 MASViewAttribute 構建 MASViewConstraint
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    
    // ... 省略
    
    if (!constraint) {
        // 作用??
        newConstraint.delegate = self;
        // 將 MASViewConstraint 新增到 make.constraints(Array) 中,等 install 時再將約束 add 到 view 上
        [self.constraints addObject:newConstraint];
    }
    // return 的 MASViewConstraint 可以繼續 鏈式呼叫
    return newConstraint;
}
複製程式碼

1.2 make.left.and

MASConstraint *mas = make.left; 中可以看到 make.left 返回的是一個 MASConstraint * 型別。

make.left.and.with and 和 with 作為 get 方法被呼叫,裡邊只是簡單的 return 了 MASConstraint self,所以可以繼續鏈式呼叫

1.3 make.left.and.width

MASConstraint *mas = make.left; 中可以看到 make.left 返回的是一個 MASConstraint * 型別。因此,鏈式呼叫到 .width 時,呼叫的不再是 maker 的方法,而是 MASConstraint 的方法:

// MASConstraint.m

- (MASConstraint *)width {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeWidth];
}
複製程式碼

MASConstraint 是一個 Abstract 抽象類:只提供介面,內部是空實現,需要子類處理。因此 width 裡呼叫的 addConstraintWithLayoutAttribute: 會呼叫到子類 MASViewConstraint 中:

// MASViewConstraint.m

- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}
複製程式碼

MASViewConstraint 中並沒有生成或新增約束,只是呼叫 delegate 去處理。這個 delegate 剛好就是前邊的 maker 物件,所以又回到了上一步 make.left 一樣的邏輯(注意上一步中省略掉沒貼的程式碼):

// MASConstraintMaker.m

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    
    // 同 make.left ,省略...
    // MASViewAttribute *viewAttribute = ;
    // MASViewConstraint *newConstraint = ;

    // make.left 時,此方法傳入的 constraint 是 nil,因此分析 make.left 時,這塊程式碼省略了
    // make.left.width 到 .width 時,這裡傳入的 constraint 就是 make.left 生成的 newConstraint 物件
    if ([constraint isKindOfClass:MASViewConstraint.class]) {
        // 注: 此處的 constraint 即 left constraint;newConstraint 即 width constraint
        NSArray *children = @[constraint, newConstraint];
        // MASCompositeConstraint 是 MASConstraint 的子類,constraint group
        MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
        compositeConstraint.delegate = self;
        
        // 替換約束:
        // 替換已經記錄到 maker.constraintArray 中的 left constraint 約束為 constraint group
        [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
        return compositeConstraint;
    }
    // ... 省略 
    // if 條件成立,已經 return,後續不會再重複 add constraint 到 array
}

// array 替換元素
- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint {
    NSUInteger index = [self.constraints indexOfObject:constraint];
    [self.constraints replaceObjectAtIndex:index withObject:replacementConstraint];
}
複製程式碼

1.4 make.left.and.width.equalTo()

用於 make.left 作為 get 方法返回的是 MASViewConstraint 物件,可以繼續鏈式呼叫:make.left.equalTo(superView),即呼叫 MASViewConstraint.equalTo();

equalTo 入參是 id 型別的 attribute,attribute 型別可以是 MASViewAttribute、UIView、NSValue,equalTo 方法內部對型別做了 if 判斷;

equalTo 是個巨集定義,展開後的呼叫鏈是: qualTo(x) -> mas_equalTo(x) -> MASViewConstraint.equalToWithRelation(x, NSLayoutRelationEqual)

注意:

  • 這裡呼叫過程中將 NSLayoutRelation 引數也傳入到了 MASViewConstraint.equalToWithRelation();
  • equalToWithRelation 中儲存了 equalTo 傳入的 id 型別 attributeNSLayoutRelation 到 MASViewConstraint 物件中,並返回 MASViewConstraint 物件,以繼續鏈式呼叫;
// MASViewConstraint.m

- (MASConstraint * (^)(id))mas_equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        // ... 簡化
        
        // 儲存了 equalTo 傳入的 attribute 和 NSLayoutRelation 型別
        self.layoutRelation = relation;
        self.secondViewAttribute = attribute;
        // 繼續返回 MASConstraint,鏈式呼叫
        return self;
    };
}
複製程式碼

1.5 make.left.equalTo().offset()

繼續鏈式呼叫 offset:make.left.equalTo(superView).offset(2);

注:

  • offsetequalTo 不同,不是像 equalTo有入參的,所以可以加括號 equalTo(xxx);
  • offset方法是一個無入參方法,但是方法的返回值是一個有入參有返回值的 block 型別,因此可以 .offset(3) 呼叫;
  • 並且,由於 block 的返回值仍是 MASConstraint 物件,所以可以繼續鏈式呼叫;

簡化邏輯如下:

    // make.left 是 get 方法
    MASConstraint* mas = make.left;
    MASConstraint*(^block)(CGFloat f) = mas.offset;
    // 由於 .offset 的 返回值是 block,所以,可以直接呼叫:
    mas.offset(2);
    // mas.offset(2) 等同於:
    block(2);
    

    // block 的返回值
    MASConstraint *mas2 = mas.offset(2);
複製程式碼

1.6 make.xxx

  • 其它 make.center make.insets.and.size 都是一樣的用法;
  • make.left.priority(MASLayoutPriorityDefaultLow) 等於 make.left.priorityLow

2:新增約束

2.1 [make install]

對 MASConstraintMaker *make 新增約束後,make install 最後執行約束:

// MASConstraintMaker.m

- (NSArray *)install {
    
    // mas_remakeConstraints 的邏輯:先對已加的約束全部移除,再重新新增
    if (self.removeExisting) {
        NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
        for (MASConstraint *constraint in installedConstraints) {
            [constraint uninstall];
        }
    }
    
    NSArray *constraints = self.constraints.copy;
    // 遍歷 make 裡新增的每個 MASConstraint 並 install
    for (MASConstraint *constraint in constraints) {
        // mas_updateConstraints 的邏輯: 標記是否需要更新,稍後在  MASViewConstraint install 裡更新已加的約束
        constraint.updateExisting = self.updateExisting;
        
        // 最終 install 約束的還是在 MASViewConstraint 裡
        [constraint install];
    }
    [self.constraints removeAllObjects];
    return constraints;
}
複製程式碼

2.2 [constraint install]

// MASViewConstraint.m

- (void)install {
    // ... 省略:避免重複新增的邏輯
    
    // 取出 MASViewConstraint 記錄的 兩個 MASViewAttribute 物件
    MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
    NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
    MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
    NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;

    // ... 省略
    
    // 1、構建一個 NSLayoutAttribute
    MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];

    // ...
    
    //    建立完約束物件後,尋找約束該新增到那個View上:
    if (self.secondViewAttribute.view) { //    如果是兩個檢視相對約束,就獲取兩種的公共父檢視。
        MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
        self.installedView = closestCommonSuperview;
    } else if (self.firstViewAttribute.isSizeAttribute) { //    如果新增的是Width或者Height,那麼就新增到當前檢視上
        self.installedView = self.firstViewAttribute.view;
    } else { //    如果既沒有指定相對檢視,也不是Size型別的約束,那麼就將該約束物件新增到當前檢視的父檢視上
        self.installedView = self.firstViewAttribute.view.superview;
    }

    // 是否已經新增了約束
    MASLayoutConstraint *existingConstraint = nil;
    
    // mas_updateConstraints 的邏輯
    if (self.updateExisting) {
        existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
    }
    if (existingConstraint) { // 存在,直接重新賦值
        existingConstraint.constant = layoutConstraint.constant;
        // ...
    } else {
        // 不存在,新增
        // 2、[view addConstraint:NSLayoutAttribute]; !!!
        [self.installedView addConstraint:layoutConstraint];
        // ...
    }
}
複製程式碼

程式碼技巧

block 靈活使用

block 寫法

    @property (nonatomic, strong) UIView *(^myBlock)(NSLayoutAttribute attr);
    
    self.myBlock = ^UIView *(NSLayoutAttribute attr) {
        return greenView;
    };

        // blcok 在右側時,^ 在最前
    dispatch_block_t t = ^void(void) {
    };
    
    UIView *(^block)(NSLayoutAttribute attr) = self.myBlock;
    
    self.myBlock = block;   
複製程式碼

更多 block 語法,見:How Do I Declare A Block in Objective-C?

block 作為入參簡化外部呼叫

在方法需要傳入一個 ConfigModel 時,使用 block 作為入參:讓外部無需關注 ConfigModel 初始化方式,只需要關注配置項

// 使用:
[view mas_makeConstraints:^(MASConstraintMaker *make) {
     make.edges.equalTo(lastView).offset(2);
}];

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {

    // new 一個 maker,然後呼叫 block 傳出去
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    // 外部對 maker 配置後,maker install
    block(constraintMaker);
    
    return [constraintMaker install];
}
複製程式碼

應用案例: SDK 初始化

[SHWAccountSDK setupConfig:^(SHWAccountConfig *config) {
    config.appKey = @"1";
    config.secret = @"2";
}];
複製程式碼

block 入參作為 result 回撥

[SHWAccountSDK getSMSCode:phoneNum success:^(BOOL isSuccess) {
    // getSMSCode 內部 new 了一個 Request 物件;
} failure:^(NSString *errCode, NSString *errMsg) {

}];

MyRequest *request;
[request startWithSuccess:^(BOOL isSuccess) {
    NSLog(@"getSMSCodeAsyncWithPhoneNum success");
} failure:^(NSString *errCode, NSString *errMsg) {
    NSLog(@"getSMSCodeAsyncWithPhoneNum fail");
}];
複製程式碼

block 作為返回值,鏈式呼叫

通常函式的入參是 block 用的比較多,但函式的返回值是 block 時,可以寫出鏈式呼叫的優雅寫法:

- (MASConstraint * (^)(CGFloat))offset {
    return ^id(CGFloat offset){
        self.offset = offset;
        return self;
    };
}

// 使用:
[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(superview.mas_bottom).offset(padding);
}];
複製程式碼

原理:

    MASConstraint *mas = [MASConstraint new];
    MASConstraint*(^block)(CGFloat f) = mas.offset;
    
    // 以下相等
    block(2);
    mas.offset(2);
    // 由於 mas.offset 的返回值是一個 (入參是 CGFloat 的)block,在後邊直接追加 (2),相當於呼叫 block
    // 並且,由於 block 的返回值仍是 mas 型別,所以後邊可以繼續鏈式呼叫     
複製程式碼

對比:

// 返回值不是 block,只是 self class 時
- (MASConstraint *)setOffset:(CGFloat)offset {
    self.offset = offset;
    return self;
}
// 使用:
MASConstraint *mas = [MASConstraint new];
// 只能這樣呼叫
[[mas setOffset:3] setOffset:1];
複製程式碼

MASBoxValue: mas_equalTo、equalTo

mas_equalTo() 可以傳入多種型別入參:·

    [view1 makeConstraints:^(MASConstraintMaker *make) {
        make.left.mas_equalTo(view2);
        make.left.mas_equalTo(3);
        make.left.mas_equalTo(@(2));

        make.left.mas_equalTo(view2.mas_left);
        make.size.mas_equalTo(CGSizeMake(10, 10));

        make.edges.equalTo(UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f));
        make.height.equalTo(@[redView, blueView])
    }];
複製程式碼

其定義如下:


#define MASBoxValue(value)      _MASBoxValue(@encode(__typeof__((value))), (value))
#define mas_equalTo(...)        equalTo(MASBoxValue((__VA_ARGS__)))

- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return ...;
    };
}

// 儘管傳入的是 id 型別,但是解析也還是要支援變參和,並將 float,double,int 這樣的值型別資料轉換成和 equalTo 一樣的物件 NSNumber 資料:
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(int)) == 0) {
        int actual = (int)va_arg(v, int);
        obj = [NSNumber numberWithInt:actual];
    } // ... 省略
    
    va_end(v);
    return obj;
}
複製程式碼

這也順便解釋了 mas_equalToequalTo 的區別:沒有區別,mas_equalTo 呼叫的還是 equalTo,只是呼叫前對入參進行了 boxValue 轉換型別

相關閱讀

相關文章