iOS框架·Masonry原始碼深度解析及學習啟示:設計模式與鏈式程式設計思想

陳滿iOS發表於2019-03-04

傳送門:鏈式程式設計小Demo

這篇文章是 Masonry 框架原始碼的解析和筆記。學習Masonry之前,先了解這個框架設計的初衷—傳統的利用系統API進行純程式碼佈局的不足。然後,根據Masonry常見的幾個鏈式語法中,順藤摸瓜地瞭解Masonry的呼叫棧。最後,學習並思考這個框架用到的設計模式鏈式程式設計思想。

1. 之前的不足:系統API純程式碼佈局

  • 系統給的自動佈局(AutoLayout)的API
+(instancetype)constraintWithItem:(id)view1
                       attribute:(NSLayoutAttribute)attr1
                       relatedBy:(NSLayoutRelation)relation
                          toItem:(nullable id)view2
                       attribute:(NSLayoutAttribute)attr2
                      multiplier:(CGFloat)multiplier
                        constant:(CGFloat)c;
複製程式碼
  • 傳統程式碼中使用系統API進行佈局
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.view.backgroundColor = [UIColor yellowColor];
    
    
    UIView *subView = [[UIView alloc] init];
    subView.backgroundColor = [UIColor redColor];
    // 在設定約束前,先將子檢視新增進來
    [self.view addSubview:subView];
    
    // 使用autoLayout約束,禁止將AutoresizingMask轉換為約束
    [subView setTranslatesAutoresizingMaskIntoConstraints:NO];
    
    // 設定subView相對於VIEW的上左下右各40畫素
    NSLayoutConstraint *constraintTop = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:40];
    NSLayoutConstraint *constraintLeft = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:40];
    // 由於iOS座標系的原點在左上角,所以設定下,右邊距使用負值
    NSLayoutConstraint *constraintBottom = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-40];
    NSLayoutConstraint *constraintRight = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeRight multiplier:1.0 constant:-40];
    
    // 將四條約束加進陣列中
    NSArray *array = [NSArray arrayWithObjects:constraintTop, constraintLeft, constraintBottom, constraintRight, nil];
    // 把約束條件設定到父檢視的Contraints中
    [self.view addConstraints:array];
}
複製程式碼

可見,系統傳統的程式碼佈局有點繁瑣。為了簡化上述傳統佈局程式碼,被廣泛應用的第三方框架 MasonryAutoLayout 進行了封裝,Swift版則是 SnapKit。這篇文章就是針對 Masonry 原始碼的解析與學習筆記。在這之前,如下圖所示,是 Masonry 原始碼的結構圖:

Masonry 原始碼結構圖

2. 順藤摸瓜:Masonry鏈式語法的呼叫棧解析

2.1 mas_makeConstraints:外部呼叫

  • 呼叫例子
#import "Masonry.h"
複製程式碼
[self.containerView addSubview:self.bannerView];
[self.bannerView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.leading.equalTo(self.containerView.mas_leading);
    make.top.equalTo(self.containerView.mas_top);
    make.trailing.equalTo(self.containerView.mas_trailing);
    make.height.equalTo(@(kViewWidth(131.0)));
}];
複製程式碼

2.2 mas_makeConstraints:實現原理,通過匯入的標頭檔案分析

  • Masonry.h
#import <Foundation/Foundation.h>

//! Project version number for Masonry.
FOUNDATION_EXPORT double MasonryVersionNumber;

//! Project version string for Masonry.
FOUNDATION_EXPORT const unsigned char MasonryVersionString[];

#import "MASUtilities.h"
#import "View+MASAdditions.h"
#import "View+MASShorthandAdditions.h"
#import "ViewController+MASAdditions.h"
#import "NSArray+MASAdditions.h"
#import "NSArray+MASShorthandAdditions.h"
#import "MASConstraint.h"
#import "MASCompositeConstraint.h"
#import "MASViewAttribute.h"
#import "MASViewConstraint.h"
#import "MASConstraintMaker.h"
#import "MASLayoutConstraint.h"
#import "NSLayoutConstraint+MASDebugAdditions.h"
複製程式碼

其中View+MASAdditions分類為UIView新增了mas_makeConstraints方法

  • View+MASAdditions.m
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}
複製程式碼
  • MASConstraintMaker.m
@interface MASConstraintMaker () <MASConstraintDelegate>

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

@end
複製程式碼
- (id)initWithView:(MAS_VIEW *)view {
    self = [super init];
    if (!self) return nil;
    
    self.view = view;
    self.constraints = NSMutableArray.new;
    
    return self;
}
複製程式碼

2.3 .top:通過MASConstraintMaker類原始碼分析

先分析設定 第一個約束屬性 的情況(且唯一一個):例如

make.top.equalTo(self.containerView.mas_top);
複製程式碼
2.3.1 MASConstraintMaker的分析
  • MASConstraintMaker.m
- (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];
    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;
}
複製程式碼

該方法返回的newConstraint是一個MASViewConstraint類的示例,而MASViewConstraint類又是MASConstraint的子類,返回型別寫成MASConstraint沒毛病。

程式碼較多,暫時可以只先看if (!constraint)裡面的程式碼。可見,最後設定 newConstraint物件代理為self (即 MASConstraintMaker),並新增到一開始準備好的 self.constraints 陣列中,返回。

其中,設定 MASViewConstraintnewConstraint 物件的 MASConstraintDelegate 代理為self (即 MASConstraintMaker),其作用就是為了能夠同時設定多個約束屬性!即鏈式語法。

  • MASConstraint+Private.h
@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
複製程式碼
2.3.2 MASConstraintMaker的繼續分析

第2.3.1節的MASConstraintMaker.m程式碼中,先是初始化了 MASViewAttribute 物件並儲存了 view、item以及 NSLayoutAttribute 三個屬性。

  • MASViewAttribute.m
- (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute {
    self = [self initWithView:view item:view layoutAttribute:layoutAttribute];
    return self;
}

- (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute {
    self = [super init];
    if (!self) return nil;
    
    _view = view;
    _item = item;
    _layoutAttribute = layoutAttribute;
    
    return self;
}
複製程式碼

然後又初始化了 MASViewConstraint 物件,內部配置了些預設引數並儲存瞭如上的第一個約束引數 MASViewAttribute

  • MASViewConstraint.m
- (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute {
    self = [super init];
    if (!self) return nil;
    
    _firstViewAttribute = firstViewAttribute;
    self.layoutPriority = MASLayoutPriorityRequired;
    self.layoutMultiplier = 1;
    
    return self;
}
複製程式碼

2.4 .equalTo :通過基類MASConstraint及其子類MASViewConstraint分析

第一個約束屬性 設定完後,走到.equalTo時,前面返回已經是一個 MASViewConstraint(繼承自MASConstraint) 物件了,因而呼叫的是在基類MASConstraint中宣告並實現的block屬性getter方法。

  • MASConstraint.m
- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}
複製程式碼

其中,基類 MASConstraint 僅僅宣告,並沒有實現equalToWithRelation抽象方法。但是,如2.3節中的鏈式語法.top,該方法返回的newConstraint實際是其子類–MASViewConstraint類的例項,故而可呼叫子類MASViewConstraint實現的equalToWithRelation方法:

  • MASViewConstraint.m
- (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;
        }
    };
}
複製程式碼

程式碼較多,暫時可先看else {裡面的程式碼。

(1) self.layoutRelation = relation;

首先是 self.layoutRelation 儲存了約束關係且重寫了 set 方法,在裡面用 self.hasLayoutRelation 這個 BOOL 標識已經有約束關係。

  • MASViewConstraint.m
- (void)setLayoutRelation:(NSLayoutRelation)layoutRelation {
    _layoutRelation = layoutRelation;
    self.hasLayoutRelation = YES;
}
複製程式碼
(2) self.secondViewAttribute = attribute;

然後同樣是重寫了 self.secondViewAttributeset 方法,這裡會根據不同的情況做不同的操作。

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

其中,第1種情況對應的是:

make.height.equalTo(@20.0f)
複製程式碼

傳入 NSValue 的時, 會直接設定 constraintoffset, centerOffset, sizeOffset, 或者 insets。呼叫棧如下:

//MASViewConstraint.m
if ([secondViewAttribute isKindOfClass:NSValue.class]) {
    [self setLayoutConstantWithValue:secondViewAttribute];
}
//MASConstraint.m
- (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);
    }
}
//MASViewConstraint.m
- (void)setOffset:(CGFloat)offset {
    self.layoutConstant = offset;
}
//MASViewConstraint.m
- (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
}
複製程式碼

第2種情況,一般是直接傳入一個檢視:

make.top.equalTo(self)
複製程式碼

這時, 就會初始化一個 layoutAttribute 屬性與 firstViewArribute 相同的 MASViewAttribute, 上面的程式碼就會使檢視與 view 頂部對齊。

第3種情況,會傳入一個檢視的 MASViewAttribute:

make.top.equalTo(view.mas_bottom);
複製程式碼

使用這種寫法時, 一般是因為約束的方向不同. 這行程式碼會使檢視的頂部與 view 的底部對齊。

2.5 .height.width:Masonry的鏈式語法特性

  • 呼叫例子
make.height.width.equalTo(@20);
複製程式碼

其中,.height 設定第一個約束屬性時,呼叫的是 MASConstraintMaker.m 中的 .heightaddConstraintWithLayoutAttribute,以及- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute

  • MASConstraintMaker.m
- (MASConstraint *)height {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeHeight];
}

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

該方法呼叫棧返回的是一個MASViewConstraint(父類是 MASConstraint) 物件。

因此,通過 .width 設定第二個約束屬性的時候,呼叫的先是基類 MASConstraint.m 中的.width,然後呼叫由子類MASViewConstraint實現的addConstraintWithLayoutAttribute方法。這時候的呼叫棧為:

  • MASConstraint.m
- (MASConstraint *)width {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeWidth];
}

- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute __unused)layoutAttribute {
    MASMethodNotImplemented();
}
複製程式碼
  • 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];
}
複製程式碼

這其中,self.delegate 是什麼呢?如2.3.1節所述,MASConstraintMaker.m 中設定了 MASViewConstraintnewConstraint 物件的 MASConstraintDelegate 代理為“self” (即 MASConstraintMaker),其作用就是為了能夠同時設定多個約束屬性,即鏈式語法。所以,第二個設定約束屬性跟第一個設定約束屬性最終 呼叫的方法一樣(都是MASConstraintMaker.m中實現的addConstraintWithLayoutAttribute)。

  • MASConstraintMaker.m
- (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;
}
複製程式碼

當設定 第二次約束屬性 並執行完之後,我們還可以發現 constraint 不為 nil,而是一個 MASViewConstraint 物件 ,所以該方法呼叫棧返回的不是 MASViewConstraint 物件,而是 MASCompositeConstraint 這個物件了,下面我們來看看這個類。

2.6 約束的集合: MASCompositeConstraint

MASCompositeConstraint 是約束的集合,它裡面有個私有的陣列用來存放多個 MASViewAttribute 物件。

make.height.width.equalTo(@20)
複製程式碼

當設定 第二個約束屬性,走到 .width 時,最終走的是:

  • MASConstraintMaker.m
- (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判讀裡面,將 .height .wight 兩條約束 MASViewConstraint物件塞到陣列裡,建立 MASCompositeConstraint 物件,並且同樣設定了 delegate,最後還把 self.constraints 裡面事先新增好的約束 MASViewConstraint 物件替換成了 MASCompositeConstraint 物件。

#pragma mark - MASConstraintDelegate

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

另外,我們可以點選 MASCompositeConstraint 初始化方法裡看看,它內部會通過 for 迴圈,把陣列裡面的所有 MASViewConstraint 物件同樣設定了 delegate

- (id)initWithChildren:(NSArray *)children {
    self = [super init];
    if (!self) return nil;

    _childConstraints = [children mutableCopy];
    for (MASConstraint *constraint in _childConstraints) {
        constraint.delegate = self;
    }

    return self;
}
複製程式碼

這麼做的目的同時是為了能夠繼續鏈式呼叫,比如我們再設定第三個約束屬性 .left

make.height.width.left.equalTo(@20);
複製程式碼

這時候的呼叫棧如下:

  • MASConstraint.m
- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
複製程式碼
  • MASCompositeConstraint.m
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    [self constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
    return self;
}

- (MASConstraint *)constraint:(MASConstraint __unused *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    id<MASConstraintDelegate> strongDelegate = self.delegate;
    MASConstraint *newConstraint = [strongDelegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
    newConstraint.delegate = self;
    [self.childConstraints addObject:newConstraint];
    return newConstraint;
}
複製程式碼

可以發現,這裡又是通過 delegate 方式,呼叫 MASConstraintMaker 工廠類中的:

  • MASConstraintMaker.m
- (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;
}
複製程式碼

此時,注意到兩個 if 體都沒有走進去,既不像第一次,也不像第二次約束設定的時候。所以,這次僅僅是初始化了個 MASViewConstraint 物件就直接返回了,然後回到上個方法中新增到 MASCompositeConstraint 的私有陣列 self.childConstraints 中返回備用。

三處的同名方法最終呼叫的還是MASConstraintMaker中的方法

關於三次 約束設定之後的 .equalTo(@20),因為執行完 .left 時,返回的是 MASCompositeConstraint 物件,到這一步的時候會有點變化,呼叫棧如下:

  • MASConstraint.m
- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}
複製程式碼
  • MASCompositeConstraint.m
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attr, NSLayoutRelation relation) {
        for (MASConstraint *constraint in self.childConstraints.copy) {
            constraint.equalToWithRelation(attr, relation);
        }
        return self;
    };
}
複製程式碼

可以發現,這裡會迴圈之前準備好的私有陣列 self.childConstraints,呼叫 MASViewConstraint.m 的 equalToWithRelation 方法,和上面講的一樣了。

2.7 新增約束到檢視

mas_makeConstraints 方法的最後會呼叫 [constraintMaker install] 方法來新增所有儲存在 self.constraints 陣列中的所有約束。

  • MASConstraintMaker.m
 - (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;
}
複製程式碼

(1). 如果需要重新構建約束,也就是 呼叫 mas_remakeConstraints:方法,會先取出檢視的所有約束,然後通過一個 for 迴圈,呼叫 uninstall 來清空所有約束:

(2). 如果不需要重新構建約束,會取出 self.constraints 陣列中準備好的約束,通過 for 迴圈,呼叫 install 來把約束新增到檢視上。

關於 install ,是基類 MASConstraint 的抽象方法,方法體由MASViewConstraintMASCompositeConstraint 實現。而 MASCompositeConstraintinstall方法體中其實也是呼叫的由MASViewConstraint類實現的install

  • MASConstraint.m
- (void)install { MASMethodNotImplemented(); }
複製程式碼
  • MASCompositeConstraint.m
- (void)install {
    for (MASConstraint *constraint in self.childConstraints) {
        constraint.updateExisting = self.updateExisting;
        [constraint install];
    }
}
複製程式碼
  • MASViewConstraint.m

這裡程式碼較多,就不分開解析了,直接分為7步寫到原始碼的註釋中,如下所示:

- (void)install {

    //【1】如果約束以及存在並是 active 會直接返回。
    if (self.hasBeenInstalled) {
        return;
    }
    
    //【2】如果 self.layoutConstraint 響應了 isActive 方法並且不為空,會啟用這條約束並新增到 mas_installedConstraints 陣列中,最後返回。
    if ([self supportsActiveProperty] && self.layoutConstraint) {
        self.layoutConstraint.active = YES;
        [self.firstViewAttribute.view.mas_installedConstraints addObject:self];
        return;
    }
    
    //【3】這邊是獲取即將用於初始化 NSLayoutConstraint 的子類 MASLayoutConstraint 的幾個屬性。
    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)
    //【4】這邊是判斷當前即將新增的約束是否是 size 型別的並且 self.secondViewAttribute 也就是約束的第二個引數是 nil,(eg make.left.equalTo(@10))會自動將約束新增到約束的第一個引數檢視的 superview 上。
    if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
        secondLayoutItem = self.firstViewAttribute.view.superview;
        secondLayoutAttribute = firstLayoutAttribute;
    }
    

    //【5】然後就會初始化 NSLayoutConstraint 的子類 MASLayoutConstraint。
    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;
    

    //【6】這段程式碼會先判斷是否有約束第二個引數的檢視,有的話會尋找約束第一個和第二引數檢視的公共 Superview,相當於求兩個數的最小公倍數;如果不滿足第一個條件,會判斷約束第一個引數是否是 size 型別的,是的話直接取到它的檢視;最後都不滿足會直接取到約束第一個引數檢視父檢視。
    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;
    }

    //【7】如果需要升級當前的約束就會獲取原有的約束,並替換為新的約束,這樣就不需要再次為 view 安裝約束。如果原來的 view 中不存在可以升級的約束,那麼就會在上一步尋找到的 installedView 上面新增約束。
    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];
    }
}
複製程式碼

其中第【6】步中的mas_closestCommonSuperview方法,它會尋找 firstLayoutItem 和 secondLayoutItem 兩個檢視的公共 superview, 相當於求兩個數的最小公倍數.

  • View+MASAdditions.m
- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view {
    MAS_VIEW *closestCommonSuperview = nil;

    MAS_VIEW *secondViewSuperview = view;
    while (!closestCommonSuperview && secondViewSuperview) {
        MAS_VIEW *firstViewSuperview = self;
        while (!closestCommonSuperview && firstViewSuperview) {
            if (secondViewSuperview == firstViewSuperview) {
                closestCommonSuperview = secondViewSuperview;
            }
            firstViewSuperview = firstViewSuperview.superview;
        }
        secondViewSuperview = secondViewSuperview.superview;
    }
    return closestCommonSuperview;
}
複製程式碼

3. 順藤再摸瓜:Masonry其它鏈式語法的呼叫棧解析(選讀)

3.1 make.edges.equalTo(view)

  • 例子
make.edges.equalTo(view)
複製程式碼

我們再來看看這種寫法,呼叫棧如下:

  • MASConstraintMaker.m
- (MASConstraint *)edges {
    return [self addConstraintWithAttributes:MASAttributeTop | MASAttributeLeft | MASAttributeRight | MASAttributeBottom];
}
- (MASConstraint *)addConstraintWithAttributes:(MASAttribute)attrs {
    __unused MASAttribute anyAttribute = (MASAttributeLeft | MASAttributeRight | MASAttributeTop | MASAttributeBottom | MASAttributeLeading
                                          | MASAttributeTrailing | MASAttributeWidth | MASAttributeHeight | MASAttributeCenterX
                                          | MASAttributeCenterY | 
                                          
                    ......
                        
    NSMutableArray *attributes = [NSMutableArray array];
    
    if (attrs & MASAttributeLeft) [attributes addObject:self.view.mas_left];
    if (attrs & MASAttributeRight) [attributes addObject:self.view.mas_right];
    if (attrs & MASAttributeTop) [attributes addObject:self.view.mas_top];
    
    				......    
    
    NSMutableArray *children = [NSMutableArray arrayWithCapacity:attributes.count];
    
    for (MASViewAttribute *a in attributes) {
        [children addObject:[[MASViewConstraint alloc] initWithFirstViewAttribute:a]];
    }
    
    MASCompositeConstraint *constraint = [[MASCompositeConstraint alloc] initWithChildren:children];
    constraint.delegate = self;
    [self.constraints addObject:constraint];
    return constraint;
}
複製程式碼

程式碼太多省略了一部分,可以發現這段程式碼作用就是返回一個包含多條約束的 MASCompositeConstraint 物件,接著後面的操作也都是一樣的了。

3.2 make.edges.equalTo(UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f));

上面3.1中例子的寫法還可以改成這樣:

make.edges.equalTo(UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f));
複製程式碼

這裡的 equalTo 需要注意下,它是一個巨集,定義在 MASConstraint.h 中:

  • MASConstraint.h
#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__)
複製程式碼

代入上述巨集定義,前面的程式碼等效成:

make.edges.equalTo(MASBoxValue(UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f)));
複製程式碼

可以發現,其實裡面呼叫的是 MASBoxValue 這個巨集,它將 C 和 Objective-C 語言中的一些基本資料結構比如說 double CGPoint CGSize 這些值用 NSValue 進行包裝。

這裡還支援直接呼叫 size、center 等,具體實現都差不多,就不熬述了:

make.center.equalTo(CGPointMake(0, 50));
make.size.equalTo(CGSizeMake(200, 100));
複製程式碼

3.3 make.height.equalTo(@[redView, blueView])

make.height.equalTo(@[redView, blueView])
複製程式碼

再來看看這種傳陣列的,在走到 .equalTo 時,最終會呼叫 MASViewConstraint.m 裡面的 equalToWithRelation 方法

  • MASConstraint.m
- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}
複製程式碼
  • MASViewConstraint.m
- (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 {   ....    }
    };
}
複製程式碼

這邊還是遍歷陣列,並且 MASViewConstraint 實現 NSCopying 協議,呼叫 [self copy] 會建立 MASViewConstraint 物件

- (id)copyWithZone:(NSZone __unused *)zone {
    MASViewConstraint *constraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:self.firstViewAttribute];
    constraint.layoutConstant = self.layoutConstant;
    constraint.layoutRelation = self.layoutRelation;
    constraint.layoutPriority = self.layoutPriority;
    constraint.layoutMultiplier = self.layoutMultiplier;
    constraint.delegate = self.delegate;
    return constraint;
}
複製程式碼

然後會根據傳的陣列裡面的 Value 型別來做不同的操作,前面講過就不熬述了:

- (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]) {
        _secondViewAttribute = secondViewAttribute;
    } else {
        NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
    }
}
複製程式碼

最後便是生成 MASCompositeConstraint 物件,並通過 delegate 方式,呼叫 MASConstraintMaker 的方法,替換 self.constraints 陣列裡的約束:

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

4. 舉一反三:框架原始碼的學習啟示

4.1 簡化的設計模式:工廠類&工廠方法

MASConstraintMaker類就是一個工廠類,負責建立MASConstraint型別的物件(依賴於MASConstraint介面,而不依賴於具體實現)。在UIView的View+MASAdditions分類中就是呼叫的MASConstraintMaker類中的一些方法。上述我們在使用Masonry給subView新增約束時,mas_makeConstraints方法中的Block的引數就是MASConstraintMaker的物件。使用者可以通過該Block回撥過來的MASConstraintMaker物件給View指定要新增的約束以及該約束的值。該工廠中的constraints屬性陣列就記錄了該工廠建立的所有MASConstraint物件。

核心類和分類的類圖

MASConstraintMaker 之所以成為約束工廠類,因為MASConstraintMaker賦值建立NSLayoutConstraint物件,因為Masonry將NSLayoutConstraint類進一步封裝成了MASViewConstraint,所以MASConstraintMaker是負責建立MASViewConstraint的物件,並呼叫MASViewConstraint物件的Install方法將該約束新增到相應的檢視中。

說了這麼多,總結一下,如果你呼叫maker.top, maker.left等等這些方法都會呼叫下方的工廠方法來建立相應的MASViewConstraint物件,並記錄在工廠物件的約束陣列中。之所以能鏈式呼叫,就是講當前的工廠物件(MASConstraintMaker)指定為MASViewConstraint物件的代理,所以一個MASViewConstraint物件就可以通過代理來呼叫工廠方法來建立另一個新的MASViewConstraint物件了,此處用到了代理模式。

工廠物件MASConstraintMaker的工廠方法

角色分析

  • Client:UIView,通過分類View+MASAdditions來扮演

  • 工廠類:MASConstraintMaker

  • 抽象產品:MASConstraint

  • 具體產品:MASViewConstraintMASCompositeConstraint

工廠類與兩個子類

4.2 真正的設計模式:組合模式

換一種角度看,Masonry 並非單純的工廠模式,而是採用了經典的 Composite 設計模式,中文可譯作組合模式

典型的 Composite 類圖
典型的 Composite 物件結構
4.2.1 經典 組合模式 中的參與者:
Client
  • 通過 Component 介面操縱組合部件的物件。
Component
  • 為組合中的物件宣告介面。
  • 在適當的情況下,實現所有類共有介面的預設行為
  • 宣告一個介面用於訪問和管理 Component 的子元件。
  • 在遞迴結構中定義一個介面,用於訪問一個父部件,並在合適的情況下實現它。
Leaf
  • 在組合中表示葉節點物件,葉節點沒有子節點。
  • 在組合中定義圖元物件的行為。
Composite
  • 定義有子部件的那些部件的行為。
  • 在 Composite 介面中實現與子部件有關的操作。
4.2.2 從 組合模式 的角度看,Masonry 框架中的角色分析:

UIView,通過分類View+MASAdditions來呼叫Masonry

Client
  • MASConstraintMaker
Component
  • MASConstraint
Leaf
  • MASViewConstraint
Composite
  • MASCompositeConstraint

4.3 程式設計思想:鏈式程式設計

Objective-C是一門動態語言,它使用了一種動態的訊息傳送機制,即物件(object)或類(class)呼叫方法。而OC中的點語法則只能通過setter和getter方法作用於類的屬性,而不能作用於某個方法。想實現鏈式語法,只能通過類似block屬性的getter方法。

鏈式程式設計思想:核心思想為將block作為方法的返回值,且返回值的型別為呼叫者本身,並將該方法以setter的形式返回,這樣就可以實現了連續呼叫,即為鏈式程式設計。

【舉例】簡單使用鏈式程式設計思想實現一個簡單計算器的功能:

4.3.1 新建一個名為CaculateMaker的類,用於運算。
新建一個名為CaculateMaker的類
4.3.2 在CaculateMaker.h檔案中宣告一個方法add:
  • CaculateMaker.h
//  CaculateMaker.h
//  ChainBlockTestApp

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface CaculateMaker : NSObject

@property (nonatomic, assign) CGFloat result;

- (CaculateMaker *(^)(CGFloat num))add;

@end
複製程式碼
4.3.3 在CaculateMaker.m檔案中實現這個方法:
  • CaculateMaker.m
//  CaculateMaker.m
//  ChainBlockTestApp


#import "CaculateMaker.h"

@implementation CaculateMaker

- (CaculateMaker *(^)(CGFloat num))add;{
    return ^CaculateMaker *(CGFloat num){
        _result += num;
        return self;
    };
}

@end
複製程式碼
4.3.4 在viewController裡面匯入CaculateMaker.h檔案,然後呼叫add方法就完成了鏈式語法:
  • ViewController.m
CaculateMaker *maker = [[CaculateMaker alloc] init];
maker.add(20).add(30);
複製程式碼

通過上面Masonry佈局可以看出,它為UIView寫了一個category,擴充了mas_makeConstraints方法,並將MASConstraintMaker物件作為block的引數傳遞,在block的實現裡完成UIView的佈局,提現了函數語言程式設計思想

4.3.5 同樣,我們也可以給NSObject新增一個NSObject+Caculate的分類,完成加法操作:
  • NSObject+Caculate.h
//  NSObject+Caculate.h
//  ChainBlockTestApp

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "CaculateMaker.h"

@interface NSObject (Caculate)

- (CGFloat)caculate:(void (^)(CaculateMaker *make))block;

@end
複製程式碼
  • NSObject+Caculate.m
//  NSObject+Caculate.m
//  ChainBlockTestApp

#import "NSObject+Caculate.h"

@implementation NSObject (Caculate)

- (CGFloat)caculate:(void (^)(CaculateMaker *make))block;{
    CaculateMaker *make = [[CaculateMaker alloc] init];
    block(make);
    return make.result;
}

@end
複製程式碼
4.3.6 最後在viewController裡面呼叫,就很輕鬆的實現了鏈式語法:
  • ViewController.m
CGFloat result = [NSObject caculate:^(CaculateMaker *maker) {
    maker.add(10).add(20).add(30);
}];
NSLog(@"結果為:%.2f",result);
複製程式碼

5. 參考閱讀

  • Masonry解析

    • http://qiufeng.me/masonry
    • https://www.cnblogs.com/ludashi/p/5591572.html
  • 工廠模式

    • https://www.jianshu.com/p/7b89b7f587f9
    • https://www.jianshu.com/p/847af218b1f0
  • 組合模式

    • http://www.runoob.com/design-pattern/composite-pattern.html
    • http://www.cnblogs.com/gaochundong/p/design_pattern_composite.html
    • http://www.cnblogs.com/peida/archive/2008/09/09/1284686.html
  • 鏈式程式設計

    • https://www.jianshu.com/p/cb9252f5105b
    • https://www.jianshu.com/p/ac8bdd3430e7

相關文章