VC的布局时机、所用方法以及UIView内部布局执行顺序

8,273 阅读6分钟

前言

听说首图能吸引人点进来

Masonry时,刚设置完布局后想使用frame干点坏事,发现并不是期望的值

- (void)viewDidLoad {
    self.btn = [[UIButton alloc] init];
    [self.view addSubview:self.btn];
    [self.btn mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerX.equalTo(self.view.mas_centerX);
        make.centerY.equalTo(self.view.mas_centerY);
        make.width.equalTo(@100);
        make.height.equalTo(@50);
    }];
    NSLog(@"%@",self.btn);// 输出frame = (0 0; 0 0) 
//  [self.view layoutIfNeeded];
}

然后试了在viewWillAppear首次出现界面还是没获取到期望值, 在viewDidAppear才获取到期望值。 解决方法:在viewDidLoad定义完Masonrybolck后调用一下[self.view layoutIfNeeded],就能马上获取到期望值。 猜测应该是VC调用了某些布局方法。 并且这些方法在viewWillAppearDidAppear之间也调用了。 现在就来分析为什么会这样。


controller对view的布局时机和所用方法


VC的生命周期

这个执行顺序挺容易理解的。

alloc:创建对象,分配空间
init (xib和非xib用initWithNibName、stroyBoard用initWithCoder) :初始化对象,初始化数据
awakeFromNib:(若控制器有关联xib才调用这方法)
loadView:优先从nib载入控制器视图 ,其次代码
viewDidLoad:载入完成,可以进行自定义数据以及动态创建其他控件。
viewWillAppear:视图将出现在屏幕之前,马上这个视图就会被展现在屏幕上了
viewWillLayoutSubviews:控制器的view将要布局子控件
viewDidLayoutSubviews:控制器的view布局子控件完成
//这期间系统可能会多次调用viewWillLayoutSubviews 、    viewDidLayoutSubviews 俩个方法
viewDidAppear:视图已在屏幕上渲染完成
viewWillDisappear:视图将被从屏幕上移除之前执行
viewDidDisappear:视图已经被从屏幕上移除,用户看不到这个视图了
dealloc:视图被销毁,此处需要对你在init和viewDidLoad中创建的对象进行释放
didReceiveMemoryWarning:收到内存警告

调用顺序

可以看到和布局有关的两个方法确实夹在viewWillAppearviewDidAppear之间,且有可能会调用多次。

// layoutSubviews调用时机
1. 当view被添加到另一个view上使用
2. 布局自己子控件时使用
3. 屏幕打横时
4. 当自己的frame发生变化时,例如手动修改、热点等。

(实测的时候发现若在viewDidLoad中,self.view加了(无论多少个)子控件VC会分别调用两次这些方法,没添加子控件分别调用一次)。原因是layoutSubviews的调用时机。被添加到另一个view时调用一次;如果加了子控件,布局子控件也会调用一次。


接下来分析[self.view layoutIfNeeded]为什么能实现立即刷新frame

首先,Masonry是建立在autolayout之上的,最终转化frame。 一开始让我惊讶的是,Masonry约束的bolck会马上执行。但frame不能立即获取。原因是RunLoop下一个循环到来才会刷新UI。

frame生成的过程

结论:

viewDidLoad定义完Masonryblock后,(从上图可以看出过了少于0.1秒的时间内)两布局方法就调用完成frame也被算出来并在画面上描绘好view了。 如果定义完后直接调用[self.view layoutIfNeeded],不用等到下一个cycle,VC会在该函数内马上同步调用viewWillLayoutSubviewsviewDidLayoutSubviews各一次,这时候frame就是期望值了。


以上弄清楚Masonry不能立即获取frame原因了。但都是分析VCUIView布局方式。那么view中实现内部布局又是怎么个程序执行顺序呢?


UIView内部布局执行顺序

布局相关方法

  • 可以分为三块 updateConstraints <--> layout --> display 前两个与布局有关,第三个与渲染有关。
// 三块的主要方法
#pragma mark - updateConstraints
//看上去好像set和get方法,但是set方法并无参数,调用就会标记为YES。
//init后调用get方法发现是YES。
setNeedsUpdateConstraints:标记需要updateConstraints。
needsUpdateConstraints:返回是否需要updateConstraints。


updateConstraintsIfNeeded:若需要,马上updateConstraints。
updateConstraints:更新约束,自定义view应该重写此方法在其中建立constraints. 注意:要在最后调用[super updateConstraints]

#pragma mark - layout
layoutIfNeeded:使用此方法强制立即进行layout,从当前view开始,此方法会遍历整个view层次(包括superviews)请求layout。因此,调用此方法会强制整个view层次布局。
setNeedsLayout:此方法会将view当前的layout设置为无效的,并在下一个upadte cycle里去触发layout更新。
layoutSubviews:如果你需要更精确控制子view,而不是使用限制或autoresizing行为,就需要实现该方法。

#pragma mark - display
setNeedsDisplay:标记整个视图的边界矩形需要重绘.
drawRect:如果你的View画自定义的内容,就要实现该方法,否则避免覆盖该方法。

分享一下我对这些方法的理解,应该对理解后面过程有帮助。如果有错误的地方,欢迎指出来。

  • 整体分成了 怎么执行需要执行的标记马上执行布局
  • 怎么执行的三类方法layoutSubviewsdrawupdateConstraints只应该被重载,绝不要在代码中显式地调用,系统会在需要的时候自动调用。 举个例子
某个view.m
  - (void)updateConstraints {
    [self.sourceCollectionView mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self);
    }];
    //super必须写在最后
    [super updateConstraints];
}

- (void)setModelArray:(NSArray *)modelArray{
    CGRect newFrame = self.frame;
    newFrame.size.height = modelArray.count * 35;
    self.frame = newFrame;

    [self updateConstraintsIfNeeded];
}

updateConstraints里写好了view内部某个Collectionview的布局。当传入模型后,view的高度改变,调用updateConstraintsIfNeeded或者setNeedsUpdateConstraints,而不要显示调用updateConstraints,在VC调用布局方法时自然会跑这个方法。

  • 很多情况下系统都会把view的需要执行标记置为YES。
  • updateConstraints是子控件对父控件的。 layoutSubviews是父控件对子控件的。会递归调用子控件的layoutSubviewsdisplay先渲染父控件,再渲染子控件。
  • 布局运行在update cycle中,一般不卡的话,1/60s就会更新一遍。
  • view的以上三个执行标记发生改变,要等到下一次update cycle后,VC才会调用布局方法计算好frame并渲染到屏幕上。
  • updateConstraintsIfNeededlayoutIfNeeded这两个马上执行方法是给我们调用的,告诉系统不用等到下一个update cycle,VC马上执行布局方法。
  • viewinit后调用needsUpdateConstraints返回YES。而暴露的set方法只能标记为YES,作用应该是告诉系统下一次cycle要更新约束。猜测底层布局好后会有别的set方法置为NO。

分析执行顺序

  • 情形1 创建HSUTestView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.testView = [[HSUTestView alloc] init];
    self.testView.backgroundColor = [UIColor blueColor];
    [self.view addSubview:self.testView];
    [self.testView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerX.equalTo(self.view.mas_centerX);
        make.centerY.equalTo(self.view.mas_centerY);
        make.width.equalTo(@100);
        make.height.equalTo(@50);
    }];
}

UIView内部执行顺序

可以看到init后把三个标记都置为YES。 然后在VC的布局方式中,viewWillLayoutSubviews中会调用updateConstraints,在viewDidLayoutSubviews会调用layoutSubviewsdraw。所以说不要显示调用 怎么执行 这三个方法。

  • 情形2 创建HSUContentView 然后add HSUTestView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    HSUContentView *contentView = [[HSUContentView alloc] init];
    [self.view addSubview:contentView];
    [contentView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerX.equalTo(self.view.mas_centerX);
        make.centerY.equalTo(self.view.mas_centerY);
        make.width.equalTo(@100);
        make.height.equalTo(@50);
    }];
    self.testView = [[HSUTestView alloc] init];
    self.testView.backgroundColor = [UIColor blueColor];
    [contentView addSubview:self.testView];
    [self.testView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(contentView);
    }];
}

view加view的执行顺序
可以看到updateConstraints是子到父。layoutSubviewsdrawRect是父到子。

最后一张图总结UIView内部布局执行顺序与VC的交互

UIView内部布局执行顺序与VC的交互


补充 以下情形会调用layoutSubviews

1、init初始化不会触发layoutSubviews 
但是是用initWithFrame 进行初始化时,当rect的值不为CGRectZero时,也会触发

2、addSubview会触发layoutSubviews

3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化

4、滚动一个UIScrollView会触发layoutSubviews

5、旋转Screen会触发父UIView上的layoutSubviews事件

6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件

7、直接调用setLayoutSubviews。

8、直接调用setNeedsLayout。

参考