最近碰到一个需求,类似于下图所示,几个 UIButton
排列在一起,在某个事件触发时,隐藏或者显示中间的 button
,重点是隐藏中间的 button
时,上下 button
之间的间距保持为原来的间距。比较好的做法是重写 alignmentRectInsets
的 get 方法,当然更新约束的方法也是 OK 的。直接创建子类去重写 alignmentRectInsets
的 get 方法不够优雅,因而想到创建分类, swizzle UIView
的 alignmentRectInsets
的 get 方法,在自己的自定义方法中去完成真正的修改逻辑。
原以为写完分类,在项目中调用再 run 一下就大功告成了,然而,我还是 naive 啊。运行项目后,发现竟然没有 work ,打断点调试,发现 swizzle method 没有调用。没有调用的原因在于 UIButton
类本身的实现已经重写了 alignmentRectInsets
的 get 方法,所以,不会再调用 UIView
的相同方法了。那怎么办呢?最简单的办法就是给 UIButton
创建分类而不是 UIView
,但是感觉这样写不是很好,有些冗余。
经过一番研究后,想到可以利用 KVO 的机制来解决这个问题。
思路
关于 KVO 的具体细节不是本文的重点,如果不是很了解,可以参考这篇文章。这里提一点,苹果在 KVO 的底层实现上,会创建一个子类对象,类名类似于 NSKVONotifying_xxxxx
,并将原来对象的 isa 指针指向这个子类对象。有了这个特性,就可以解决我们之前的问题啦!
那解决的思路是怎么样的呢?主要还是围绕 KVO 会动态创建子类的特性。具体思路可以分为以下几步:
- 让苹果为我们的目标类的对象动态创建子类对象
- 获取目标对象真正的类
- 拿到需要 swizzle 的
alignmentRectInsets
方法的函数签名 - 在 KVO 子类添加该方法和对应的 IMP
第一步很简单,只要一个对象观察某个键值,苹果就会为这个对象动态创建子类对象。但这里有个地方需要注意,当某个对象观察某个键值时,需要在该对象 dealloc
方法中移除对该键值的观察,否则,会导致 KVO still registering when deallocated
的 crash 。第二步依赖于第一步中的动态创建的子类对象,调用 class_getSuperclass()
方法来获取目标对象真正的类。第三步承接第二步,知道了目标对象的真正类,则可以拿到 swizzle 的 alignmentRectInsets
的 get 方法的函数签名,这没什么问题。第四步将完整的 selector
、函数签名以及自己实现的 IMP 添加到 KVO 子类的方法列表中即可。
如何解决 KVO 属性移除
刚才有提到,KVO 观察的键值,需要在对象 dealloc
时移除,但是,在 category 中是无法重写 dealloc
方法的。那怎么解决这个问题呢?我们知道,一个对象 A 持有另一个对象 B ,且 B 仅被 A 持有,那么当对象 A 释放时,也会释放对 B 的持有,也就表示在 A 的 dealloc
触发时,B 的 dealloc
方法也会触发。按照这个思想,我们可以创建一个单例对象作为 observer
,再实例化一个对象 remover
去持有该单例对象,而让目标对象去持有该实例化的对象 remover
,在 remover
的 dealloc
方法中执行移除 KVO 键值的操作。当目标对象销毁时,就会释放 remover
,也就会触发 remover
的 dealloc
方法。对这两个类的定义如下:
|
|
|
|
注意,ICFakeKVORemover
中的 target 的修饰符要用 unsafe_unretained
而不能用 weak
,否则,在 dealloc
方法执行时, _target 已经置为 nil 而无法移除键值观察。至于其中的详细细节,可以参考我的另一篇文章Weak 属性在 dealloc 背后的逻辑。这样当 ICFakeKVOObserver
单例对象销毁时,ICFakeKVORemover
对象的 dealloc
方法也会触发。
在 KVO 子类添加 alignmentRectInsets
方法和对应的 IMP
如何在子类中添加目标方法以及相应的 IMP 已经在上面提过,核心代码如下:
|
|
这里面有个需要注意的点,那就是如何创建符合要求的 IMP
?我们知道 OC 语言的核心在于消息传递,我们调用的方法本质上是会转变成消息发送给目标对象,依据消息中的 selector
找到对应的 IMP
,然后调用它。我们所写的方法实现在经过 clang 编译之后会变成 C 函数,而函数参数也会发生变化。比如 - (void)viewWillAppear:(BOOL)animated
编译后会变成 static void viewWillAppear(UIViewController *self, SEL sel, bool animated)
,其中函数名不变,返回值则会在 OC 和 C 之间做转换,而参数列表则多了两个参数 self
和 sel
分别代表了当前方法的调用者和对应的方法名。编译后的函数实现可以理解为就是真正的 IMP
。那么我们要创建自己的 IMP
,则只需要按照要求在函数参数中加上 self
和 sel
即可。这里的 ic_alignmentRectInsets
IMP
代码如下:
|
|
写完 category 再调用一下,大功告成!没想到由于 UIButton
的 alignmentRectInsets
在 category 中的失效问题竟然学习到了这么多知识,酥服。