美文网首页
IOS基础:视图布局

IOS基础:视图布局

作者: 时光啊混蛋_97boy | 来源:发表于2020-10-21 09:21 被阅读0次

    原创:知识点总结性文章
    创作不易,请珍惜,之后会持续更新,不断完善
    个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
    温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

    目录

    • 一、Auto Layout
      • 1、简介
      • 2、原理
      • 3、优先级约束
      • 4、重新布局
    • 二、Masonry
      • 1、使用
        • 常见用法
        • 注意事项
        • 实战:滚动视图
      • 2、原理
        • 通过点语法的链式调用进行布局
        • Masonry框架的整体架构
    • 三、ASDK
      • 1、简介
      • 2、原理
      • 3、特性
        • a、ASDK 的图层预合成
        • b、ASDK 异步并发操作
        • c、Runloop 任务分发
    • Demo
    • 参考文献

    一、Auto Layout

    1、简介

    在 iOS 平台上出现了不同尺寸的移动设备,使得原有的 frame布局方式无法很好地适配不同尺寸的屏幕,所以,为了解决这一问题 Auto Layout就诞生了。Auto Layout 的诞生并没有如同苹果的其它框架一样收到开发者的好评,它自诞生的第一天起就饱受 iOS 开发者的批评,其蹩脚、冗长的语法使得它在刚刚面世就被无数开发者吐槽,写了几个屏幕的代码都不能完成一个简单的布局。

    2、原理

    frame 时代下布局的需要的两个信息:origin/centersize(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 的布局性能如何。

    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(不想变大约束)
    ❶ 需求

    Label1Label2中的显示内容是从网络获取的,并且内容长度不定。我们要求优先显示Label1,也就是说以Label1的宽度为准,不过Label1会有一个最大宽度,当Label1显示到最大宽度时,Label1的内容会被压缩,剩下的部分显示Label2。当然,当Label1没有显示到最大值时,剩下的部分仍然显示Label2Label2显示不全的也会被压缩。

    ❷ 思路

    接下来我们按照上述的描述添加相应的约束,我们为Label1添加了TopLeftWidthHeight四个约束,这四个约束足以来确定该Label的位置了。不过需要注意的一点该LabelWidth是小于等于某一个值,此处我们指定的Width <= 200。也就是该LabelWidth的最大值为200。

    因为要求前面Label内容显示完成后,剩下的部分就显示右边Label的内容,所以我们为Label2添加了LeftTopRight以及Height的约束。当然Left是以右边的Label为基准的,而Right则是以父视图为基准的。

    从下方截图中我们可以看出,有些约束添加完是红色的,这就是约束有冲突了。也就是当前添加的约束不能确实当前控件的位置。从上述的约束我们不难发现,横向来看,两个Label的宽度都是不确定的,而且其内容环抱的优先级又是一致的,所以会报错。具体的错误信息是“Content Priority Ambiguity”,也就是说内容优先级是模棱两可的,无法确定是先确定第一个Label的宽度还是先确定第二个Label的宽度。

    解决方案是将右边的LabelContent Hugging Priority的优先级调低,当然第一个LabelContent 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添加的约束有TopLeftHeight以及Width >= 50,我们为SecondLabel添加的约束为Left(以FirstRight为参照)、TopRightHeight以及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 PriorityContent Compression Resistance Priority属性来实现:

    Demo
    • 最左边是用户图像
    • 黄色Label是用户昵称(昵称长度不确定)
    • 蓝色Label是评论发表时间(时间长度不确定)
    • 当用户昵称长度变长时,蓝色label自动右移,移到屏幕边缘时,用户昵称继续增加,昵称将缩略显示
    • 当用户昵称变短时,蓝色label自动左移
    ❷ 思路

    当用户昵称过长时,我们希望过长的部分省略显示,即昵称过长时,黄色label优先被压缩,其横向抗压缩优先级要低。当用户昵称太短时,我们希望蓝色label向左侧靠过来,即昵称太短时,蓝色label向左靠,要被拉长,其横向抗拉伸优先级要低。

    设置黄色labelContent Compression Resistance Priority(抗压缩优先级)横向优先级为749,修改蓝色labelContent 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等命名,用topbottom命名
    • 类似搭积木,上一个搭好了才能有下一个
    • 前提:布局视图必须先被添加到父视图中
    • SnapKit布局框架:就是MasonrySwift版本

    a、特殊的导航栏:自动根据bar高度设置的引导属性值,存在navigationBar时,mas_ topLayoutGuideBottom相当于增加了44。不存在navigationBar时,mas_ topLayoutGuideBottom 相对于0。

    make.top.equalTo(self.mas_topLayoutGuide);//顶部
    

    b、mas_equalTo 和 equalTo 区别:mas_equalToequalTo多了类型转换操作,一般来说,大多数时候两个方法都是通用的,但是对于数值元素使用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的区别topMASConstraintMaker的属性,mas_topview的分类属性,因为平时我们肯定会用到类似UIView+Extension的分类,为了避免和这些分类中的方法重名,才会有类似mas_topmas_center的出现。总之make后面用topview后面用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

    1. 生成对象A : MASViewConstraint(view.top)
    2. Adelegate设为make
    3. A放入makeconstraints中,此时make.constraints = [A]
    4. 返回A

    make.top.right

    1. 生成对象B:MASViewConstraint(view.right)
    2. 使用AB生成MASCompositeConstraint对象C,将Cdelegate设为make
    3. make.constraints中替换成C,此时make.constraints = [C]C.childConstraints = [A,B]
    4. 返回C

    make.top.right.bottom

    1. 生成对象D:MASViewConstraint(view.bottom),将DdelegateC
    2. D放入C.childConstraints中,此时C.childConstraints = [A,B,D]
    3. 返回C

    make.top.right.bottom.left

    1. 生成对象E:MASViewConstraint(view.left)Edelegate为C
    2. E放入C.childConstraints中,此时C.childConstraints = [A,B,D,E]
    3. 返回C

    make.top.right.bottom.left.equalTo(superview)
    会依次调用A,B,D,EequalTo(superView)

    b、Masonry框架的整体架构

    Masonry框架的整体架构
    • View+MASAdditions:最左边的红色框的这个类,这是Masonry框架最主要的一个类,主要是最下面的四个添加和修改约束的方法

    • MASConstraintMaker:中间绿色框中的这个类,这是Masonry框架中的过渡类,链接最左边和最右边之间的关系,也是链式语法的发起点和添加约束的执行点。MASConstraintMaker类就是一个工厂类,负责创建和安装MASConstraint类型的对象(依赖于MASConstraint接口,而不依赖于具体实现)

    • 核心类:最右边的黄色框的这个类群,这是Masonry框架中的核心基础类群,这个类群又分为两个部分:

    • 约束类群:黄色框上面三个类,其中MASConstraint是一个抽象类,不可被实例化。我们可以将MASConstraint是对NSLayoutConstriant的封装,看做是一个接口或者协议。MASViewConstraintMASCompositeConstraint都继承自MASConstraint,其中MASViewConstraint用于定义一个单独的约束,而MASCompositeConstraint则用于定义一组约束条件,例如定义sizeinsert等参数时返回的其实都是MASCompositeConstraint

    • 属性类群:主要是指MASViewAttribute,主要是对NSLayoutAttribute的扩展,方便我们进行约束定义和修改

    • 附属类群:还有一些工具类没有在这张图中进行展示,例如NSArray+MASAdditionsNSLayoutConstraint+MASDebugAdditionsMASLayoutConstraint等,都定义了一些工具和简化方法。

    三、ASDK(AsyncDisplayKit)

    1、简介

    Auto Layout 不止在复杂 UI 界面布局的表现不佳,它还会强制视图在主线程上布局;所以在 ASDK 中提供了另一种可以在后台线程中运行的布局引擎 。ASDK 是一个很庞大的库,它本身并不推荐你把整个 App 全部都改为 ASDK 驱动,把最需要提升交互性能的地方用 ASDK 进行优化就足够了。

    AsyncDisplayKitFacebook 开源的一个用于保持 iOS 界面流畅的库,ASDK 的做法是将渲染绘制的工作抛到后台线程进行,并在每次 Runloop 结束时,将绘制结果交给 CALayer进行展示。

    其实 ASDK 的布局引擎大部分都是对 ComponentKit 的封装,不过由于摆脱了 Auto Layout 这一套低效但是通用的布局方式,ASDK 的布局计算不仅在后台并发线程中进行、而且通过引入Flexbox 提升了布局的性能,但是 ASDK 的使用相对比较复杂,如果只想对布局性能进行优化,更推荐单独使用ComponentKit框架。

    2、原理

    ASDK 的基本原理

    ASDK认为,阻塞主线程的任务,主要分为上面这三大类。文本和布局的计算、渲染、解码、绘制都可以通过各种方式异步执行,但 UIKitCore Animation 相关操作必需在主线程进行。ASDK 的目标,就是尽量把这些任务从主线程挪走,而挪不走的,就尽量优化性能。

    ASDisplayNode

    常见的 UIViewCALayer 的关系:View 持有Layer用于显示,View 中大部分显示属性实际是从 Layer映射而来;Layerdelegate 在这里是 View,当其属性改变、动画产生时,View 能够得到通知。UIViewCALayer 不是线程安全的,并且只能在主线程创建、访问和销毁。

    ASDK 为此创建了 ASDisplayNode 类,包装了常见的视图属性(比如 frame/bounds/alpha/transform/backgroundColor/superNode/subNodes 等),然后它用UIView->CALayer 相同的方式,实现了 ASNode->UIView 这样一个关系。

    ASDisplayNode 充当了原来 UIView 的功能

    当不需要响应触摸事件时,ASDisplayNode 可以被设置为 layer backed,即 ASDisplayNode 充当了原来 UIView 的功能,节省了更多资源。

    UIViewCALayer不同,ASDisplayNode 是线程安全的,它可以在后台线程创建和修改。Node 刚创建时,并不会在内部新建 UIViewCALayer,直到第一次在主线程访问 viewlayer属性时,它才会在内部生成对应的对象。当它的属性(比如frame/transform)改变后,它并不会立刻同步到其持有的 viewlayer去,而是把被改变的属性保存到内部的一个中间变量,稍后在需要时,再通过某个机制一次性设置到内部的 viewlayer

    通过模拟和封装 UIView/CALayer,开发者可以把代码中的UIView替换为 ASNode,很大的降低了开发和学习成本,同时能获得 ASDK 底层大量的性能优化。为了方便使用, ASDK 把大量常用控件都封装成了 ASNode 的子类,比如 ButtonControlCellImageImageViewTextTableViewCollectionView 等。利用这些控件,开发者可以尽量避免直接使用 UIKit相关控件,以获得更完整的性能提升。

    3、特性

    a、ASDK 的图层预合成

    有时一个 layer会包含很多 sub-layer,而这些 sub-layer并不需要响应触摸事件,也不需要进行动画和位置调整。ASDK 为此实现了一个被称为 pre-composing 的技术,可以把这些 sub-layer 合成渲染为一张图片。开发时,ASNode已经替代了UIViewCALayer;直接使用各种 Node控件并设置为layer backed后,ASNode甚至可以通过预合成来避免创建内部的UIViewCALayer`。

    通过这种方式,把一个大的层级,通过一个大的绘制方法绘制到一张图上,性能会获得很大提升。CPU 避免了创建 UIKit 对象的资源消耗,GPU 避免了多张texture合成和渲染的消耗,更少的 bitmap 也意味着更少的内存占用。

    b、ASDK 异步并发操作

    充分利用多核的优势、并发执行任务对保持界面流畅有很大作用。ASDK 把布局计算、文本排版、图片/文本/图形渲染等操作都封装成较小的任务,并利用GCD 异步并发执行。如果开发者使用了 ASNode 相关的控件,那么这些并发操作会自动在后台进行,无需进行过多配置。

    c、Runloop 任务分发

    任务分发

    Core AnimationRunLoop 中注册了一个 Observer,监听了 BeforeWaitingExit 事件。这个Observer的优先级是 2000000,低于常见的其他 Observer。当一个触摸事件到来时,RunLoop被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置UIViewframe、修改 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解析

    相关文章

      网友评论

          本文标题:IOS基础:视图布局

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