EXTConcreteProtocol浅析

在Swift中,一个Protocol能够通过extension为特定的类型提供method, initializer, subscript, computed property的默认实现。这着实是一个很好的特性。那么在Objective-C中是否能够实现类似的效果?答案是肯定的。有若干个库都提供了自己的实现,因为大同小异,这边稍微讲一下libextobj库中的EXTConcreteProtocol的实现。

一个简单的例子

example.h

@protocol MyProtocol
@required
- (void)someRequiredMethod;

@optional
- (void)someOptionalMethod;

@concrete
- (BOOL)isConcrete;

@end

example.m

@concreteprotocol(MyProtocol)

- (BOOL)isConcrete {
    return YES;
}

// this will not actually get added to conforming classes, since they are
// required to have their own implementation
- (void)someRequiredMethod {}

@end

偷懒摘取了源码注释上的内容,代码看上去比较简洁,我们可以看看简单的几行代码都做了什么。

两个宏

concrete

这个宏的定义很简单:

#define concrete optional

可以看到使用@concrete时,其实展开后是@optional。也就是说我们定义了的值被认为是可选的,这样一来某个class遵从此协议的时候,就不用提供自己的实现了(有了optional编译器不会再提示)。如果不实现,则使用提供的默认的实现。

concreteprotocol(NAME)

这个宏展开后相对复杂,可以拆解成以下几个部分:

  1. 定义一个{NAME}_ProtocolMethodContainer的interface,即为一个container class;
  2. 提供{NAME}_ProtocolMethodContainer的implementation,实现+load方法,调用ext_addConcreteProtocol
  3. 定义一个名为ext_{NAME}_inject的constructor,在类初始化后调用ext_loadConcreteProtocol
  4. 此container class包含了对应Protocol所需要实现的方法。

其中+load首先调用,constructor在Objective-C Runtime设置完成后调用。

关于constructor的介绍,可见《__attribute__指令》

若干个函数

ext_addConcreteProtocol

BOOL ext_addConcreteProtocol (Protocol *protocol, Class containerClass)

这个函数在+load中先执行到。内部调用了ext_loadSpecialProtocol,其回调中执行ext_injectConcreteProtocol

ext_loadSpecialProtocol

BOOL ext_loadSpecialProtocol (Protocol *protocol, void (^injectionBehavior)(Class destinationClass))

这个函数在其他地方定义。虽然代码比较长,但完成的功能比较简单,主要是将protocol和传入的回调(injectionBlock)记录起来,并设置ready为NO,留待后续使用。

此步骤主要为收集相关Protocol信息。

ext_loadConcreteProtocol

void ext_loadConcreteProtocol (Protocol *protocol)

此函数在constructor中自动被执行,其直接调用了ext_specialProtocolReadyForInjection函数,而此函数遍历了ext_loadSpecialProtocol中所存储的数据,置ready = YES,标记已经准备就绪,并在多次调用后会处理完列表中所有ready = NO状态的数据,最后会调用ext_injectSpecialProtocols

此步骤主要确认相关类已经加载成功,前序工作完成。

ext_injectSpecialProtocols

static void ext_injectSpecialProtocols (void)

这个函数首先通过Protocol的依赖关系进行了排序。接下来获取了所有的类。遍历所有的Protocol和类。如果类遵从此Protocol,则通过之前保存的injectionBlock注入(调用的ext_injectConcreteProtocol)。

ext_injectConcreteProtocol

static void ext_injectConcreteProtocol (Protocol *protocol, Class containerClass, Class class)`

可以看到参数为protocol, containerClass, class。从名字可以知道protocol为其所遵从的协议,containerClass为上面concreteProtocol宏所定义的类,class为需要注入的类。

整个过程也中规中矩:

  1. 获取containerClass的所有实例方法和类方法;
  2. 获取class的meta class;
  3. 分别遍历1中获取的实例方法和类方法,如果class不存在对应方法,则动态地将containerClass的对应实现添加到class中。

通过以上函数,就完成了Protocol的默认实现。

简单概括

  1. @optional定义可选(optional)的property/method等;
  2. concreteProtocol定义了containerClass,实现了对应Protocol需要实现的方法;
  3. +load动态地收集此类Protocol信息;
  4. constructor在Runtime设置完成后,自动执行,并在最后一次调用注入方法;
  5. 注入方法时,通过遍历对应Protocol和类的关系,将containerClass的实现一一添加到相应的类里。

其他

这是一个精彩的利用Objective-C Runtime的例子,可见Runtime的强大。但利用不当,也很可能造成一些难以排查的问题。功能虽好,用时需谨慎。