Swift支持协议方法的默认实现,而Objective-C不支持,突然想到多年前sunnyxx
开源的 ProtocolKit ,顺手翻翻源码。
- @defs
- __COUNTER__
- 中间类的生成与实现
- 数据结构
- 加载所需数据
- 将实现方法注入到目标类
- 性能问题
- libextobjc
- @defs
从使用来看ProtocolKit的实现似乎全靠@defs
,据唐巧
的这篇文章所言,@defs
可以用来返回一个与当前Objective-C具有相同内存布局的struct。遗憾的是,当前架构与平台不支持了,这应该是__OBJC2__
之前的产物。
虽然不支持,但是可以看到,@defs
在代码中仍被标识为关键字颜色,而ProtocolKit重定义了defs:
// 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
替换成_pk_extension
- 在
Interface
部分_pk_extension
中传入protocol
又通过一系列的宏定义来完成中间类的声明与实现 -
Implementation
部分在中间类调用load
时,通过_pk_extension_load
函数生成所需数据 - 通过
Get container class name by counter
部分的宏定义获取中间类 -
_pk_extension_load
生成所需数据
先来看看被中间类是如何获取的,在这里出现了一个宏__COUNTER__
。
- __COUNTER__
__COUNTER__
是个宏,每次编译会自动加一,起始值为0
#define a printf("%d\n", __COUNTER__)
#define b printf("%d\n", __COUNTER__)
int main(int argc, const char * argv[]) {
@autoreleasepool {
a; b;
printf("%d\n", __COUNTER__);
printf("%d\n", __COUNTER__);
}
return 0;
}
__COUNTER__
再举个例子:
#define a printf("%d\n", __COUNTER__)
#define b printf("%d\n", __COUNTER__)
@interface A : NSObject
@end
@implementation A
+ (void)counter0 {
printf("%d\n", __COUNTER__);
}
+ (void)counter1 {
printf("%d\n", __COUNTER__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
a; b;
printf("%d\n", __COUNTER__);
printf("%d\n", __COUNTER__);
}
return 0;
}
__COUNTER__
注意:__COUNTER__的数值与调用次数无关,仅与编译相关。这里不管A是否调用counter0与counter1,不管调用了一次还是多次,在其内部的数值都是固定的0和1。
- 中间类的生成与实现
#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
以文章开头的Forkable
协议为例,这里最终调用_pk_get_container_class_imp_concat
时,对应的参数分别为__PKContainer_
、Forkable
、0
,宏替换后得到类名__PKContainer_Forkable_0
。
调用@defs(Forkable)
时,变为_pk_extension_imp (Forkable, __PKContainer_Forkable_0)
,宏展开后的代码为:
@protocol Forkable;
@interface __PKContainer_Forkable_0 : NSObject <Forkable> @end
@implementation __PKContainer_Forkable_0
+ (void)load {
_pk_extension_load(@protocol(Forkable), __PKContainer_Forkable_0.class);
}
显然展开后的代码缺少@end
,回头看看Demo用法:
@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
@end在这里补齐了,由于protocol前没加@,所以使用defs时需要加@,由于宏展开后缺少@end,所以@defs必须与@end配对出现。
- 数据结构
typedef struct {
Protocol *__unsafe_unretained protocol; // 协议
Method *instanceMethods; // 实例方法实现列表
unsigned instanceMethodCount; // 实现的实例方法个数
Method *classMethods; // 类方法实现列表
unsigned classMethodCount; // 实现的类方法个数
} PKExtendedProtocol;
static PKExtendedProtocol *allExtendedProtocols = NULL;
static pthread_mutex_t protocolsLoadingLock = PTHREAD_MUTEX_INITIALIZER;
static size_t extendedProtcolCount = 0, extendedProtcolCapacity = 0;
通过allExtendedProtocols来存储协议与对应的默认实现,通过protocolsLoadingLock确保线程安全,通过extendedProtcolCount与extendedProtcolCapacity对allExtendedProtocols扩容。
扩容代码如下:
if (extendedProtcolCount >= extendedProtcolCapacity) {
size_t newCapacity = 0;
if (extendedProtcolCapacity == 0) {
newCapacity = 1;
} else {
newCapacity = extendedProtcolCapacity << 1;
}
allExtendedProtocols = realloc(allExtendedProtocols, sizeof(*allExtendedProtocols) * newCapacity);
extendedProtcolCapacity = newCapacity;
}
逻辑很简单,首次使用容量为1,以后每当容量不足时扩容2倍。
- 加载所需数据
在中间类的生成宏展开中,可以看到load
时调用了_pk_extension_load
函数:
void _pk_extension_load(Protocol *protocol, Class containerClass) {
// 加锁
pthread_mutex_lock(&protocolsLoadingLock);
// 扩容
if (extendedProtcolCount >= extendedProtcolCapacity) {
size_t newCapacity = 0;
if (extendedProtcolCapacity == 0) {
newCapacity = 1;
} else {
newCapacity = extendedProtcolCapacity << 1;
}
allExtendedProtocols = realloc(allExtendedProtocols, sizeof(*allExtendedProtocols) * newCapacity);
extendedProtcolCapacity = newCapacity;
}
// 寻找传入protocol所在allExtendedProtocols中的位置
size_t resultIndex = SIZE_T_MAX;
for (size_t index = 0; index < extendedProtcolCount; ++index) {
if (allExtendedProtocols[index].protocol == protocol) {
resultIndex = index;
break;
}
}
// 如果没找到,新建struct并存入allExtendedProtocols中
if (resultIndex == SIZE_T_MAX) {
allExtendedProtocols[extendedProtcolCount] = (PKExtendedProtocol){
.protocol = protocol,
.instanceMethods = NULL,
.instanceMethodCount = 0,
.classMethods = NULL,
.classMethodCount = 0,
};
resultIndex = extendedProtcolCount;
extendedProtcolCount++;
}
// 合并protocol方法实现
_pk_extension_merge(&(allExtendedProtocols[resultIndex]), containerClass);
// 解锁
pthread_mutex_unlock(&protocolsLoadingLock);
}
- 通过protocolsLoadingLock确保allExtendedProtocols线程安全
- 对allExtendedProtocols扩容
- 寻找传入protocol在allExtendedProtocols中的位置
- 如果没找到,新建struct并存入allExtendedProtocols中
- 合并protocol方法实现
_pk_extension_load中调用_pk_extension_merge对protocol方法进行合并,而真正的合并逻辑在_pk_extension_create_merged中:
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;
}
existMethods
与existMethodCount
对应着struct中的Methods与count,appendingMethods
与appendingMethodCount
对应着中间类中的Methods与count(在_pk_extension_load函数中已区分了实例方法与类方法)。
然而,到目前为止只是完成了将中间类实现的protocol方法存储到struct中,并没有将实现方法注入到目标类。
- 将实现方法注入到目标类
由__attribute__((constructor))
修饰的函数会在load
函数之后,main
函数之前调用。
__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;
}
获取class列表后判断每个class是否遵守struct中存储的protocol协议(这里有两层for循环,要命),如果遵守协议,调用_pk_extension_inject_class
函数,继续跟进:
static void _pk_extension_inject_class(Class targetClass, PKExtendedProtocol extendedProtocol) {
// 动态注入struct中存储的实例方法
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);
}
// 动态注入struct中存储的类方法
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);
}
}
逻辑也很简答,通过runtime动态对目标类注入struct中存储的实例方法和类方法(实例方法存储在类中,类方法存储在元类中)。
- 性能问题
前面说了,在main
函数之前已经调用了两层for循环,而如果找到某个class遵守协议,则调用_pk_extension_inject_class
函数,函数内部继续通过for循环判断目标类是否已经实现某个协议方法。这还不是最致命的,最致命的是class_getInstanceMethod
这个函数会触发initialize
。
也就是说,通过@defs
实现的协议拓展,但凡某个类遵守这个协议,都会在main
函数之前调用initialize
。此时class的生命周期已经和原始逻辑不同了,依赖于initalize
完成的工作可能无法正常进行。
在Pull requests中也发现了这样一条信息
性能问题这里的问题描述不是很精准,并非所有的class都会在premain阶段执行initialized,而是遵守了通过@defs拓展协议的class会在premain阶段执行initialized。
作者移除了__attribute__((constructor))
对_pk_extension_inject_entry
函数的修饰,并新增:
__attribute__((constructor)) static void _pk_extension_inject_entry(void) {
_pk_swizzleMethod(object_getClass([NSObject class]), @selector(resolveInstanceMethod:), @selector(_pk_resolveInstanceMethod:));
_pk_swizzleMethod(object_getClass([NSObject class]), @selector(resolveClassMethod:), @selector(_pk_resolveClassMethod:));
}
@implementation NSObject (PKExtendedProtocol)
+ (BOOL)_pk_resolveInstanceMethod:(SEL)sel {
_pk_extension_try_inject_entry_class(self);
return [self _pk_resolveInstanceMethod:sel];
}
+ (BOOL)_pk_resolveClassMethod:(SEL)sel {
_pk_extension_try_inject_entry_class(self);
return [self _pk_resolveClassMethod:sel];
}
@end
static void _pk_extension_try_inject_entry_class(Class class) {
// 防止递归死锁,因为 class_getInstanceMethod(), 会触发 resolveInstanceMethod: 等方法的调用,就会导致递归调用,引起死锁
// 这边没必要用 递归锁,内部 for 循环,才引起的递归。 代码不用重复执行
NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary;
if ([threadDictionary objectForKey:@"_pk_injecting"]) {
return;
}
pthread_mutex_lock(&protocolsLoadingLock);
[threadDictionary setObject:@1 forKey:@"_pk_injecting"];
_pk_extension_inject_entry_class(class);
[threadDictionary removeObjectForKey:@"_pk_injecting"];
pthread_mutex_unlock(&protocolsLoadingLock);
}
在premain阶段hookresolveInstanceMethod
与resolveClassMethod
,调用某个类的协议方法时,因找不到具体的方法实现会走runtime消息转发流程,此时在转发过程中注入方法实现,从而延迟目标类的initialized调用到正常逻辑。显然,it works!
- libextobjc
在ProtocolKit的Issues中发现了libextobjc
大致看了下EXTConcreteProtocol.m
中的实现,注入逻辑没什么变化,但是ProtocolKit
使用起来确实更便捷。
refs:
网友评论