iOS开发-layoutSubviews、setNeedsLayout、layoutIfNeeded的爱恨情仇

iOS 中 UIKit 为 UIView 提供了这些方法来进行视图的更新与重绘:

//更新方法
- (void)setNeedsLayout;
- (void)layoutIfNeeded;

- (void)layoutSubviews;

//重绘方法
- (void)drawRect:(CGRect)rect;

- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;

View Drawing Cycle

要理解视图的更新与重绘,那么就必然要知道 iOS 的绘图机制 The View Drawing Cycle。

那么这个 The View Drawing Cycle 到底是什么呢,官方是这样解释的:

The system waits until the end of the current run loop before initiating any drawing operations. This delay gives you a chance to invalidate multiple views, add or remove views from your hierarchy, hide views, resize views, and reposition views all at once. All of the changes you make are then reflected at the same time.

翻译:在开始任何绘图操作之前,系统将一直等到当前RunLoop结束。这种延迟使您有机会使多个视图失效、从层次结构中添加或删除视图、隐藏视图、调整视图大小以及同时重新定位视图。然后,您所做的所有更改都会同时反映出来。

显然修改 UIView 的 frame、bounds 等属性后并不会马上重新绘制,而是用 RunLoop 把多次绘制聚集在一个 Cycle 里一起渲染,这显然是更加高效的行为。 

layoutSubviews

layoutSubviews 是为了对 subviews 重新布局,layoutSubviews 默认不做任何事情,需要子类进行重写。

我们不应该直接调用 layoutSubviews 方法,如果要强制进行布局更新,应在下次 The View Drawing Cycle 之前调用 setNeedsLayout 方法。

如果不想等待 The View Drawing Cycle,要立即更新视图的布局,请调用 layoutIfNeeded 方法。

layoutSubviews的触发机制:

  • init 初始化不会触发 layoutSubviews
  • addSubview 会触发 layoutSubviews
  • 修改 view 的 frame 会触发 layoutSubviews(注意是修改)
  • 滚动一个 UIScrollView 会触发 layoutSubviews
  • 旋转 Screen 会触发父视图上的 layoutSubviews 
  • 改变 view 大小的时候也会触发父视图上的 layoutSubviews

验证layoutSubviews的触发机制:

1、创建一个 ISTTestView,然后重写 layoutSubviews。

- (void)layoutSubviews {
    NSLog(@"layoutSubviews 被调用");
}

2、然后在 viewDidLoad中初始化一个ISTTestView *view,但不赋值frame。

- (void)viewDidLoad {
    [super viewDidLoad];
    ISTTestView *view = [[ISTTestView alloc]init];
}

运行后,控制台无信息输出,所以说明 <init 初始化不会触发 layoutSubviews>。

3、接着给view的frame赋值,但不addSubview。

- (void)viewDidLoad {
    [super viewDidLoad];
    ISTTestView *view = [[ISTTestView alloc]init];
    view.frame = CGRectMake(100, 100, 100, 100);
}

运行后,控制台无信息输出,但这并不足以说明修改 view 的 frame 不会触发 layoutSubviews。

4、然后撤销给view的frame赋值,addSubview。 

- (void)viewDidLoad {
    [super viewDidLoad];
    ISTTestView *view = [[ISTTestView alloc]init];
    //view.frame = CGRectMake(100, 100, 100, 100);
    [self.view addSubview:view];
}

运行后,控制台输出下面信息,说明<addSubview 的确会触发 layoutSubviews>。

iSatan[53376:4525497] layoutSubviews 被调用

5、然后给view的frame赋值,且addSubview,并让view在touchesBegan事件中修改frame。 

- (void)viewDidLoad {
    [super viewDidLoad];
    ISTTestView *view = [[ISTTestView alloc]init];
    view.frame = CGRectMake(100, 100, 100, 100);
    [self.view addSubview:view];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    static bool is;
    if ((is = !is)) {
        self.view.subviews.firstObject.frame = CGRectMake(100, 100, 200, 100);
    } else {
        self.view.subviews.firstObject.frame = CGRectMake(100, 100, 100, 100);
    }
}

运行后, 控制台输出下面信息,说明<修改 view 的 frame 的确会触发 layoutSubviews,但是需要addSubview之后>。

//运行但未点击屏幕
iSatan[53438:4541988] layoutSubviews 被调用

//点击屏幕
iSatan[53438:4541988] layoutSubviews 被调用

6、给view添加一个子label,然后在touchesBegan事件中修改label的frame。

运行后, 控制台输出下面信息,说明<改变 view 大小的时候也会触发父视图上的 layoutSubviews>。

//运行但未点击屏幕
iSatan[1718:156422] layoutSubviews 被调用

//点击屏幕
iSatan[1718:156422] layoutSubviews 被调用

setNeedsLayout

官方文档是这样描述setNeedsLayout方法的:

Call this method on your application’s main thread when you want to adjust the layout of a view’s subviews. This method makes a note of the request and returns immediately. Because this method does not force an immediate update, but instead waits for the next update cycle, you can use it to invalidate the layout of multiple views before any of those views are updated. This behavior allows you to consolidate all of your layout updates to one update cycle, which is usually better for performance.

翻译:当需要调整视图子视图的布局时,在应用程序的主线程上调用此方法。此方法记录请求并立即返回。由于此方法不强制立即更新,而是等待下一个更新周期,因此可以在更新任何视图之前使用它来使多个视图的布局无效。此行为允许您将所有布局更新合并到一个更新周期,这通常对性能更好。

当一个UIView对象调用 setNeedsLayout 方法时,实际上等同于做了一个标记,告诉系统需要重新布局,但不会立刻执行,直到 drawing cycle 循环到达该节点时,才会调用 layoutSubviews 方法重新布局。

layoutIfNeeded

Use this method to force the view to update its layout immediately. When using Auto Layout, the layout engine updates the position of views as needed to satisfy changes in constraints. Using the view that receives the message as the root view, this method lays out the view subtree starting at the root. If no layout updates are pending, this method exits without modifying the layout or calling any layout-related callbacks.

翻译:使用此方法强制视图立即更新其布局。使用自动布局时,布局引擎会根据需要更新视图的位置,以满足约束的更改。使用接收消息的视图作为根视图,此方法从根开始布局视图子树。如果没有挂起的布局更新,则此方法在不修改布局或调用任何与布局相关的回调的情况下退出。

如果布局更新处于待处理状态,则立即布置子视图。

一般,在我们需要立即更新约束来显示过渡动画效果才会使用这个方法。

self.margin.constant = 100;
[UIView animateWithDuration:0.25 animations:^{
    [self.view layoutIfNeeded];
}];

 

猜你喜欢

转载自blog.csdn.net/qq_36557133/article/details/98411856