美文网首页 iOS进阶之Masonry
iOS进阶之masonry细致入微(一)

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

作者: 天蓬大元 | 来源:发表于2019-01-07 08:59 被阅读1次

    一,理清思路

    当前梳理文件坐标
    View+MASAdditions
    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);
    
    make.left_1
    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的对象,但只持有了当前要添加的约束,却没有持有添加约束的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) {
            ///注意这里用的是newConstraintm,该对象并没有持有view
            ///这里我们先不管设置代理的作用,其实,这个代理也很好玩,我们后面肯定会提到。
            newConstraint.delegate = self;
            ///将当前的这个持有约束类型的对象添加进数组进行存储
            [self.constraints addObject:newConstraint];
        }
        ///注意了,这里返回的对象是没有持有view的MASViewConstraint类的对象。接下来的点语法,就是由这个对象完成的。
        return newConstraint;
    }
    
    接下来是调用mas_equalTo(),由于前面的分析,我们得知,调用这个方法的对象是没有持有view的MASViewConstraint类的对象。接下来,我们看这个方法做了什么?
    这里插一点东西:
    /**
    *  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。我们给其一个名字叫:小黑。通俗点讲,没有持有view的MASViewConstraint类的对象调用这个方法,会得到一个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到底干了什么?
    先进行伪代码拆分
        小黑 = [没有持有view的MASViewConstraint类的对象 mas_equalTo(lable.superview.mas_left)];
        没有持有view的MASViewConstraint类的对象 = [没有持有view的MASViewConstraint类的对象 小黑];
        小黑做的事情:
         ^id(id attribute) {
            return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
        }
    
    好好的体会下这里小黑存在的意义!
    还记得这里的self代表的是哪个类的对象吗?答案是MASViewConstraint。
    @interface MASViewConstraint : MASConstraint
    
    多态的基本使用,这里self指代的是类MASViewConstraint的一个对象,只保存了当前要添加的约束,却没有保存添加约束的view。好,那我们来看看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该知道是哪个对象了吧,目前该对象不仅持有了要添加的约束类型,还持有了如何添加该约束的方式NSLayoutRelationEqual
                self.layoutRelation = relation;
                ///这里保存了外面传进来的存有调用lable.superview.mas_left返回的MASViewAttribute对象。
                self.secondViewAttribute = attribute;
                ///返回的还是这个self,你要时刻记得返回的对象是谁,这是理解链式调用中相当重要的一点。
                return self;
            }
        };
    }
    
    好,我们暂停一下,看看当前的返回对象都被添加了哪些东西。
    1,view要添加的约束类型,NSLayoutAttributeLeft
    2,view与superView的约束关系,NSLayoutRelationEqual
    3,view相对于superView的约束条件,利用MASViewAttribute对象封装
    好,我们继续
    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需要添加的约束(NSLayoutAttributeLeft),view与superView的约束关系(NSLayoutRelationEqual)以及view相对于superView的约束条件(利用MASViewAttribute对象封装)
    newConstraint持有该约束条件下的参数(10)。
    

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

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

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

    相关文章

      网友评论

        本文标题:iOS进阶之masonry细致入微(一)

        本文链接:https://www.haomeiwen.com/subject/mieyrqtx.html