iOS进阶之masonry细致入微(一)

一,理清思路

3109982-b96abd1262d8993c.jpg
当前梳理文件坐标

3109982-4398c7812c268764.jpg
View+MASAdditions

3109982-514bdb681ed684e2.jpg
NSArray+MASAdditions

二,深入实践

1,使用masonry,最常用的三个API
///设置约束
- (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;
///更新已有约束
- (NSArray *)mas_updateConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;
///重新设置约束
- (NSArray *)mas_remakeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;

2,了解mas_makeConstraints方法的调用全过程

///例子1
UILabel *lable = [[UILabel alloc]init];
[self.view addSubview:lable];
[lable mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.mas_equalTo(lable.superview.mas_left).offset(10);
    make.right.mas_equalTo(-10);
    make.top.mas_equalTo(0);
    make.bottom.mas_equalTo(0);
}];
UIKit中UILabel及其父类并没有[mas_makeConstraints]这个方法,且我们无法在其及其父类源文件中直接添加这个方法,那我们为什么可以使用呢?这里涉及到了一个知识点:Category。利用这个特性,我们可以在不更改原来类源码的情况下,在运行时给这个类动态添加方法。内部是利用runtime的特性实现的。但该特性无法直接给已知类添加成员变量,原因在于OC是C的超集,利用runtime运行时,将c语言封装成面向对象的OC。OC对象在编译时,编译器就已经将其在内存堆中的内存分布规划好,程序运行时,是无法动态改变某个对象的内存布局的。iOS编程中,有一个概念叫做组合,有一个规范叫做少用继承多用组合。这里讲的组合就是一个对象作为另一个对象的成员变量。比如自定义Person里面有很多成员变量,有字符串类型的名字,字典类型的各科成绩等,那么Person类的对象在编译时,编译器会为字符串对象和字典对象分配内存,这个过程中,Person类的对象的内存布局就确定了。如果你尝试在运行时以组合的方式给该Person类的对象添加一个成员变量,则必须为该成员变量分配内存。那么问题来了:这时该对象的内存本来已经分配好,其上下的位置的内存极有已经被使用,无法直接临近扩展内存,如果在其他位置分配内存,那这个对象的内存分布就会很混乱,而且容易导致内存浪费。所以,基于iOS的开发原则,我们无法使用Category来动态添加成员变量。那么,为什么可以动态添加方法呢?方法有自己的运行栈,当方法被执行时,系统会自动分配栈内存给函数使用。既然是在运行期被调用时才会分配内存,这就可以解释为什么可以动态添加方法了。有兴趣的读者,可以自行研究一下,运行时一个对象的组成,可以加深你的理解。当然,并不是一定就不能动态添加成员变量。在Masonry的MASViewConstraint的.m文件中,作者就实现了动态添加属性的操作。
由于Category的存在,使得我们可以轻松的使用Masonry。至于后面的回调操作,应该好理解。
我们来看看这个方法里做了什么?
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}
[self.translatesAutoresizingMaskIntoConstraints = NO];Masonry这个库本质上是对系统的autolayout的封装。如果要使用autolayout进行布局,则必须把当前view的translatesAutoresizingMaskIntoConstraints属性设置为NO。官方解释为:
如果此属性的值为YES,系统将创建一组约束,这些约束将复制视图的自动调整掩码指定的行为。这还允许您使用视图的框架,边界或中心属性修改视图的大小和位置,从而允许您在自动布局中创建静态的基于框架的布局。
请注意,自动调整遮罩约束完全指定视图的大小和位置;因此,如果不引入冲突,则无法添加其他约束来修改此大小或位置。如果要使用“自动布局”动态计算视图的大小和位置,则必须将此属性设置为“否”,然后为视图提供非模糊,非冲突的约束集。
默认情况下,对于以编程方式创建的任何视图,该属性都设置为YES。如果在Interface Builder中添加视图,系统会自动将此属性设置为NO。
这也告诉我们,在使用Masonry时,设置的约束是不能冲突的。
 MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
 block(constraintMaker);
使用当前的view对象创建了一个constraintMaker爱丽丝,并通过回调将该对象传到block块中使用。我们这里要记住,该对象持有了当前需要添加约束的view。
调用block,则代码调用又回到了view中。这里的代码执行顺序是:
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);

^(MASConstraintMaker *make) {
    make.left.mas_equalTo(lable.superview.mas_left).offset(10);
    make.right.mas_equalTo(-10);
    make.top.mas_equalTo(0);
    make.bottom.mas_equalTo(0);
}

return [constraintMaker install];
仔细想想这里的处理,感受一下代码的魅力,其实,Masonry这个库中,有许多有意思的代码处理,我们后面会一一提到。
block块中,我们的处理是给当前的view添加约束。那这里作者巧妙的引入了链式调用的方式,让添加约束的操作如丝般顺滑。
这个链式调用的处理,我们需要一点点道来。
链式调用,顾名思义,就是可以一直[点]下去。那么iOS开发中,哪里可以用点语法呢?对象对属性的调用时可以使用点语法。那么如果想实现对点语法的连续调用,则必须保证上一次的点语法调用要返回一个对象,这样才可以继续调用点语法。那么,我们来看一下Masonry是如何实现这种链式调用的。

make.left:

- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
这里我们先不管[self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];这个方法。这里我们可以看到,调用left后,会返回一个MASConstraint对象。后面的调用亦是如此。
支持链式调用的两个条件
    1,支持点语法
    2,返回一个接下来要使用的对象
如果你想写出这样的代码,就好好的看看Masonry这个库的源码吧。
等我们添加完约束后,才会去调用[constraintMaker install]方法,将约束加载到对应的view上。所以,当我们添加多个约束时,就需要暂时将约束存储起来,在install方法被调用时,统一加载。
///例子2
make.left.mas_equalTo(lable.superview.mas_left).offset(10);
3109982-b70daf3def0bf707.jpg
make.left_1

3109982-b697a2acc4bf202c.jpg
make.left_2

所以,重点在这个方法,我们来看一下这个方法干了什么?

- (MASConstraint *)constraint:(MASConstraint *)constraint
    addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute
{
    ///先利用当前要添加约束的view初始化一个MASViewAttribute的对象,还记得self.view是怎么回事吧!并且将要添加的约束也传进去。MASViewAttribute这个类的作用,我们稍后再说。
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    ///这个初始化一个MASViewConstraint的对象葛二蛋,同时持有了MASViewAttribute的一个对象,该对象持有要添加约束的view以及约束条件
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    ///这里目前传入的constraint为nil,所以我们先不管这个if里的东西
    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;
    }
    ///这里目前传入的constraint为nil,所以这个if里的代码会被执行。
    if (!constraint) {
        ///这里我们先不管设置代理的作用,其实,这个代理也很好玩,我们后面肯定会提到。
        newConstraint.delegate = self;
        ///将当前的对象葛二蛋添加进数组进行存储
        [self.constraints addObject:newConstraint];
    }
    ///注意了,这里返回的对象是MASViewConstraint类的对象葛二蛋。葛二蛋持有一个保存view和约束条件的MASViewAttribute对象。接下来的点语法,就是由葛二蛋完成的。
    return newConstraint;
}
接下来是调用mas_equalTo(),由于前面的分析,我们得知,调用这个方法的对象葛二蛋持有一个保存view和约束条件的MASViewAttribute对象。接下来,我们看这个方法做了什么?
这里插一点东西:
/**
*  Convenience auto-boxing macros for MASConstraint methods.
*
*  Defining MAS_SHORTHAND_GLOBALS will turn on auto-boxing for default syntax.
*  A potential drawback of this is that the unprefixed macros will appear in global scope.
*/
#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__)

#endif
equalTo与mas_equalTo的区别,这里就一目了然了。
#define MASBoxValue(value) _MASBoxValue(@encode(__typeof__((value))), (value))
/**
*  Given a scalar or struct value, wraps it in NSValue
*  Based on EXPObjectify: https://github.com/specta/expecta
*/
/**
*  inline关键字用来定义一个类的内联函数,引入它的主要原因是用它替代C中表达式形式的宏定义。
*/
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(double)) == 0) {
        double actual = (double)va_arg(v, double);
        obj = [NSNumber numberWithDouble:actual];
    } else if (strcmp(type, @encode(float)) == 0) {
        float actual = (float)va_arg(v, double);
        obj = [NSNumber numberWithFloat:actual];
    } else if (strcmp(type, @encode(int)) == 0) {
        int actual = (int)va_arg(v, int);
        obj = [NSNumber numberWithInt:actual];
    } else if (strcmp(type, @encode(long)) == 0) {
        long actual = (long)va_arg(v, long);
        obj = [NSNumber numberWithLong:actual];
    } else if (strcmp(type, @encode(long long)) == 0) {
        long long actual = (long long)va_arg(v, long long);
        obj = [NSNumber numberWithLongLong:actual];
    } else if (strcmp(type, @encode(short)) == 0) {
        short actual = (short)va_arg(v, int);
        obj = [NSNumber numberWithShort:actual];
    } else if (strcmp(type, @encode(char)) == 0) {
        char actual = (char)va_arg(v, int);
        obj = [NSNumber numberWithChar:actual];
    } else if (strcmp(type, @encode(bool)) == 0) {
        bool actual = (bool)va_arg(v, int);
        obj = [NSNumber numberWithBool:actual];
    } else if (strcmp(type, @encode(unsigned char)) == 0) {
        unsigned char actual = (unsigned char)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedChar:actual];
    } else if (strcmp(type, @encode(unsigned int)) == 0) {
        unsigned int actual = (unsigned int)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedInt:actual];
    } else if (strcmp(type, @encode(unsigned long)) == 0) {
        unsigned long actual = (unsigned long)va_arg(v, unsigned long);
        obj = [NSNumber numberWithUnsignedLong:actual];
    } else if (strcmp(type, @encode(unsigned long long)) == 0) {
        unsigned long long actual = (unsigned long long)va_arg(v, unsigned long long);
        obj = [NSNumber numberWithUnsignedLongLong:actual];
    } else if (strcmp(type, @encode(unsigned short)) == 0) {
        unsigned short actual = (unsigned short)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedShort:actual];
    }
    va_end(v);
    return obj;
}
根据值,获取类型,对基本数据使用NSNumber封装,方便入参统一使用id类型。是不是似曾相识?
让我们回到正题,mas_equalTo最终调用了equalTo,我们来看看这个函数干了什么?
/**
 *     Sets the constraint relation to NSLayoutRelationEqual
 *    returns a block which accepts one of the following:
 *    MASViewAttribute, UIView, NSValue, NSArray
 *    see readme for more details.
 */
- (MASConstraint * (^)(id attr))equalTo;
这个方法的返回值是一个block。我们给其一个名字叫:小黑。通俗点讲,葛二蛋调用这个方法,会得到一个block对象小黑。
该block可以接受任意对象作为入参,然后返回一个MASConstraint类族的对象。
先不急着强行理解,我们看下这个方法的实现。
- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {///第一个id是回调的返回值;第二个id是回调的参数。
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}
1,第一个 return在干什么?它在调用小黑,此时,小黑的入参(id attribute)就是(lable.superview.mas_left)。
2,第二个 return在干什么?它调用了self.equalToWithRelation(attribute, NSLayoutRelationEqual)方法,返回了一个id类型的对象。
好,到这里,我们就可以分析一下这个equalTo到底干了什么?
先进行伪代码拆分
    小黑 = [葛二蛋  mas_equalTo];
    葛二蛋 = [葛二蛋 小黑(lable.superview.mas_left)];
    小黑做的事情:
     ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    }
好好的体会下这里小黑存在的意义!
还记得这里的self代表的是哪个类的对象吗?答案是MASViewConstraint。
@interface MASViewConstraint : MASConstraint
多态的基本使用,这里self指代的是类MASViewConstraint的一个对象,好,那我们来看看equalToWithRelation这个方法做了什么?
- (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该知道是哪个对象了吧,目前葛二蛋不仅持有了要添加的view和约束类型,还持有了如何添加该约束的方式NSLayoutRelationEqual
            self.layoutRelation = relation;
            ///这里保存了外面传进来的存有调用lable.superview.mas_left返回的MASViewAttribute对象。这里你应该看一下源码,看看怎么获取到这个对象的。
            self.secondViewAttribute = attribute;
            ///返回的还是这个self,你要时刻记得返回的对象是谁,这是理解链式调用中相当重要的一点。
            return self;
        }
    };
}
好,我们暂停一下,看看当前的返回对象都被添加了哪些东西。
1,view要添加的约束类型和要添加约束的view,NSLayoutAttributeLeft
2,view与superView的约束关系,NSLayoutRelationEqual
3,view相对于superView的约束条件和要添加约束的view,NSLayoutAttributeLeft
这里需要提前点明:葛二蛋的firstViewAttribute存储第一条信息,layoutRelation存储了第二条信息,secondViewAttribute存储了第三条信息。
好,我们继续
make.left.mas_equalTo(lable.superview.mas_left).offset(10);
接下来是调用offset(),谁在调用呢?这个时候你不该还不知道谁在调用。
- (MASConstraint * (^)(CGFloat))offset {
    return ^id(CGFloat offset){
        self.offset = offset;
        return self;
    };
}
这里依然存在小黑的影子,不知道你是否理解了小黑存在的意义?多思考,远比被动接受有趣的多。
依然是多态的使用,我们不过多讨论。来看看里面干了啥?将传入的参数保存在self中,还是要强调,你要知道此时的self是谁。保存完成后,返回self。
截止到这里,我们就成功的将一条完整的约束包存到了self中。还记得哪里有保存这个self吗?往上翻翻,MASConstraintMaker对象调用left时,保存的newConstraint是不是self?如果还迷糊,你不妨从头再看一遍。
总结一下就是:
MASConstraintMaker对象:make
MASViewConstraint对象:newConstraint
首先初始化make,并持有当前需要添加约束的view对象(label),然后初始化newConstraint,并且将其保存到make的constraints数组中。
newConstraint对象持有view需要添加的约束和要添加约束的view(NSLayoutAttributeLeft),view与superView的约束关系(NSLayoutRelationEqual)以及view相对于superView的约束条件和要添加约束的view(NSLayoutAttributeLeft)
newConstraint持有该约束条件下的参数(10)。

这里的newConstraint就是葛二蛋。

===> label的左边距离lable.superview的左边10

MASConstraintMaker用来干啥?MASViewConstraint用来干啥?MASViewAttribute用来干啥?你可以尝试思考一下了。

未完待续......
作者声明:如果帮到你,或者你发现我的文章有错误,欢迎大家在评论区与我讨论,共同进步。

猜你喜欢

转载自blog.csdn.net/weixin_34006965/article/details/87492514
今日推荐