0x01 Abstract Class
Java、C++ 等 OOP 语言有一个抽象类的概念,即一个类实现了部分方法,另一部分的方法必须由继承它的子类来实现。Objective-C 在设计上没有这个概念,转而提供了用途类似的 协议,除了不能给方法加默认实现以外,与抽象类的用法大体相同。但是在实际项目中,让一个协议实现一些共通的方法还是很有必要的,比如很多类都遵守了某一个协议,而这个协议中某一个方法的实现大体上都一样的时候,在每一个子类内部都 copy 一份同样的代码就不太合适了。
一种规避 copy 的做法是把它的实现抽离到全局方法中,比如下面的协议:
1 | @protocol MyProtocol <NSObject> |
如果所有子类的 method2
的实现都差不多,就可以将它抽到一个全局方法(或者一个单例类的方法)中:
1 | void MyProtocolMethod2(id<MyProtocol> instance) { |
另一种办法是抛弃 @protocol
,直接使用 @interface
,然后使用文档说明的方式约定它是一个抽象类
1 | // MyBaseClass.h |
以上两个方法都可以达成目的,但都有一些缺陷:前一种方法把 MyProtocol 相关的代码放到了全局环境中,不优雅;后一种方法在编译阶段没有提示,需要由开发人员仔细阅读文档才能避免误用。StackOverflow 的一篇答案还提供了另一个方案:在每一个子类的 +initialize
方法中通过 class_addMethod
把协议的默认实现加到方法列表当中,但这样也略显繁琐。
0x02 EXTConcreteProtocol
一个第三方库 libextobjc 通过 EXTConcreteProtocol
神奇地实现了这个功能,使用方法与原生协议类似:
1 | // MyProtocol.h |
这样声明以后,对于任何遵守 MyProtocol 协议的类,如果没有重写 method2 方法,都会有一个在 MyProtocol.m 中声明的默认实现。
这个库为什么这么吊,@concrete
和 @concreteprotocol
到底做了什么。其实 concrete
只是 optional
的别名,为了提示调用者就算不重写这个方法也一定会有的,重点还是在 concreteprotocol
宏上。
查看 EXTConcreteProtocol
源码可以知道,@concreteprotocol(MyProtocol)
这一行通过宏定义的方式生成了这样的一个包装类:
1 | @interface MyProtocol_ProtocolMethodContainer : NSObject <MyProtocol> |
其中 ext_addConcreteProtocol
在 load
方法中被调用,它的作用是把将要对 MyProtocol 进行的注入操作缓存到一个全局列表中,除此之外还有一些边界条件的判断和加锁什么的。
__attribute__((constructor))
是 GCC 的一个编译器指令(其实是 Clang 的指令,但我翻遍了 Clang 的官方文档并没有找到关于 constructor 的描述- -),被它标记的函数会在整个 Objective-C runtime 初始化完毕之后,在 main()
函数之前被调用。这时 ext_loadConcreteProtocol
函数会遍历 runtime 中所有的 Class,对其中每一个遵从 MyProtocol 协议的 Class 进行缓存过的注入操作:
1 |
|
虽然调用层级很复杂,但最终还是调用了 class_addMethod 方法给 Class 自动加上了默认的实现,原理跟上面的 StackOverflow 给的答案是一样的。