setNeedsLayout与layoutIfNeeded的区别

解释

更新布局总会重新触发layoutSubviews方法。

  • layoutSubviews
    继承于UIView的子类重写,进行布局更新,刷新视图。如果某个视图自身的bounds或者子视图的bounds发生改变,那么这个方法会在当前runloop结束的时候被调用。为什么不是立即调用呢?因为渲染毕竟比较消耗性能,特别是视图层级复杂的时候。这种机制下任何UI控件布局上的变动不会立即生效,而是每次间隔一个周期,所有UI控件在布局上的变动统一生效并且在视图上更新,苹果通过这种高性能的机制保障了视图渲染的流畅性。
image

从上图中可以看到,runloopobserver回调=>CoreAnimation渲染引擎一次事务的提交=>CoreAnimation递归查询图层是否有布局上的更新=>CALayer layoutSublayers=>UIView layoutSubviews 这样一个调用的流程。从这里也可以看到UIView其实就是相当于CALayer的代理。

image

顺便看一眼drawRect方法的调用栈,从CA::Layer::layout_and_display_if_needed方法之前都是一样的。

  • setNeedsLayout
    标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,在下一轮runloop结束前刷新,对于这一轮runloop之内的所有布局和UI上的更新只会刷新一次,layoutSubviews一定会被调用。
  • layoutIfNeeded
    如果有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)。

关键点

  • layoutIfNeeded不一定会调用layoutSubviews方法。
  • setNeedsLayout一定会调用layoutSubviews方法(有延迟,在下一轮runloop结束前)。
  • 如果想在当前runloop中立即刷新,调用顺序应该是
[self setNeedsLayout];
[self layoutIfNeeded];

反之可能会出现布局错误的问题。

前言:

开发得跟view打交道,我们也经常看到苹果官方代码有layout方法的相关调用,但是大家可知道什么时候调用,什么时候需要吗?针对网上大部分资料讲得不够清晰,我决定用Demo来讲解

一、layoutSubviews

不能直接调用这个方法。如果强制刷新布局,请调用 setNeedsLayout,如果想马上刷新界面,请调用layoutIfNeeded

二、setNeedsLayout跟layoutIfNeded

setNeedsLayout调整视图的子视图的布局时,是在应用程序的主线程调用此方法的。此方法记录调整的布局请求并立即返回。注意,此方法不会强制立即更新,而是等待下一个更新周期才进行刷新页面布局。此行为允许你将所有的布局更新合并到一个更新周期,这很适合用来优化性能。

首先我们在Main.storyboard拖取出一个adjust Height按钮以及带有约束的一个viewRed
如图:

image

viewRedConstraints拖到ViewController.swift,并作为属性

下面我用一段代码验证下

    @IBAction func adjustedBtnClick(_ sender: Any) {
        view.layoutIfNeeded()
        if self.redViewHeight.constant == 30.0 {
            self.redViewHeight.constant = self.view.frame.height - 90
        }else {
            self.redViewHeight.constant = 30.0
        }
        UIView.animate(withDuration: 2.0) { 
            self.view.layoutIfNeeded()
        }
    }

当按下按钮时,你看到的第一件事是调用view.layoutIfNeeded()。请记住,此方法强制立即布局并显示更新。你可能会想知道为什么在我们进行约束更改之前调用它呢,因为苹果认为这是一个最佳做法,以确保任何以前的更新等待更新周期的完成,所以我已经添加了它。

当这样的约束被更新时,它会自动执行相当于setNeedsLayout的操作,因此在下一个更新周期内不需要更新视图,所以不用额外添加代码,我们就会看到视图约束更新,但它不会有动画效果。但在我们的例子中,添加了2秒的动画块UIView.animate,在该块内,我们通过layoutIfNeeded方法强制立即布局。由于此布局同步发生,因此在动画块中捕获来自约束更改的帧移动,因此如果你现在运行应用程序,则可以看到红色视图在2秒钟内如何变大变小。
效果如下:

image

当我们把动画块代码替换成

   UIView.animate(withDuration: 2.0) { 
       self.view.setNeedsLayout()
   }

现在我们在动画块中正在做的是将视图标记为需要布局更新,但不会立即强制执行。相反,setNeedsLayout方法返回,视图只是在列表中,以在下一个更新周期中进行更新。净效果是在动画块中不会发生动画,因为该块中的视图没有更改。
效果如下:

image

在这种情况下单击按钮将立即根据更新的约束更新视图大小,而不是动画更新。等一下,如果我们没有使用layoutIfNeeded,为什么会立即?
对我们而言,红色视图的大小变化立即发生。起初它似乎是反直觉的,因为我们没有使用layoutIfNeeded强制立即更新。但是,我们没有在动画块的上下文中进行视图更新,也就是说刚好是一个周期,所以它似乎是立即的,跟没有UIView.animate这段代码效果是一样的。因为我们标记为更新,更新周期的发生的地方也是动画开始的地方。
因此,由于我们的代码已经标记该视图需要通过setNeedsLayout进行布局更新,所以是在更新周期中立即启动视图更新,而不是从更新周期开始约束更改和帧移动的动画。

刷新子对象布局:

-layoutSubviews方法:这个方法,默认没有做任何事情,需要子类进行重写
------------ 2018.8.25 更新 ------------
这个方法在iOS 5.1和更早版本上没有任何作用。子类可以根据需要重写此方法,以执行更精确的子视图布局。只有当子视图的自动调整、约束的行为不能满足你时,你才应该重写此方法

-setNeedsLayout方法: 标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,但layoutSubviews一定会被调用

-layoutIfNeeded方法:如果有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)关于在什么的情景会使用该方法,stackoverflow回答的比较详细

  • 如果要立即刷新,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局
  • 在视图第一次显示之前,标记肯定是“需要刷新”的,所以直接调用[view layoutIfNeeded]就会进行立即更新

你可能感兴趣的:(setNeedsLayout与layoutIfNeeded的区别)