IOS基础:视图布局
原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- 一、Auto Layout
- 1、简介
- 2、原理
- 3、优先级约束
- 4、重新布局
- 二、Masonry
- 1、使用
- 常见用法
- 注意事项
- 实战:滚动视图
- 2、原理
- 通过点语法的链式调用进行布局
- Masonry框架的整体架构
- 1、使用
- 三、ASDK
- 1、简介
- 2、原理
- 3、特性
- a、ASDK 的图层预合成
- b、ASDK 异步并发操作
- c、Runloop 任务分发
- Demo
- 参考文献
一、Auto Layout
1、简介
在 iOS 平台上出现了不同尺寸的移动设备,使得原有的 frame
布局方式无法很好地适配不同尺寸的屏幕,所以,为了解决这一问题 Auto Layout
就诞生了。Auto Layout
的诞生并没有如同苹果的其它框架一样收到开发者的好评,它自诞生的第一天起就饱受 iOS 开发者的批评,其蹩脚、冗长的语法使得它在刚刚面世就被无数开发者吐槽,写了几个屏幕的代码都不能完成一个简单的布局。
2、原理
frame
时代下布局的需要的两个信息:origin/center
和 size(x & y、width & height)
以左上角的(0, 0)
为坐标的原点,找到坐标(x, y)
,然后绘制一个大小为(width, height)
的矩形,这样就完成了一个最简单的布局。而Auto Layout
的布局方式与上面所说的 frame
有些不同,frame
表示与父视图之间的绝对距离,但是 Auto Layout
中大部分的约束都是描述性的,表示视图间相对距离,类似这样:
A.left = Superview.left + 50
A.top = Superview.top + 30
A.width = 100
A.height = 100
B.left = (A.left + A.width)/(A.right) + 30
B.top = A.top
B.width = A.width
B.height = A.height
Xcode中实际代码如下:
// 翻译过来就是: 在view1的左侧,view2的右侧,再多10个点的地方
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:view2
attribute:NSLayoutAttributeRight
multiplier:1
constant:10];
虽然上面的约束很好的表示了各个视图之间的关系,但是 Auto Layout
实际上并没有改变原有的布局方式,只是将原有没有太多意义的(x, y)
值,变成了描述性的代码。在使用Auto Layout
进行布局时,可以指定一系列的约束,比如视图的高度、宽度等等。而每一个约束其实都是一个简单的线性等式或不等式,整个界面上的所有约束在一起就(没有冲突)定义了整个系统的布局。在涉及冲突发生时,Auto Layout
会尝试 break
一些优先级低的约束,尽量满足最多并且优先级最高的约束。因为布局系统在最后仍然需要通过 frame
来进行,所以 Auto Layout
虽然为开发者在描述布局时带来了一些好处,不过它相比原有的布局系统加入了从约束计算 frame
的过程,而在这里,我们需要了解 Auto Layout
的布局性能如何。
想要让 iOS 应用的视图保持 60 FPS
的刷新频率,我们必须在 1/60 = 16.67 ms
之内完成包括布局、绘制以及渲染等操作。也就是说如果当前界面上的视图大于 100 的话,使用 Auto Layout
是很难达到绝对流畅的要求的;而在使用 frame
时,同一个界面下哪怕有 500 个视图,也是可以在 16.67 ms
之内完成布局的。不过在一般情况下,在 iOS 的整个UIWindow
中也不会一次性出现如此多的视图。
3、优先级约束
抗拉伸优先级Content Hugging Priority
的水平和竖直方向的默认值都是250,而视图抗压缩优先级Content Compression Resistance Priority
的水平和竖直的默认值是750。
其实我不太明白这东西的意义在哪里,我做完Demo后也还是迷迷糊糊的。感觉时间被浪费在了这个无意义的东西上面。你不去设置这个东西,功能也自动实现了。
a、Content Hugging Priority(不想变大约束)
❶ 需求
Label1
和Label2
中的显示内容是从网络获取的,并且内容长度不定。我们要求优先显示Label1
,也就是说以Label1
的宽度为准,不过Label1
会有一个最大宽度,当Label1
显示到最大宽度时,Label1
的内容会被压缩,剩下的部分显示Label2
。当然,当Label1
没有显示到最大值时,剩下的部分仍然显示Label2
。Label2
显示不全的也会被压缩。
❷ 思路
接下来我们按照上述的描述添加相应的约束,我们为Label1
添加了Top
、Left
、Width
和Height
四个约束,这四个约束足以来确定该Label
的位置了。不过需要注意的一点该Label
的Width
是小于等于某一个值,此处我们指定的Width <= 200
。也就是该Label
的Width
的最大值为200。
因为要求前面Label
内容显示完成后,剩下的部分就显示右边Label
的内容,所以我们为Label2
添加了Left
、Top
、Right
以及Height
的约束。当然Left
是以右边的Label
为基准的,而Right
则是以父视图为基准的。
从下方截图中我们可以看出,有些约束添加完是红色的,这就是约束有冲突了。也就是当前添加的约束不能确实当前控件的位置。从上述的约束我们不难发现,横向来看,两个Label
的宽度都是不确定的,而且其内容环抱的优先级又是一致的,所以会报错。具体的错误信息是“Content Priority Ambiguity”
,也就是说内容优先级是模棱两可的,无法确定是先确定第一个Label
的宽度还是先确定第二个Label
的宽度。
解决方案是将右边的Label
的Content Hugging Priority
的优先级调低,当然第一个Label
的Content Hugging Priority
相对就高了,所以左边的Label
会优先确定其宽度。当左边Label
的宽度确定了,那么右边Label
的宽度也就是随着确定了,所以下方的错误也就解决了。
为了动态的看一下约束的效果,我们为每个Label
添加了一个Step
控件,该控件主要是用来控制对应Label
的大小的。
❸ 实现
优先级- (void)createSubviews
{
// Hug
UILabel *topLeftLabel = [[UILabel alloc] initWithFrame:CGRectZero];
topLeftLabel.text = @"宇";
topLeftLabel.backgroundColor = [UIColor redColor];
// 设置Hug,不想变大被拉伸
[topLeftLabel setContentHuggingPriority:251 forAxis:UILayoutConstraintAxisHorizontal];
[self.view addSubview:topLeftLabel];
[topLeftLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(topLeftStepper.mas_bottom).offset(20);
make.left.equalTo(self.view).offset(10);
make.width.mas_lessThanOrEqualTo(200);
make.height.equalTo(@100);
}];
self.topLeftLabel = topLeftLabel;
UILabel *topRightLabel = [[UILabel alloc] initWithFrame:CGRectZero];
topRightLabel.text = @"宙";
topRightLabel.backgroundColor = [UIColor yellowColor];
// 设置Hug
[topRightLabel setContentHuggingPriority:250 forAxis:UILayoutConstraintAxisHorizontal];
[self.view addSubview:topRightLabel];
[topRightLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(topRightStepper.mas_bottom).offset(20);
make.left.equalTo(topLeftLabel.mas_right).offset(8);
make.right.equalTo(self.view);
make.height.equalTo(@100);
}];
self.topRightLabel = topRightLabel;
}
#pragma mark - Events
- (void)clickTopLeftStepper:(id)sender
{
UIStepper *stepper = (UIStepper *)sender;
NSLog(@"stepperValue:%f,topLeftValue:%f",stepper.value,self.topLeftValue);
if (stepper.value > self.topLeftValue)
{
self.topLeftLabel.text = [self.topLeftLabel.text stringByAppendingString:@"宇"];
}
else
{
self.topLeftLabel.text = [self.topLeftLabel.text substringToIndex:(self.topLeftLabel.text.length - 1)];
}
self.topLeftValue = stepper.value;
}
- (void)clickTopRightStepper:(id)sender
{
UIStepper *stepper = (UIStepper *)sender;
if (stepper.value > self.topRightValue)
{
self.topRightLabel.text = [self.topRightLabel.text stringByAppendingString:@"宙"];
}
else
{
self.topRightLabel.text = [self.topRightLabel.text substringToIndex:(self.topRightLabel.text.length - 1)];
}
self.topRightValue = stepper.value;
}
b、Content Compression Priority(不想变小约束)
视图抗压缩优先级,该优先级越大则说明内容压缩阻力越大,也就是说内容越难被压缩。当两个Label
并排显示,并且屏幕不足以显示两个Label
的所有内容时,则会优先压缩抗压缩优先级越小的。
❶ 思路
为FirstLabel
添加的约束有Top
、Left
、Height
以及Width >= 50
,我们为SecondLabel
添加的约束为Left
(以First
的Right
为参照)、Top
、Right
、Height
以及Width>=100
。报错原因也很明确“Content Priority Ambiguity”
,也是内容优先级冲突,用大白话解释就是水平方向上无法确定两个Label
的宽度。
减少第二个Label
的水平压缩阻力,将现在的750修改成749,使得SecondLabel
在其他视图之前被裁剪。
为了直观的看一下该优先级的效果,我们添加了一个Switch
开关来修改上述两个Label
的优先级。当Switch
开关打开时,FirstLabel
的压缩阻力优先级大于SecondLabel
,开关关闭就相反了。
❷ 实现
- (void)createSubviews
{
// Compress
UILabel *bottomLeftLabel = [[UILabel alloc] initWithFrame:CGRectZero];
bottomLeftLabel.text = @"洪";
bottomLeftLabel.backgroundColor = [UIColor redColor];
[self.view addSubview:bottomLeftLabel];
[bottomLeftLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(bottomSwitch.mas_bottom).offset(20);
make.left.equalTo(self.view).offset(10);
make.width.mas_greaterThanOrEqualTo(50);
make.height.equalTo(@100);
}];
self.bottomLeftLabel = bottomLeftLabel;
UILabel *bottomRightLabel = [[UILabel alloc] initWithFrame:CGRectZero];
bottomRightLabel.text = @"荒";
bottomRightLabel.backgroundColor = [UIColor yellowColor];
[self.view addSubview:bottomRightLabel];
[bottomRightLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(bottomSwitch.mas_bottom).offset(20);
make.left.equalTo(bottomLeftLabel.mas_right).offset(8);
make.right.equalTo(self.view);
make.height.equalTo(@100);
make.width.mas_greaterThanOrEqualTo(100);
}];
self.bottomRightLabel = bottomRightLabel;
}
#pragma mark - Events
- (void)clickBottomLeftStepper:(id)sender
{
UIStepper *stepper = (UIStepper *)sender;
if (stepper.value > self.bottomLeftValue)
{
self.bottomLeftLabel.text = [self.bottomLeftLabel.text stringByAppendingString:@"洪"];
}
else
{
self.bottomLeftLabel.text = [self.bottomLeftLabel.text substringToIndex:(self.bottomLeftLabel.text.length - 1)];
}
self.bottomLeftValue = stepper.value;
}
- (void)clickBottomRightStepper:(id)sender
{
UIStepper *stepper = (UIStepper *)sender;
if (stepper.value > self.bottomRightValue)
{
self.bottomRightLabel.text = [self.bottomRightLabel.text stringByAppendingString:@"荒"];
}
else
{
self.bottomRightLabel.text = [self.bottomRightLabel.text substringToIndex:(self.bottomRightLabel.text.length - 1)];
}
self.bottomRightValue = stepper.value;
}
- (void)setPriority:(UISwitch *)sender
{
// 设置抗压缩,不想缩小
if (sender.isOn)
{
[self.bottomRightLabel setContentCompressionResistancePriority:755 forAxis:UILayoutConstraintAxisHorizontal];
}
else
{
[self.bottomRightLabel setContentCompressionResistancePriority:745 forAxis:UILayoutConstraintAxisHorizontal];
}
}
c、使用场景
❶ 需求
在不计算文字宽度和不修改约束的前提下,怎么通过设置Content Hugging Priority
和Content Compression Resistance Priority
属性来实现:
- 最左边是用户图像
- 黄色
Label
是用户昵称(昵称长度不确定) - 蓝色
Label
是评论发表时间(时间长度不确定) - 当用户昵称长度变长时,蓝色
label
自动右移,移到屏幕边缘时,用户昵称继续增加,昵称将缩略显示 - 当用户昵称变短时,蓝色
label
自动左移
❷ 思路
当用户昵称过长时,我们希望过长的部分省略显示,即昵称过长时,黄色label
优先被压缩,其横向抗压缩优先级要低。当用户昵称太短时,我们希望蓝色label
向左侧靠过来,即昵称太短时,蓝色label
向左靠,要被拉长,其横向抗拉伸优先级要低。
设置黄色label
的Content Compression Resistance Priority
(抗压缩优先级)横向优先级为749,修改蓝色label
的Content Hugging Priority
(抗拉伸优先级)横向优先级为250。
创建一个定时器用来时刻改变黄色label
的文字长度。
❸ 思路
static NSString *const NameText = @"这是一个很长很长的昵称";
static NSInteger changeLength = -1;// 记录单次变化长度
- (void)viewDidLoad
{
[super viewDidLoad];
[self createSubviews];
self.nameLeftLabel.text = NameText;
self.timeRightLabel.text = @"一周以前一周以前一周以前一周以前";
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(action) userInfo:nil repeats:YES];
}
- (void)createSubviews
{
// 昵称
UIImageView *portrait = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"luckcoffee.JPG"]];
[self.view addSubview:portrait];
[portrait mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view).offset(10);
make.width.height.equalTo(@50);
make.top.equalTo(bottomLeftLabel.mas_bottom).offset(20);
}];
UILabel *nameLeftLabel = [[UILabel alloc] initWithFrame:CGRectZero];
nameLeftLabel.backgroundColor = [UIColor blueColor];
[self.view addSubview:nameLeftLabel];
// 设置Hug,不想变大被拉伸
[nameLeftLabel setContentHuggingPriority:251 forAxis:UILayoutConstraintAxisHorizontal];// 251抗拒拉伸
// 设置抗压缩,不想缩小
[nameLeftLabel setContentCompressionResistancePriority:749 forAxis:UILayoutConstraintAxisHorizontal];// 749想缩小
[self.view addSubview:topLeftLabel];
[nameLeftLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(bottomLeftLabel.mas_bottom).offset(20);
make.left.equalTo(portrait.mas_right).offset(10);
make.height.equalTo(@50);
}];
self.nameLeftLabel = nameLeftLabel;
UILabel *timeRightLabel = [[UILabel alloc] initWithFrame:CGRectZero];
timeRightLabel.backgroundColor = [UIColor greenColor];
// 设置Hug,不想变大被拉伸
[nameLeftLabel setContentHuggingPriority:250 forAxis:UILayoutConstraintAxisHorizontal];// 250默认值
// 设置抗压缩,不想缩小
[nameLeftLabel setContentCompressionResistancePriority:750 forAxis:UILayoutConstraintAxisHorizontal];// 750默认值
[self.view addSubview:timeRightLabel];
[timeRightLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(bottomLeftLabel.mas_bottom).offset(20);
make.left.equalTo(nameLeftLabel.mas_right).offset(8);
make.right.equalTo(self.view);
make.height.equalTo(@50);
}];
self.timeRightLabel = timeRightLabel;
}
-(void)action
{
// 当前昵称
NSString *name = [NameText substringToIndex:self.nameLeftLabel.text.length + changeLength];
// 设置昵称
self.nameLeftLabel.text = name;
if(self.nameLeftLabel.text.length <= 3)
{
// 达到最小宽度后开始增加,步数为1
changeLength = 1;
}
else if(self.nameLeftLabel.text.length == NameText.length)
{
// 达到最大宽度后开始减少,步数为1
changeLength = -1;;
}
}
4、重新布局
重新布局a、View
.h文件
@interface LayoutHeaderView : UIView
@end
@interface LayoutFooterView : UIView
@end
@interface LayoutBodyView : UIView
@property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) UILabel *textLabel;
@end
.m文件
@implementation LayoutHeaderView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.layer.borderWidth = 2.0;
self.layer.borderColor = [UIColor redColor].CGColor;
}
return self;
}
@end
@implementation LayoutFooterView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.layer.borderWidth = 2.0;
self.layer.borderColor = [UIColor greenColor].CGColor;
}
return self;
}
@end
@implementation LayoutBodyView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.layer.borderWidth = 2.0;
self.layer.borderColor = [UIColor blueColor].CGColor;
self.textLabel = [[UILabel alloc] initWithFrame:CGRectZero];
self.textLabel.font = [UIFont systemFontOfSize:20.0];
self.textLabel.numberOfLines = 0;
self.textLabel.backgroundColor = [UIColor lightGrayColor];
[self addSubview:self.textLabel];
self.text = @"";
}
return self;
}
- (void)layoutSubviews
{
[super layoutSubviews];
// 根据宽度和字号自动计算Label高度
CGSize size = [self.text boundingRectWithSize:CGSizeMake(320, 2000) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:20.0]} context:nil].size;
self.textLabel.frame = CGRectMake(0, 0, 320, size.height);
}
- (void)setText:(NSString *)text
{
_text = text;
self.textLabel.text = text;
// 重新计算Label布局
[self setNeedsLayout];
}
@end
b、ViewController
扩展
@interface LayoutSubviewsViewController ()
@property (nonatomic, strong) LayoutHeaderView *headerView;
@property (nonatomic, strong) LayoutBodyView *bodyView;
@property (nonatomic, strong) LayoutFooterView *footerView;
@property (nonatomic, assign) BOOL bodyTextChanged;// 文本是否改变
@end
创建视图
- (void)createSubViews
{
self.headerView = [[LayoutHeaderView alloc] initWithFrame:CGRectMake(0, 164, 320, 100)];
[self.view addSubview:self.headerView];
self.bodyView = [[LayoutBodyView alloc] initWithFrame:CGRectMake(0, 264, 320, 304)];
[self.view addSubview:self.bodyView];
self.footerView = [[LayoutFooterView alloc] initWithFrame:CGRectMake(0, 568, 320, 100)];
[self.view addSubview:self.footerView];
self.bodyView.text = @"嗨,你好呀";
self.bodyTextChanged = YES;
UIButton *changeBodyBtn = [UIButton buttonWithType:UIButtonTypeCustom];
changeBodyBtn.frame = CGRectMake(0, 164, 200, 44);
[changeBodyBtn setTitle:@"更改文本" forState:UIControlStateNormal];
[changeBodyBtn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[changeBodyBtn addTarget:self action:@selector(changeBody) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:changeBodyBtn];
}
事件方法
- (void)changeBody
{
if (self.bodyTextChanged)
{
self.bodyView.text = @"一八五六至一八五七年间,法国《巴黎杂志》上连载的一部小说轰动了文坛,同时也在社会上引起了轩然大波。怒不可遏的司法当局对作者提起公诉,指控小说“伤风败俗、亵渎宗教”,并传唤作者到法庭受审。这位作者就是居斯塔夫·福楼拜,这部小说就是他的代表作《包法利夫人》。审判的闹剧最后以“宣判无罪”告结束,而隐居乡野、籍籍无名的作者却从此奠定了自己的文学声誉和在文学史上的地位。";
self.bodyTextChanged = NO;
}
else
{
self.bodyView.text = @"但奇怪的是,这个在家人眼中智力如此低下的居斯塔夫,却很早就显露了文学天赋。他还没有学会阅读便在头脑里构思故事,还没有学会写作就开始自编自演戏剧,他十三岁时编了一份手抄的小报,十四五岁已醉心于创作,可是直到三十六岁才开始发表作品。";
self.bodyTextChanged = YES;
}
}
二、Masonry
真正使 Auto Layout
大规模投入使用的应该还是 Masonry
,它使用了链式的语法对 Auto Layout
进行了很好的封装,使得 Auto Layout
更加简单易用。
1、使用
a、常见用法
支持的属性:
left 左
top 上
right 右
bottom 下
leading 左
trailing 右
width 宽
height 高
centerX x轴中心
centerY y轴中心
baseline 基线
leftMargin 左边默认边距好像是20,下面的类似
rightMargin
topMargin
bottomMargin
leadingMargin
trailingMargin
centerXWithinMargins
centerYWithinMargins;
edges 4边
size 大小
center 中心
常见用法:
// 常量
make.top.equalTo(@20);
// 点和面
make.center.mas_equalTo(CGPointMake(0, 50));
make.size.mas_equalTo(CGSizeMake(200, 100));
// 中央
make.centerX.equalTo(@0);
make.center.equalTo(self.bottomView);
// 多个约束可连在一起简写
make.top.left.equalTo(superView.mas_top).offset(padding);
// 能使用一组属性进行约束
make.height.equalTo(@[greenView.mas_height, redView.mas_height]);
// 倍数:底部视图宽的1/3高
make.height.equalTo(self.bottomInnerView.mas_width).multipliedBy(3);
// 约等于:运行时确定,比如文本框宽度随着输入增加,但有一个最大限度
make.width.height.lessThanOrEqualTo(self.bottomView);
支持的方法
-
mas_makeConstraints
只负责新增约束 -
mas_updateConstraints
针对上面的情况 会更新在block
中出现的约束 -
mas_remakeConstraints
则会清除之前的所有约束 仅保留最新的约束
优先级
- priority: 来设定一个明确地优先级的值,是有参数的
-
priorityHigh: 没有参数,使用默认的
MASLayoutPriorityDefaultHigh
值 -
priorityMedium:使用默认的
MASLayoutPriorityDefaultMedium
值 -
priorityLow:使用默认的
MASLayoutPriorityDefaultLow
值
Masonry
会优先实现优先级高的约束,发生冲突时,放弃优先级低的约束。
b、注意事项
- 不要使用数字12等命名,用
top
、bottom
命名 - 类似搭积木,上一个搭好了才能有下一个
- 前提:布局视图必须先被添加到父视图中
-
SnapKit
布局框架:就是Masonry
的Swift
版本
a、特殊的导航栏:自动根据bar
高度设置的引导属性值,存在navigationBar
时,mas_ topLayoutGuideBottom
相当于增加了44。不存在navigationBar
时,mas_ topLayoutGuideBottom
相对于0。
make.top.equalTo(self.mas_topLayoutGuide);//顶部
b、mas_equalTo 和 equalTo 区别:mas_equalTo
比equalTo
多了类型转换操作,一般来说,大多数时候两个方法都是通用的,但是对于数值元素使用mas_equalTo
,如make.height.mas_equalTo(200);
。对于对象或是多个属性的处理,使用equalTo
,如make.width.equalTo(view2);
,或者传入NSNumber对象make.height.equalTo(@200);
。特别是多个属性时,必须使用equalTo
,例如 make.left.and.right.equalTo(self.view)
。
c、with和and:这连个方法其实没有做任何操作,方法只是返回对象本身,这这个方法的左右完全是为了方法写的时候的可读性。
d、top和mas_top的区别:top
是MASConstraintMaker
的属性,mas_top
是view
的分类属性,因为平时我们肯定会用到类似UIView+Extension
的分类,为了避免和这些分类中的方法重名,才会有类似mas_top
、mas_center
的出现。总之make
后面用top
,view
后面用mas_top
。如make.top.equalTo(self.view.mas_top).offset(10);
c、实战:滚动视图
滚动视图scrollView
@property (strong, nonatomic) UIScrollView *scrollView;
UIScrollView *scrollView = UIScrollView.new;
self.scrollView = scrollView;
[self addSubview:scrollView];
[scrollView mas_makeConstraints:^(MASConstraintMaker *make) {make.edges.equalTo(self); }]
contentView
UIView *contentView = UIView.new;
[self.scrollView addSubview:contentView];
[contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.scrollView);
make.width.equalTo(self.scrollView);
}];
滚动视图内容布局
UIView *lastView;
CGFloat height = 25;
for (int i = 0; i < 10; i++) {
UIView *view = UIView.new;
view.backgroundColor = [self randomColor];
[contentView addSubview:view];
[view mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(lastView ? lastView.mas_bottom : @0);
make.left.equalTo(@0);
make.width.equalTo(contentView.mas_width);
make.height.equalTo(@(height));
}];
height += 25;
lastView = view;
}
[contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.equalTo(lastView.mas_bottom);
}];
2、原理
a、通过点语法的链式调用进行布局
源码学习应该直接从用到的方法着手,然后一步一步深入分析源码中每一步的目的和意义,顺藤摸瓜,逐个击破。
通过点语法的链式调用进行布局:原因就是getter
方法和Objective-C
里面,调用方法是可以使用点语法的,但这仅限于没有参数的方法。block
就是一个代码块,但是它的神奇之处在于在内联(inline)
执行的时候还可以传递参数。同时block
本身也可以被作为参数在方法和函数间传递。对以下代码进行解析:
make.top.right.bottom.left.equalTo(superview)
make.top
- 生成对象
A : MASViewConstraint(view.top)
- 将
A
的delegate
设为make
- 将
A
放入make
的constraints
中,此时make.constraints = [A]
- 返回A
make.top.right
- 生成对象
B:MASViewConstraint(view.right)
- 使用
A
和B
生成MASCompositeConstraint
对象C
,将C
的delegate
设为make
- 将
make.constraints
中替换成C
,此时make.constraints = [C]
,C.childConstraints = [A,B]
- 返回C
make.top.right.bottom
- 生成对象
D:MASViewConstraint(view.bottom)
,将D
的delegate
为C
- 将
D
放入C.childConstraints
中,此时C.childConstraints = [A,B,D]
- 返回C
make.top.right.bottom.left
- 生成对象
E:MASViewConstraint(view.left)
,E
的delegate
为C - 将
E
放入C.childConstraints
中,此时C.childConstraints = [A,B,D,E]
- 返回
C
make.top.right.bottom.left.equalTo(superview)
会依次调用A,B,D,E
的equalTo(superView)
b、Masonry框架的整体架构
Masonry框架的整体架构-
View+MASAdditions:最左边的红色框的这个类,这是
Masonry
框架最主要的一个类,主要是最下面的四个添加和修改约束的方法 -
MASConstraintMaker:中间绿色框中的这个类,这是
Masonry
框架中的过渡类,链接最左边和最右边之间的关系,也是链式语法的发起点和添加约束的执行点。MASConstraintMaker
类就是一个工厂类,负责创建和安装MASConstraint
类型的对象(依赖于MASConstraint
接口,而不依赖于具体实现) -
核心类:最右边的黄色框的这个类群,这是
Masonry
框架中的核心基础类群,这个类群又分为两个部分: -
约束类群:黄色框上面三个类,其中
MASConstraint
是一个抽象类,不可被实例化。我们可以将MASConstraint
是对NSLayoutConstriant
的封装,看做是一个接口或者协议。MASViewConstraint
和MASCompositeConstraint
都继承自MASConstraint
,其中MASViewConstraint
用于定义一个单独的约束,而MASCompositeConstraint
则用于定义一组约束条件,例如定义size
、insert
等参数时返回的其实都是MASCompositeConstraint
。 -
属性类群:主要是指
MASViewAttribute
,主要是对NSLayoutAttribute
的扩展,方便我们进行约束定义和修改 -
附属类群:还有一些工具类没有在这张图中进行展示,例如
NSArray+MASAdditions
、NSLayoutConstraint+MASDebugAdditions
、MASLayoutConstraint
等,都定义了一些工具和简化方法。
三、ASDK(AsyncDisplayKit)
1、简介
Auto Layout
不止在复杂 UI
界面布局的表现不佳,它还会强制视图在主线程上布局;所以在 ASDK
中提供了另一种可以在后台线程中运行的布局引擎 。ASDK
是一个很庞大的库,它本身并不推荐你把整个 App 全部都改为 ASDK
驱动,把最需要提升交互性能的地方用 ASDK
进行优化就足够了。
AsyncDisplayKit
是 Facebook
开源的一个用于保持 iOS 界面流畅的库,ASDK
的做法是将渲染绘制的工作抛到后台线程进行,并在每次 Runloop
结束时,将绘制结果交给 CALayer
进行展示。
其实 ASDK
的布局引擎大部分都是对 ComponentKit
的封装,不过由于摆脱了 Auto Layout
这一套低效但是通用的布局方式,ASDK
的布局计算不仅在后台并发线程中进行、而且通过引入Flexbox
提升了布局的性能,但是 ASDK
的使用相对比较复杂,如果只想对布局性能进行优化,更推荐单独使用ComponentKit
框架。
2、原理
ASDK 的基本原理ASDK
认为,阻塞主线程的任务,主要分为上面这三大类。文本和布局的计算、渲染、解码、绘制都可以通过各种方式异步执行,但 UIKit
和Core Animation
相关操作必需在主线程进行。ASDK
的目标,就是尽量把这些任务从主线程挪走,而挪不走的,就尽量优化性能。
常见的 UIView
和 CALayer
的关系:View
持有Layer
用于显示,View
中大部分显示属性实际是从 Layer
映射而来;Layer
的 delegate
在这里是 View
,当其属性改变、动画产生时,View
能够得到通知。UIView
和 CALayer
不是线程安全的,并且只能在主线程创建、访问和销毁。
ASDK
为此创建了 ASDisplayNode
类,包装了常见的视图属性(比如 frame/bounds/alpha/transform/backgroundColor/superNode/subNodes
等),然后它用UIView->CALayer
相同的方式,实现了 ASNode->UIView
这样一个关系。
当不需要响应触摸事件时,ASDisplayNode
可以被设置为 layer backed
,即 ASDisplayNode
充当了原来 UIView
的功能,节省了更多资源。
与 UIView
和 CALayer
不同,ASDisplayNode
是线程安全的,它可以在后台线程创建和修改。Node
刚创建时,并不会在内部新建 UIView
和 CALayer
,直到第一次在主线程访问 view
或layer
属性时,它才会在内部生成对应的对象。当它的属性(比如frame/transform
)改变后,它并不会立刻同步到其持有的 view
或layer
去,而是把被改变的属性保存到内部的一个中间变量,稍后在需要时,再通过某个机制一次性设置到内部的 view
或 layer
。
通过模拟和封装 UIView/CALayer
,开发者可以把代码中的UIView
替换为 ASNode
,很大的降低了开发和学习成本,同时能获得 ASDK
底层大量的性能优化。为了方便使用, ASDK
把大量常用控件都封装成了 ASNode
的子类,比如 Button
、Control
、Cell
、Image
、ImageView
、Text
、TableView
、CollectionView
等。利用这些控件,开发者可以尽量避免直接使用 UIKit
相关控件,以获得更完整的性能提升。
3、特性
a、ASDK 的图层预合成
有时一个 layer
会包含很多 sub-layer
,而这些 sub-layer
并不需要响应触摸事件,也不需要进行动画和位置调整。ASDK
为此实现了一个被称为 pre-composing
的技术,可以把这些 sub-layer 合成渲染为一张图片。开发时,
ASNode已经替代了
UIView和
CALayer;直接使用各种
Node控件并设置为
layer backed后,
ASNode甚至可以通过预合成来避免创建内部的
UIView和
CALayer`。
通过这种方式,把一个大的层级,通过一个大的绘制方法绘制到一张图上,性能会获得很大提升。CPU
避免了创建 UIKit
对象的资源消耗,GPU
避免了多张texture
合成和渲染的消耗,更少的 bitmap
也意味着更少的内存占用。
b、ASDK 异步并发操作
充分利用多核的优势、并发执行任务对保持界面流畅有很大作用。ASDK
把布局计算、文本排版、图片/文本/图形渲染等操作都封装成较小的任务,并利用GCD
异步并发执行。如果开发者使用了 ASNode
相关的控件,那么这些并发操作会自动在后台进行,无需进行过多配置。
c、Runloop 任务分发
任务分发Core Animation
在 RunLoop
中注册了一个 Observer
,监听了 BeforeWaiting
和 Exit
事件。这个Observer
的优先级是 2000000
,低于常见的其他 Observer
。当一个触摸事件到来时,RunLoop
被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置UIView
的frame
、修改 CALayer
的透明度、为视图添加一个动画;这些操作最终都会被 CALayer
捕获,并通过 CATransaction
提交到一个中间状态去。当上面所有操作结束后,RunLoop
即将进入休眠(或者退出)时,关注该事件的 Observer
都会得到通知。这时 Core Animation
注册的那个Observer
就会在回调中,把所有的中间状态合并提交到 GPU
去显示;如果此处有动画,Core Animation
会通过 DisplayLink
等机制多次触发相关流程。
ASDK
在此处模拟了 Core Animation
的这个机制:所有针对 ASNode
的修改和提交,总有些任务是必需放入主线程执行的。当出现这种任务时,ASNode
会把任务用 ASAsyncTransaction(Group)
封装并提交到一个全局的容器去。ASDK
也在 RunLoop
中注册了一个 Observer
,监视的事件和 Core Animation
一样,但优先级比Core Animation
要低。当 RunLoop
进入休眠前、Core Animation
处理完事件后,ASDK
就会执行该RunLoop
内提交的所有任务。通过这种机制,ASDK
可以在合适的机会把异步、并发的操作同步到主线程去。
Demo
Demo在我的Github上,欢迎下载。
ViewLayoutDemo
参考文献
iOS学习——布局利器Masonry框架源码深度剖析
AsyncDisplayKit
iOS开发之AutoLayout中的Content Hugging Priority和 Content Compression Resistance Priority解析