Kealdish's Studio.

为 Objective-C 下的 protocol 添加默认实现

字数统计: 1.3k阅读时长: 6 min
2018/10/24 Share

最近有业务有需求需要为 UIViewControllerUIViewControllerTransitioningDelegate 协议提供默认的实现,使用 swift 开发默认是支持给协议添加默认实现的,但是,目前主项目中使用的是 Objective-C 做开发,而 Objective-C 下是不支持给协议添加默认实现的。调研一番后,找到了合适的解决方案—— PKProtocolExtension

原理解析

宏定义

PKProtocolExtension.h 中的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
// For a magic reserved keyword color, use @defs(your_protocol_name)
#define defs _pk_extension
// Interface
#define _pk_extension($protocol) _pk_extension_imp($protocol, _pk_get_container_class($protocol))
// Implementation
#define _pk_extension_imp($protocol, $container_class) \
protocol $protocol; \
@interface $container_class : NSObject <$protocol> @end \
@implementation $container_class \
+ (void)load { \
_pk_extension_load(@protocol($protocol), $container_class.class); \
} \
// Get container class name by counter
#define _pk_get_container_class($protocol) _pk_get_container_class_imp($protocol, __COUNTER__)
#define _pk_get_container_class_imp($protocol, $counter) _pk_get_container_class_imp_concat(__PKContainer_, $protocol, $counter)
#define _pk_get_container_class_imp_concat($a, $b, $c) $a ## $b ## _ ## $c
void _pk_extension_load(Protocol *protocol, Class containerClass);

首先是定义了保留关键字 @defs 来封装真正的实现宏。defs_pk_extension($protocol)_pk_extension_imp($protocol, $container_class) 三个宏组合后的作用是创建自定义的容器类,然后实现协议的方法,这些实现即为该协议的默认实现。而容器类的名字则是由 _pk_get_container_class($protocol)_pk_get_container_class_imp($protocol, $counter)_pk_get_container_class_imp_concat($a, $b, $c) 三个宏来决定。这里面的 __COUNTER__ 是累加数字宏,默认是 0 ,每调用一次,就会在之前的结果上加一。 ## 是连接符,将符号两边的参数连接起来,如果有一个参数为空,它会自动把空余的部分吃掉。以上面的代码为例,如果 $b 为空,则实际的字符串为 $a_$c 。

为了更直观地看到宏的作用,我们来写一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Protocol
@protocol Forkable <NSObject>
@optional
- (void)fork;
@required
- (NSString *)github;
@end
// Protocol Extension
@defs(Forkable)
- (void)fork {
NSLog(@"Forkable protocol extension: I'm forking (%@).", self.github);
}
- (NSString *)github {
return @"This is a required method, concrete class must override me.";
}
@end

展开宏定义后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Protocol
@protocol Forkable <NSObject>
@optional
- (void)fork;
@required
- (NSString *)github;
@end
// Protocol Extension
@protocol Forkable; @interface __PKContainer_Forkable_0 : NSObject <Forkable> @end
@implementation __PKContainer_Forkable_0 + (void)load { _pk_extension_load(@protocol(Forkable), __PKContainer_Forkable_0.class); }
- (void)fork {
NSLog(@"Forkable protocol extension: I'm forking (%@).", self.github);
}
- (NSString *)github {
return @"This is a required method, concrete class must override me.";
}
@end

提取协议方法及实现

从宏定义预编译之后的代码可以看出,核心方法是 _pk_extension_load 。该方法的作用是将容器类遵循的协议方法和实现保存到一个全局数组 allExtendedProtocols 中,数组元素的类型定义为一个结构体 PKExtendedProtocol

1
2
3
4
5
6
7
typedef struct {
Protocol *__unsafe_unretained protocol;
Method *instanceMethods;
unsigned instanceMethodCount;
Method *classMethods;
unsigned classMethodCount;
} PKExtendedProtocol;

提取的逻辑处理则是依赖于 _pk_extension_create_merged_pk_extension_merge 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Method *_pk_extension_create_merged(Method *existMethods, unsigned existMethodCount, Method *appendingMethods, unsigned appendingMethodCount) {
if (existMethodCount == 0) {
return appendingMethods;
}
unsigned mergedMethodCount = existMethodCount + appendingMethodCount;
Method *mergedMethods = malloc(mergedMethodCount * sizeof(Method));
memcpy(mergedMethods, existMethods, existMethodCount * sizeof(Method));
memcpy(mergedMethods + existMethodCount, appendingMethods, appendingMethodCount * sizeof(Method));
return mergedMethods;
}
void _pk_extension_merge(PKExtendedProtocol *extendedProtocol, Class containerClass) {
// Instance methods
unsigned appendingInstanceMethodCount = 0;
// 获取宿主类实现的协议方法列表
Method *appendingInstanceMethods = class_copyMethodList(containerClass, &appendingInstanceMethodCount);
Method *mergedInstanceMethods = _pk_extension_create_merged(extendedProtocol->instanceMethods,
extendedProtocol->instanceMethodCount,
appendingInstanceMethods,
appendingInstanceMethodCount);
free(extendedProtocol->instanceMethods);
extendedProtocol->instanceMethods = mergedInstanceMethods;
extendedProtocol->instanceMethodCount += appendingInstanceMethodCount;
// Class methods
unsigned appendingClassMethodCount = 0;
Method *appendingClassMethods = class_copyMethodList(object_getClass(containerClass), &appendingClassMethodCount);
Method *mergedClassMethods = _pk_extension_create_merged(extendedProtocol->classMethods,
extendedProtocol->classMethodCount,
appendingClassMethods,
appendingClassMethodCount);
free(extendedProtocol->classMethods);
extendedProtocol->classMethods = mergedClassMethods;
extendedProtocol->classMethodCount += appendingClassMethodCount;
}

注入协议的方法和实现

成功提取相关协议的方法和实现后,怎么注入并且何时注入到相应的类中呢?我们知道在 main 方法调用之前,所有的类都已经注册到 runtime 中了,那么,在执行 main 方法之前去注入无疑是比较恰当的时机,但是,这个时机又必须得比加载 load 方法晚,否则,还没提取完协议方法,就到了注入环节无疑是不正确的。要完成这个目标,就需要借助于 __attribute__((constructor)) 。被该属性修饰过的方法会在 load 方法之后 main 方法之前被调用。我们在注入方法中获取所有注册类,然后循环遍历找到符合条件的类,再将对应的协议方法和实现注入到该类中即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
__attribute__((constructor)) static void _pk_extension_inject_entry(void) {
pthread_mutex_lock(&protocolsLoadingLock);
unsigned classCount = 0;
Class *allClasses = objc_copyClassList(&classCount);
@autoreleasepool {
for (unsigned protocolIndex = 0; protocolIndex < extendedProtcolCount; ++protocolIndex) {
PKExtendedProtocol extendedProtcol = allExtendedProtocols[protocolIndex];
for (unsigned classIndex = 0; classIndex < classCount; ++classIndex) {
Class class = allClasses[classIndex];
if (!class_conformsToProtocol(class, extendedProtcol.protocol)) {
continue;
}
_pk_extension_inject_class(class, extendedProtcol);
}
}
}
pthread_mutex_unlock(&protocolsLoadingLock);
free(allClasses);
free(allExtendedProtocols);
extendedProtcolCount = 0, extendedProtcolCapacity = 0;
}
static void _pk_extension_inject_class(Class targetClass, PKExtendedProtocol extendedProtocol) {
for (unsigned methodIndex = 0; methodIndex < extendedProtocol.instanceMethodCount; ++methodIndex) {
Method method = extendedProtocol.instanceMethods[methodIndex];
SEL selector = method_getName(method);
if (class_getInstanceMethod(targetClass, selector)) {
continue;
}
IMP imp = method_getImplementation(method);
const char *types = method_getTypeEncoding(method);
class_addMethod(targetClass, selector, imp, types);
}
Class targetMetaClass = object_getClass(targetClass);
for (unsigned methodIndex = 0; methodIndex < extendedProtocol.classMethodCount; ++methodIndex) {
Method method = extendedProtocol.classMethods[methodIndex];
SEL selector = method_getName(method);
if (selector == @selector(load) || selector == @selector(initialize)) {
continue;
}
if (class_getInstanceMethod(targetMetaClass, selector)) {
continue;
}
IMP imp = method_getImplementation(method);
const char *types = method_getTypeEncoding(method);
class_addMethod(targetMetaClass, selector, imp, types);
}
}

总结

protocolExtension 依赖于 runtime ,在 load 方法执行之后向已经在 runtime 中注册类中挑选合适的类进行注入操作。但是,这种做法本身也有一些值得商榷的地方,比如,在注入方法中遍历所有已注册的类的操作,本身就是耗时操作,会影响 App 的启动速度。有想过使用 runtime 的消息转发机制来进行懒加载,但是,这么做的问题在于如果某个实现子类重写了消息转发机制的相关方法,并且没有调用父类的相关方法,则该类的注入操作便会失败。

CATALOG
  1. 1. 原理解析
    1. 1.1. 宏定义
    2. 1.2. 提取协议方法及实现
    3. 1.3. 注入协议的方法和实现
  2. 2. 总结