layoutIfNeeded, setNeedsLayout
setNeedsUpdateConstraints, updateConstraintsIfNeeded
setNeedsDisplay
一. setNeedsUpdateConstraints, updateConstraintsIfNeeded
这两个方法涉及到Auto Layout相关知识 autolayout详细说明的文章
Auto Layout布局过程涉及延迟机制,并非一有约束更新就马上进行布局重绘,当有约束更改时,系统的默认做法是延迟更新,目的是实现批量更改约束、绘制视图,避免频繁遍历视图层级,优化性能。当更新约束太慢影响到后序代码逻辑,也可强制马上更新。
- 关于Auto Layout的布局流程,Apple给出图示如上:即Layout Cycle是一个在App运行循环RunLoop下循环执行的一个过程。
- App启动后开启RunLoop,循环检测图层树中是否存在约束变化;
- 当发生Constrints Change(直接or间接设置、更新、移除约束),RunLoop检测到约束变化;
- RunLoop发现约束变化后,就会进入Deferred Layout阶段,视图的位置、尺寸值会在这个过程计算,设置到对应视图上,并绘制出来;
- 执行完一轮布局,RunLoop会继续检查视图树的约束更新情况,当再次发现约束更新,则执行新一轮布局。
Constraints Change
Changes to constraint expressions
Activating or deactivating
Setting the constant or priority
Adding or removing views
Engine recomputes the layout
Engine variables receive new values
Views call superview.setNeedsLayout()
Deferred Layout Pass
Reposition misplaced views
Two passes through the view hierarchy
Update constraints
Reassign view frames
Request via setNeedsUpdateConstraints()
Often not needed
Initial constraints in IB
Separate logic is harder to follow
Implement it when
Changing constraints in place is too slow
A view is making redundant changes
Traverse the view hierarchy, top-down
Call layoutSubviews()(or layout() on OSX)
Position the view`s subviews
Copy subview frames from the layout engine
对于重写layoutSubviews,Apple的一些建议
Overriding layoutSubviews
Override when constraints are insufficient
Some views have already been laid out
DO
Invoke super.layoutSubviews()
Invalidate layout within your subtree
Don`t
Call setNeedsUpdateConstraints()
Invalidate layout outside your subtree
Modify constraints indiscriminately
1. updateConstraints
在上面提到的Deferred Layout Pass过程包括两个步骤:Update Constraints和Reassign View Frames。就是在Update Constraints过程中调用的这个方法。自定义view应该重写此方法在其中建立constraints. 注意:要在实现的最后调用[super updateConstraints]。
2. setNeedsUpdateConstraints
当一个自定义view的某个属性发生改变,并且可能影响到constraint时,需要调用此方法去标记constraints需要在未来的某个点更新,系统然后调用updateConstraints.
3. needsUpdateConstraints
当constraint-based layout system使用此返回值去决定是否需要调用updateConstraints作为正常布局过程的一部分。
4. updateConstraintsIfNeeded
立即触发约束更新,自动更新布局。
二. layoutIfNeeded, setNeedsLayout
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
这个函数内部的调用栈大概是这样的:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
1. layoutSubviews
继承于UIView的子类重写,进行布局更新,刷新视图。如果某个视图自身的bounds或者子视图的bounds发生改变,那么这个方法会在当前runloop结束的时候被调用。为什么不是立即调用呢?因为渲染毕竟比较消耗性能,特别是视图层级复杂的时候。这种机制下任何UI控件布局上的变动不会立即生效,而是每次间隔一个周期,所有UI控件在布局上的变动统一生效并且在视图上更新,苹果通过这种高性能的机制保障了视图渲染的流畅性。
从上图中可以看到,runloop
的observer
回调=>CoreAnimation
渲染引擎一次事务的提交=>CoreAnimation
递归查询图层是否有布局上的更新=>CALayer
layoutSublayers
=>UIView
layoutSubviews
这样一个调用的流程。从这里也可以看到UIView
其实就是相当于CALayer
的代理。
顺便看一眼drawRect
方法的调用栈,从CA::Layer::layout_and_display_if_needed
方法之前都是一样的。
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事件。
在苹果的官方文档中强调:
You should override this method only if the autoresizing behaviors of the subviews do not offer the behavior you want.
layoutSubviews, 当我们在某个类的内部调整子视图位置时,需要调用。
反过来的意思就是说:如果你想要在外部设置subviews的位置,就不要重写。
2. setNeedsLayout
setNeedsLayout用来标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,在下一轮runloop结束前刷新,对于这一轮runloop之内的所有布局和UI上的更新只会刷新一次,layoutSubviews一定会被调用。
3. layoutIfNeeded
如果有需要刷新的标记,调用了layoutIfNeeded会立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)。
如果要立即刷新,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局
在视图第一次显示之前,标记总是“需要刷新”的,可以直接调用[view layoutIfNeeded]。
三. setNeedsDisplay
1. drawRect
drawRect在以下情况下会被调用:
1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect调用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值).
2、该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。
以上1,2推荐;而3,4不提倡
drawRect方法使用注意点:
1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect,让系统自动调该方法。
2、若使用CAlayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法
3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕
2. setNeedsDisplay、setNeedsDisplayInRect
setNeedsDisplay和setNeedsDisplayInRect
四. Auto Layout Process 自动布局过程(以上3个之间的关系)
与使用springs and struts(autoresizingMask)比较,Auto layout在view显示之前,多引入了两个步骤:updating constraints 和laying out views。
每一个步骤都依赖于上一个。display依赖layout,而layout依赖updating constraints。显示之前首先得知道布局,想要完整的布局就得更新约束(约束才能得出布局啊)。
updating constraints -> layout -> display
第一步:updating constraints,被称为测量阶段,其从下向上(from subview to super view),为下一步layout准备信息。
可以通过调用方法setNeedUpdateConstraints去触发此步。constraints的改变也会自动的触发此步。但是,当你自定义view的时候,如果一些改变可能会影响到布局的时候,通常需要自己去通知Auto layout,updateConstraintsIfNeeded。
自定义view的话,通常可以重写updateConstraints方法,在其中可以添加view需要的局部的contraints。
第二步:layout,其从上向下(from super view to subview),此步主要应用上一步的信息去设置view的center和bounds。可以通过调用setNeedsLayout去触发此步骤,此方法不会立即应用layout。如果想要系统立即的更新layout,可以调用layoutIfNeeded。另外,自定义view可以重写方法layoutSubViews来在layout的工程中得到更多的定制化效果。
第三步:display,此步时把 view 渲染到屏幕上,它与你是否使用Auto layout无关,其操作是从上向下(from super view to subview),通过调用setNeedsDisplay触发,
因为每一步都依赖前一步,因此一个display可能会触发layout,当有任何layout没有被处理的时候,同理,layout可能会触发updating constraints,当constraint system更新改变的时候。
需要注意的是,这三步不是单向的,constraint-based layout是一个迭代的过程,layout过程中,可能去改变constraints,有一次触发updating constraints,进行一轮layout过程。
注意:如果你每一次调用自定义layoutSubviews都会导致另一个布局传递,那么你将会陷入一个无限循环中。
就是说,layout 和 updateConstraints 不断迭代最终确立了整个布局和显示,然后交给屏幕去显示。
有兴趣可以看看下面的文章
iOS UIView异步绘制
iOS CoreAnimation 渲染流程
iOS 图像显示原理、UI卡顿掉帧、异步绘制、离屏渲染