写一篇源码浅析-Masory

写这篇源码解析的引子呢,是因为最近做了一次“大改版”的需求,有些功能重新开发,也有一些改动小的功能在老代码上修改,因为设计的UI改动比较多,review代码的时候就发现,很多copy来的旧代码,被删删改改换了一个新面貌。其中有不少是页面UI相关的代码,copy过来很容易就缺少自己的思考,发现了很多Masonry的使用不太合理,比如view的首次布局就使用mas_remakeConstraints等等。

其实大家做开发都好多年了,这种错误很容易发现也很容易改正,用的都很6却不是每个人都知道Masonry的工作原理,索性最近居家办公,抽空写一写我自己对Masonry的一点儿读码笔记。也在掘金上看了一些别人写的文章,也学习到了很多,自己写一写再加深一点印象。

为什么代码可以这么写

先看看平时我们是如何使用Masonry的:

[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {

        make.leading.top.mas_equalTo(0);

        make.width.mas_equalTo(110);

    }];
复制代码

上面是一段Masonry的最常用的代码,那么第一个问题是:

为什么self.titleLabel可以直接调用mas_makeConstraints?

这是个很显而易见的问题,Masonry是写了个UIView的分类吧,方便任何UIView以及其子类可以直接使用Masonry中的布局方法。---------View+MASAdditions

下一个问题:

为什么可以make.leading.top.mas_equalTo(0);这样点语法的形式一直调用

点语法,我们可能最先想到的就是调用了getter方法,返回一个对应的对象,比如这样一句简单的代码self.label.text = @"xxx",不难理解,self.label返回懒加载的UILabel对象,而UILabel又有一个text的属性,可以通过点语法访问这个属性,并赋值。

说到这里,是不是就有些明白了,点语法不仅可以调用getter方法,其实定义的任何方法都可以,只是用点语法调用的时候会收获一个warning ----- Property access result unused - getters should not be used for side effects. 但是调用效果和[]调用是一样的。

再回到make.leading.top.mas_equalTo(0);这句代码中来,make是一个MASConstraintMaker对象,MASConstraintMaker拥有leading这个属性,所以make.leading其实就是调用了getter方法,而leadingMASConstraint类型,那么就去看看,MASConstraint是不是得有个top属性啊,肯定有啊 ,没有这代码得报错啊!

扫描二维码关注公众号,回复: 14223741 查看本文章

就这么一步步分析,其实这个点语法,说高级点叫链式调用(method chaining),就得遵循这么个规则。除了上面说的可以通过点语法多次连接调用,还有一个更重要的知识点是这种方式如何传参。

跟到mas_equalTo(0);这个方法的源码中,首先可以看到,mas_equalTo返回的是一个block类型,具体实现:

- (MASConstraint * (^)(id))mas_equalTo {

    return ^id(id attribute) {

        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);

    };
}
复制代码

MASConstraint * (^)(id)是一个带参数有返回值的block,就是说调用完.mas_equalTo其实是返回了一个block类型,block如何执行呢,我们当然知道,直接block()就可以,而这里的block是需要参数的,那我们正好也传了一个数值的参数进去,最后,这个block执行完继续收获一个MASConstraint

说到这里,是不是有点恍然大悟了,每个点语法调用一次就返回一个MASConstraint,想想日常使用的场景,是不是后面还可以继续.mas_offset(10),可以继续链式调用。也就是说,每次链式调用都返回自身,然后就可以继续调用自身的其他方法了。

为什么不直接使用NSLayoutConstraint

NSLayoutConstraint是UIKit库中自带的UI布局大法,却被我们抛弃,除了一些封装的三方或者二方组件中,为了减少依赖,基本很少出现在项目代码中,可能说的有些绝对,在我们的项目中是这样。欣赏一段使用代码:

[superview addConstraints:@[

    //view1 constraints
    [NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeTop
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeTop
                                multiplier:1.0
                                  constant:padding.top],

    [NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeLeft
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeLeft
                                multiplier:1.0
                                  constant:padding.left],

    [NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeBottom
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeBottom
                                multiplier:1.0
                                  constant:-padding.bottom],

    [NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeRight
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeRight
                                multiplier:1
                                  constant:-padding.right],

 ]];

复制代码

这仅仅是设置一个view的约束,就如此繁琐,但好在代码可读性不差,嗯~不爱但也不要伤害。

Masory的出现,帮助开发者大大提高开发效率同时降低代码量,但是,如果你看过源码,你一定知道,被你用的很6的Masory就是基于NSLayoutConstraint的封装。跟到源码中,最后会看到下面的代码:

MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];
[self.installedView addConstraint:layoutConstraint];
复制代码

和上面NSLayoutConstraint的使用如出一辙,而MASLayoutConstraint也是继承自NSLayoutConstraint。了解了这一点,我们再继续去浅析一下Masory的工作原理。

浅析原理

前面一直铺垫,终于写到原理部分了。就是要看看源码,这里好像也没什么技巧,就debug断点看看调用链,然后学习一下作者的编码技巧吧。再回到最开始这句代码:

[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.leading.top.mas_equalTo(0);
        make.width.mas_equalTo(110);
    }];
复制代码

debug执行一步,调入UIView+MASAdditions.m中:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}
复制代码

- mas_makeConstraints:这个方法的参数是一个block,block在方法内部被执行,所以这里的重点是,block中我们要如何把想加在view上的约束,告诉Masory

这里就要研究另外一个重要的类,根据名字就可以知道,这是一个“创建约束”的类MASConstraintMaker。那是不是可以猜测,把约束都交给这个“创建约束”的类,他内部就会自动帮我做很多事,比如组织一系列NSLayoutConstraint需要的参数。因为上面说过了Masory是基于NSLayoutConstraint的封装,那Masory为我们开发者提供的最重要的一件事就是,把我们写的一些简单的top,bottom等,变成NSLayoutConstraint的实例,最终加载到view上。

继续查看MASConstraintMaker的初始化方法:

- (id)initWithView:(MAS_VIEW *)view {
    self = [super init];
    if (!self) return nil;  
    self.view = view;
    self.constraints = NSMutableArray.new;
    return self;
}
复制代码

记录了需要加约束的view,还创建了一个用来存放约束的数组constraints,为啥用数组存呢,也不难理解,因为想要view放在一个指定的位置,肯定是有一组约束来共同实现的。

继续往下执行到block(constraintMaker);,执行这个block中的代码,就是我们真正写约束的代码了:

^(MASConstraintMaker *make) {
        make.leading.top.mas_equalTo(0);
        make.width.mas_equalTo(110);
}
复制代码

上面分析链式调用的时候就知道了,这样一串儿的点语法,其实是调用了一个又一个的方法,比如leading:

- (MASConstraint *)leading {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeading];
}
复制代码

这里就得继续介绍一个抽象类MASConstraint,他有两个子类,分别是MASViewConstraintMASCompositeConstraint,为啥要有两个子类呢,继续往下看源码就知道了。

最后- addConstraintWithLayoutAttribute调入下面的方法中,这里就用到了上面说的两个子类:

- (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;
}
复制代码

又出现一个类MASViewAttribute,先不用太关注,这个类就是标记了一下约束是leading,然后又进一步组装成了MASViewConstraintMASViewConstraint才是具体描述一个约束的对象。当第一次调入这个方法的时候,肯定要走if (!constraint) 这个分支,然后把组装的newConstraint加入存放约束的数组constraints中,并返回newConstraint。这里就可以看出抽象类和派生类存在的意义了,因为是集成关系,可以直接返回MASViewConstraintMASCompositeConstraint

到这里.leading的调用就结束了,总结一下就是组装了一个用来描述约束leading的对象,然后把这个对象存入约束数组中。

然后继续.top的调用,还是重复上面的调用栈,又进入- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute 这个方法里,两次调用有什么不同呢?显而易见,make.leading的时候,是MASConstraint类型,而make.leading调用完返回了MASViewConstraint类型,再进入这个方法呢,就不是直接通过make了,而是通过记录了leading的那个MASViewConstraint对象,所以当top的调用进入方法,就会进入if ([constraint isKindOfClass:MASViewConstraint.class]) 这个分支中,最终把两次调用得到的两个约束存入MASCompositeConstraint对象中并返回。

分析到这里就不难理解了,MASViewConstraint是对一个单一的约束进行描述的对象,而MASCompositeConstraint则是对多个混合的约束进行描述的对象。这里就更加感受到两个派生类的妙用了吧。以及对方法的复用,不管外部使用者是一个一个约束的设置,还是混合着好几个约束一起设置,最后都会进入这个方法里一起处理,最终存入到约束数组中。

还剩下一个.mas_equalTo的调用,mas_equalTo方法返回一个block,上面也介绍过了,block块内的代码如下,在调用测通过.mas_equalTo()执行了这个block

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

根据源码可以看到这个block的参数是一个id类型,联想到我们日常的使用,不难知道,mas_equalTo后面可以传入一个数字,也可以是其他view的某个约束,比如mas_equalTo(self.button.mas_bottom)等,所以这里的类型不能是一个固定的类型。在回想一下NSLayoutConstraint的使用,想一想走到这一步,对于创建一个NSLayoutConstraint还缺哪些参数嘛?

[NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeTop
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeTop
                                multiplier:1.0
                                  constant:padding.top],
复制代码

你心里应该已经有了答案,mas_equalTo后面传入的数字,就是上面代码中的padding.top,如果mas_equalTo(self.button.mas_bottom)呢,self.button.mas_bottom是一个MASViewAttribute类型,最后会被解析为上面代码中的superviewNSLayoutAttributeTop这两个参数。

我们继续看源码:

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

        }
    };
}
复制代码

NSLayoutRelation是一个枚举值,这里我们调用了mas_equalTo传入的枚举值是NSLayoutRelationEqualattribute传入的是一个数字,并被处理成了NSNumber类型。attribute是数组类型的先不看,到这里,这句make.leading.top.mas_equalTo(0);代码终于是被执行完了。但是,到这里好像只是有了约束的各种描述,存在一个MASConstraint类型的对象中,还有最重要的一步没有做,那就是把约束加载到对应的view上,还需要[self addConstraints:@[constrant]];

又要回到最初的代码,我们这一系列的分析,都只是执行了一个block而已啊,就是这段代码中的block(constraintMaker);,这个方法还没有执行完,还有最后的一句代码[constraintMaker install]

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}
复制代码

install做了什么事呢:

- (void)install {
.....省略一些逻辑判断......
    MAS_VIEW firstLayoutItem = self.firstViewAttribute.item;
    NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
    MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
    NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
    if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
        secondLayoutItem = self.firstViewAttribute.view.superview;
        secondLayoutAttribute = firstLayoutAttribute;
    }
   
    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;
    
.....省略一些逻辑判断......
   
        [self.installedView addConstraint:layoutConstraint];

}
复制代码

省略了一些逻辑判断,install就做了一件最重要的事,那就是把前面组装的约束加载到view上,实现了对view的自动布局。

以上对源码的分析,设计了masory中最重要的几个类,以及链式调用,block的运用,还有很多可以深入研究的东西这里就不唠叨了。这篇浅析确实写的太唠叨了,自我吐槽一下。希望如此的唠叨,可以让每一位读到这篇文章的人都可以有自己的收获。

思考?

1.使用Masory为什么不会导致循环引用(block)?

这个问题比较简单,思考一下循环引用发生的条件就知道了,masonry中设置布局的⽅法中的block对象并没有被View所引⽤,⽽是直接在⽅法内部同步执⾏完成,不满足发生循环引用的条件。

2.mas_makeConstraints、mas_updateConstraints、mas_remakeConstraints的区别?

这三个方法是我们写布局代码时最常用的,也是文章开头我说的引子,理解了三者的区别才能在不同的场景下选择最适合的来用。深入源码,其实区别就一步了然了。

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    constraintMaker.updateExisting = YES;
    block(constraintMaker);
    return [constraintMaker install];
}

- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    constraintMaker.removeExisting = YES;
    block(constraintMaker);
    return [constraintMaker install];
}
复制代码

区别就只是设置了updateExistingremoveExisting,在install时根据这两个值,做了一系列对应的操作。先看removeExisting,也就是mas_remakeConstraints与mas_makeConstraints的区别:

- (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;
}
复制代码

如果设置了removeExisting,会先拿到view上已经设置的所有约束,然后遍历全部做一次uninstall,就是把view上所有的约束都删除掉,然后再处理新约束的install。所以mas_remakeConstraints是重新为view设置新的约束,完全不受之前约束的影响。

而对于updateExisting,此处只是把所有存储的约束都标记一下是需要更新的,并没有删除之前的约束。真正更新约束的代码在MASViewConstraintinstall方法中,就是上面我们分析源码的时候 ,省略的那一部分逻辑代码。

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];
    }
    
复制代码

如果设置了updateExisting,会去遍历view上已经设置的所有约束,然后遍历和当前这个约束做对比,如果找到了和当前这个约束一样的则返回,否则返回nil,如果找到了一样的约束,就直接做一次更新。这里的对比,是把firstItem、secondItem、firstAttribute、secondAttribute等等这些属性都做了对比,都相等才算找到,那么问题就来了,我们update了啥?

答案是:constant,是一个float值,就是NSLayoutConstraint中,表示偏移量的一个值,比如make.width.mas_equalTo(110);中,可以把110update为90这样。而对于make.size.mas_equalTo(CGSizeMake(6, 6));这种,也是可以更新的,因为其本质是更新了widthheight,分解成了两个约束。

我想结论应该是,mas_updateConstraints只可以更新约束中描述偏移量或者size的常量值。

猜你喜欢

转载自juejin.im/post/7104166192432021534