ProtocolKit 解读

作者: 01_Jack | 来源:发表于2019-11-28 18:12 被阅读0次

    Swift支持协议方法的默认实现,而Objective-C不支持,突然想到多年前sunnyxx开源的 ProtocolKit ,顺手翻翻源码。

    • @defs
    • __COUNTER__
    • 中间类的生成与实现
    • 数据结构
    • 加载所需数据
    • 将实现方法注入到目标类
    • 性能问题
    • libextobjc

    • @defs
    Protocol Extension

    从使用来看ProtocolKit的实现似乎全靠@defs,据唐巧这篇文章所言,@defs可以用来返回一个与当前Objective-C具有相同内存布局的struct。遗憾的是,当前架构与平台不支持了,这应该是__OBJC2__之前的产物。

    @defs

    虽然不支持,但是可以看到,@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);
    
    1. defs替换成_pk_extension
    2. Interface部分_pk_extension中传入protocol又通过一系列的宏定义来完成中间类的声明与实现
    3. Implementation部分在中间类调用load时,通过_pk_extension_load函数生成所需数据
    4. 通过Get container class name by counter部分的宏定义获取中间类
    5. _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_Forkable0,宏替换后得到类名__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);
    }
    
    1. 通过protocolsLoadingLock确保allExtendedProtocols线程安全
    2. 对allExtendedProtocols扩容
    3. 寻找传入protocol在allExtendedProtocols中的位置
    4. 如果没找到,新建struct并存入allExtendedProtocols中
    5. 合并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;
    }
    

    existMethodsexistMethodCount对应着struct中的Methods与count,appendingMethodsappendingMethodCount对应着中间类中的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阶段hookresolveInstanceMethodresolveClassMethod,调用某个类的协议方法时,因找不到具体的方法实现会走runtime消息转发流程,此时在转发过程中注入方法实现,从而延迟目标类的initialized调用到正常逻辑。显然,it works!

    libextobjc

    大致看了下EXTConcreteProtocol.m中的实现,注入逻辑没什么变化,但是ProtocolKit使用起来确实更便捷。


    refs:

    1. ProtocolKit
    2. 那些被遗漏的Objective-C保留字
    3. ProtocolKit 解析与改进
    4. libextobjc

    相关文章

      网友评论

        本文标题:ProtocolKit 解读

        本文链接:https://www.haomeiwen.com/subject/dauvwctx.html