iOS进阶之masonry细致入微(一)
2019-01-07 本文已影响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用来干啥?你可以尝试思考一下了。
未完待续......
作者声明:如果帮到你,或者你发现我的文章有错误,欢迎大家在评论区与我讨论,共同进步。