『libextobjc』Objctive-C 协议的默认实现
继续阅读 libextobjc 的源码,看到一个非常有趣的实现—— Objective-C 的 protocol 默认实现。当然,这不比 Swift 的 extension 默认实现,Objective-C 在这方面没有 Swift 强大,并不能完全的实现 POP,但是这不妨给我们提供一种思路。
首先,列举一下当面对这个问题时,都有哪些疑问:
- 会用到方法注入,但是什么时候注入?
- 以什么形式获取默认实现的 SEL 与 IMP?
- 怎样减少性能开销?
然后,我们来一起看看源码的实现思路(因为更注重实现步骤和思想,所以在文章中不会出现大量的源码,大家最好自行对照源码进行阅读)。
用法
// MyProtocol.h |
在 EXTConcreteProtocol.h
中能找到 concrete
与 concreteprotocol
这对宏。concrete
很简单,也很巧妙,它就是用于修饰 protocol 方法的 optional
。用 concrete
宏的好处有两个:
- 沿用
optional
的作用,防止遵循该 protocol 的类因为没实现代理方法,而报警告。 - 语义更加清晰,能直接表情这以下的方法是已经默认实现了的。
然后来看看 concreteprotocol
的定义,首先,concreteprotocol
会将传入的 protocol name 与字符串 _ProtocolMethodContainer
拼接,即 MyProtocol_ProtocolMethodContainer
。因为源码不易阅读,我将它简化了一下(去掉注释与报错信息):
被框住的代码就是 concreteprotocol(MyProtocol)
展开的部分。这段代码能解答之前我们的两个疑问:
- 什么时候注入:无论是在
+load
还是在被__attribute__((constructor))
修饰的函数中,至少能保证注入是发生在main
函数之前的(关于+load
与__attribute__((constructor))
的执行顺序,请参考我之前的文章:attribute)。 - 以怎样的形式获取 SEL 与 IMP:这个宏直接为 protocol 扩展了一个容器类,所以默认实现的方法都是存在这个类中的,之后要进行注入,方法的 SEL 与 IMP 也应该是从这个容器类中进行获取。
所以,根据调用顺序,我们接下来分成两步来分析整个实现。
ext_addConcreteProtocol
首先被调用的是 +load
中的 ext_addConcreteProtocol
函数。这个函数接收两个参数:当前的 protocol 对象,以及对应的容器类。实现中又调用了另外一个:
BOOL ext_addConcreteProtocol (Protocol *protocol, Class containerClass) { |
ext_loadSpecialProtocol
函数接收两个参数:当前 protocol,以及一个参数为 Class destinationClass
的 block。我们先不看这个 block 的具体实现,先来看看 ext_loadSpecialProtocol
都做了些什么。
ext_loadSpecialProtocol
同样,我简化了 ext_loadSpecialProtocol
的实现,代码大多用注释描述来代替:
// protocol: 当前默认实现的 protocol |
所以,整个函数走下来,作用就是将 {protocol, block}
追加到数组中。
也就是说,在执行 __attribute__((constructor))
修饰的方法以前,所有默认实现的 protocol,都会被加到这个数组中。
接下来,我们来看 +load
之后做了什么。
ext_loadConcreteProtocol
同样,ext_loadConcreteProtocol
内部也调用了另一个函数:
void ext_loadConcreteProtocol (Protocol *protocol) { |
整个函数的实现很简单,用于确保 +load
中加入数组的所有 protocol 都能找到:
void ext_specialProtocolReadyForInjection (Protocol *protocol) { |
在 ext_specialProtocolReadyForInjection
的实现中,if (++specialProtocolsReady == specialProtocolCount)
这个判断比较有趣,它能回答我们的第三个问题(如何节省开销):
+load
与__attribute__((constructor))
的优先级能使得所有 protocol 加入完成以后,再进行处理。- ready 计数
specialProtocolsReady
使得所有默认实现均判断无误后,再进行注入。
到此,好像已经完事具备,马上就可以进行注入了。但苍天饶过谁,我们还有很重要的一个问题没有考虑。
ext_injectSpecialProtocols
优先级问题:如果 protocolA <ProtocolB>
,也就是 protocolA
遵循 protocolB
,那么谁的优先级更高呢?除此之外,如果遵循 protocol 的 class,自己也实现了默认方法呢?
这个问题,在 ext_injectSpecialProtocols
函数中能得到答案:
static void ext_injectSpecialProtocols (void) { |
所以,整个方法的任务也很清晰:
- 对 protocol 进行优先级排序,给出具体注入的先后顺序,防止方法覆盖或无法注入。
- 获取全部 class 列表。
- 两层循环遍历,将 class 与其遵循的 protocol 进行匹配。
- 调用 struct 中的 block,并将目标 class 传出,进行注入。
接下来,终于到了最后一步,来看看 block 中的注入方法的实现。
ext_injectConcreteProtocol
block 是在 +load
中就已经赋值了,而 block 的实现,就是直接调用了 ext_injectConcreteProtocol
函数:
// 函数有三个参数 |
到此,方法注入就已经全部完成了。
总结
面向过程的过完了整个源码,从头再来梳理一下:
- 注入实现思路的重点在于,使用宏为 protocol 扩展了一个容器类。
- 容器类中,利用
+load
与__attribute__((constructor))
的特性,将注入流程分为了两个部分。 - 在
+load
中,将 protocol,执行注入的 block 打包成 struct,然后将 struct 装进数组。 - 当执行到
__attribute__((constructor))
时,也就表示所有类的+load
都已经执行过了,再对数组进行优先级排序。 - 排序完成后,两层循环嵌套,查找遵循了 protocol 的 class。
- 调用 block 执行注入。