最近在实习工作中遇到了许多需要通过增加约束进行布局的场景,之前我采用Swift编程时这种情况往往选择直接采用storyboard中“拖拽”式地add constraint 。但目前公司项目iOS的界面开发都需要用OC的纯代码实现,好在第三方库Masonry足够强大好用,开发中很多布局需求都能通过其解决。但好死不死,前段时间我在开发一个按钮组件时重写了其layoutSubviews方法并往其中添加约束,运行发现约束不生效并引出了之后一系列autoLayout、view更新绘制的问题。
本次探究校验过程基于我自己写的一个播放/暂停按钮NKPlayerSwitch,其继承自第三方库lottie-ios中的LOTAnimatedSwitch,其中又包含属性animationView(用于播放动画内容)。通过简单地往一个空页面上添加一个NKPlayerSwitch,并动态地修改相关属性,以及重写相关view的layoutSubviews方法,观察显示结果最终分析得出结论。
知识回顾 & 参考资料
UIViewController的生命周期,以下方法均可以在ViewController中重写:layoutSubviews()调用时机:
- init初始化不会触发
layoutSubviews。但是是用initWithFrame进行初始化时,当rect的值不为CGRectZero时,也会触发 addSubview会触发layoutSubviews- 设置UIView的Frame会触发
layoutSubviews,更严谨地说,frame.size值发生变化才会触发layoutSubviews - 改变一个UIView.frame.size的时候也会触发父UIView上的
layoutSubviews事件 - view1的位置移动,若根据约束view2的位置依赖于view1,此时会触发两者最近公共父视图superView的
layoutSubviews方法使约束生效,因为本质上这一约束对应的NSLayoutConstraint是被add在superView上 - 滚动一个
UIScrollView会触发layoutSubviews - 旋转Screen会触发父UIView上的
layoutSubviews事件
实验过程
本次实验所有内容都来源于一个困惑🤔————“constraints究竟在哪个阶段生效?”
最开始,当我在自定义组件NKPlayerSwitch的layoutSubviews()中增加约束,运行后却发现其并没有生效:
updateConstraints,layoutSubviews,drawRect,以保证在休眠前使本次runLoop中的相关布局修改生效,其流程图大致如下:
layoutSubviews负责计算、设置各个控件的位置大小并对其进行布局,联想到UIView的方法addConstraint,约束是和UIView绑定的属性,因此人们认为 “constraints在UIView的layoutSubview()中生效”。阅读到这里,我觉得是我太笨逼,将增加约束部分写在了[super layoutSubviews]后面,此时基类UIView的layoutSubviews方法已经执行完毕。可我仔细阅读打印日志发现,我的playSwitch的layoutSubviews()执行了两次,且在第一次执行完毕之后其animationView的size就已经变成(44,44)我设定的约束size(24,24)自始至终没有生效。
这里为什么会执行两次layoutSubviews()也是我百思不得解的地方???
layoutSubviews()中添加的约束,在第二次执行时不就应该被UIView给“安排上了”吗?回想自己小时候写作业,经常对着正确答案编过程,如今看来Coding的时候也需要我们对着运行结果反推实现流程。
查阅了多个地方,终于在LOTAnimatedSwitch的父类LOTAnimatedControl的layoutSubviews中看到了这么一段代码:
- (void)layoutSubviews {
[super layoutSubviews];
_animationView.frame = self.bounds;
}
可以,我又能说服自己了:LOTAnimatedControl本质也继承自UIView,因此无论是第几次布局,在不断调用父类的layoutSubviews()时首先调用UIView的方法,constraints在此时生效,但在后面LOTAnimatedControl的方法中又将animationView的frame修改为self.bounds,相当于约束被抵消。
到这里似乎问题已经解决了,网上大家的说法果然是正确的。但好奇的我又做了新的尝试,我将自己NKPlayerSwitch的layoutSubviews()中[super layoutSubviews]这一行注释掉,仅仅在其中添加约束,重新运行结果会是怎样呢?
layoutSubviews阶段其frame'size都为(0, 0),但到了最后viewDidAppear时又变成了(24, 24)。可见我设置的约束在layoutSubviews之后,视图最终显示之前的某个阶段又生效了。
因此为了查明原因,我首先选择给runLoop添加监听器,甚至自定义observer的order希望能够精准定位到约束生效的时机。中间过程较为繁琐在此省略,最终的结果显示,constraints生效时间在runLoop的整个beforeWaiting“即将休眠”之前。
在这之后,我自己在NKPlayerSwitch中另外定义了一个子视图TestView继承自UIView,重写它的setFrame、setCenter、setBounds方法,和其CALayer的setFrame、setPositon、setBounds方便设置断点进行监听。并又在layoutSubviews中对其添加约束将其宽高设置为24。阅读CALayer相关文档,找到了其中layoutSublayersOfLayer:(CALayer *)layer这一方法。设置断点调试发现,layoutSubviews就是在这个方法中被调用,而且在此之后也会有一个时间点令约束生效,调用testView的setBounds、setCenter等方法。
由于本人知识有限,且
layoutSublayersOfLayer:方法并未开源,故具体生效时机和逻辑可能并非如我所想,还需要进一步考究。
layoutSubviews中明明就已经添加了约束,可在这之后layoutSublayersOfLayer结束时视图的size并没有更新,即约束并没有立即生效。
update Constraints方法——该方法负责将当前UIView的约束“刷新”,只有被刷新后的约束才会在之后的某个生效时间点进行布局。
P.S: 我觉得既然大家都认为约束生效于
layoutSubviews阶段,或许有他的道理。因此我在layoutSubviews的不同阶段打印_testView的信息,果不其然,在第二次layoutSubviews中[super layoutSubviews]前后size不一致,可见UIView的layoutSubviews阶段确实同样存在constraints生效的过程。
实验结论
- Masonry“添加”的约束
NSLayoutConstraint最终只是通过调用UIView的addConstraints:被保存在了UIView的constraints属性中“持有”,必须经过updateConstraints“激活”,并在之后某个布局过程才会实际“生效” - constraints的生效时机目前来看有两处,整体都位于
layoutSublayersOfLayer( )中,runLoop“即将休眠”之前——一是在UIView的layoutSubviews( )阶段,二是在[self layoutSubviews]之后。
- 约束生效时,本质是调用改变bounds和center,并未直接调用
setFrame方法。 - 使用时,约束建议添加在UIViewController的
viewDidLoad或viewWillLayoutSubviews方法中,UIView的initWithFrame、与约束相关属性的set方法中,最好不要在layoutSubviews中添加约束。 - 对视图布局方式应该尽可能统一,不要frame和constraints混用。
疑问 ???
-
初始化时,作为主视图的子视图之一,playSwitch的
layoutSubviews方法在runLoop休眠前总是被调用两次。 -
在
NKPlayerSwitch的layoutSubviews方法中使用Masonry对playSwitch中的animationView添加约束,指定其大小size。运行发现同样在初始化时,主视图的viewDidlayoutSubviews被调用两次,说明其主视图view此时两次layoutSubviews。