如何在 Objective-C 中實現協議擴展

Swift 中的協議擴展爲 iOS 開發帶來了很是多的可能性,它爲咱們提供了一種相似多重繼承的功能,幫助咱們減小一切可能致使重複代碼的地方。html

關於 Protocol Extension

在 Swift 中比較出名的 Then 就是使用了協議擴展爲全部的 AnyObject 添加方法,並且不須要調用 runtime 相關的 API,其實現簡直是我見過最簡單的開源框架之一:ios

public protocol Then {}

extension Then where Self: AnyObject {
    public func then(@noescape block: Self -> Void) -> Self {
        block(self)
        return self
    }
}

extension NSObject: Then {}

只有這麼幾行代碼,就能爲全部的 NSObject 添加下面的功能:git

let titleLabel = UILabel().then {
    $0.textColor = .blackColor()
    $0.textAlignment = .Center
}

這裏沒有調用任何的 runtime 相關 API,也沒有在 NSObject 中進行任何的方法聲明,甚至 protocol Then {} 協議自己都只有一個大括號,整個 Then 框架就是基於協議擴展來實現的。github

在 Objective-C 中一樣有協議,可是這些協議只是至關於接口,遵循某個協議的類只代表實現了這些接口,每一個類都須要對這些接口有單獨的實現,這就極可能會致使重複代碼的產生。swift

而協議擴展能夠調用協議中聲明的方法,以及 where Self: AnyObject 中的 AnyObject 的類/實例方法,這就大大提升了可操做性,便於開發者寫出一些意想不到的擴展。數組

若是讀者對 Protocol Extension 興趣或者不瞭解協議擴展,能夠閱讀最後的 Reference 瞭解相關內容。app

ProtocolKit

其實協議擴展的強大之處就在於它能爲遵循協議的類添加一些方法的實現,而不僅是一些接口,而今天爲各位讀者介紹的 ProtocolKit 就實現了這一功能,爲遵循協議的類添加方法。框架

ProtocolKit 的使用

咱們先來看一下如何使用 ProtocolKit,首先定義一個協議:jsp

@protocol TestProtocol

@required

- (void)fizz;

@optional

- (void)buzz;

@end

在協議中定義了兩個方法,必須實現的方法 fizz 以及可選實現 buzz,而後使用 ProtocolKit 提供的接口 defs 來定義協議中方法的實現了:函數

@defs(TestProtocol)

- (void)buzz {
    NSLog(@"Buzz");
}

@end

這樣全部遵循 TestProtocol 協議的對象均可以調用 buzz 方法,哪怕它們沒有實現:

protocol-demo

上面的 XXObject 雖然沒有實現 buzz 方法,可是該方法仍然成功執行了。

ProtocolKit 的實現

ProtocolKit 的主要原理仍然是 runtime 以及宏的;經過宏的使用來隱藏類的聲明以及實現的代碼,而後在 main 函數運行以前,將類中的方法實現加載到內存,使用 runtime 將實現注入到目標類中。

若是你對上面的原理有所疑惑也不是太大的問題,這裏只是給你一個 ProtocolKit 原理的簡單描述,讓你瞭解它是如何工做的。

ProtocolKit 中有兩條重要的執行路線:

  • _pk_extension_load 將協議擴展中的方法實現加載到了內存

  • _pk_extension_inject_entry 負責將擴展協議注入到實現協議的類

加載實現

首先要解決的問題是如何將方法實現加載到內存中,這裏能夠先了解一下上面使用到的 defs 接口,它其實只是一個調用了其它宏的超級宏這名字是我編的

#define defs _pk_extension

#define _pk_extension($protocol) _pk_extension_imp($protocol, _pk_get_container_class($protocol))

#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); \
    } \

#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

使用 defs 做爲接口的是由於它是一個保留的 keyword,Xcode 會將它渲染成與 @property 等其餘關鍵字相同的顏色。

上面的這一坨宏並不須要一個一個來分析,只須要看一下最後展開會變成什麼:

@protocol TestProtocol; 

@interface __PKContainer_TestProtocol_0 : NSObject <TestProtocol>

@end

@implementation __PKContainer_TestProtocol_0

+ (void)load {
    _pk_extension_load(@protocol(TestProtocol), __PKContainer_TestProtocol_0.class); 
}

根據上面宏的展開結果,這裏能夠介紹上面的一坨宏的做用:

  • defs 這貨沒什麼好說的,只是 _pk_extension 的別名,爲了提供一個更加合適的名字做爲接口

  • _pk_extension_pk_extension_imp 中傳入 $protocol_pk_get_container_class($protocol) 參數

    • _pk_get_container_class 的執行生成一個類名,上面生成的類名就是 __PKContainer_TestProtocol_0,這個類名是 __PKContainer_$protocol__COUNTER__ 拼接而成的(__COUNTER__ 只是一個計數器,能夠理解爲每次調用時加一)

  • _pk_extension_imp 會以傳入的類名生成一個遵循當前 $protocol 協議的類,而後在 + load 方法中執行 _pk_extension_load 加載擴展協議

經過宏的運用成功隱藏了 __PKContainer_TestProtocol_0 類的聲明以及實現,還有 _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;
    }
    
    ...

    pthread_mutex_unlock(&protocolsLoadingLock);
}

ProtocolKit 使用了 protocolsLoadingLock 來保證靜態變量 allExtendedProtocols 以及 extendedProtcolCount extendedProtcolCapacity 不會由於線程競爭致使問題:

  • allExtendedProtocols 用於保存全部的 PKExtendedProtocol 結構體

  • 後面的兩個變量確保數組不會越界,並在數組滿的時候,將內存佔用地址翻倍

方法的後半部分會在靜態變量中尋找或建立傳入的 protocol 對應的 PKExtendedProtocol 結構體:

size_t resultIndex = SIZE_T_MAX;
for (size_t index = 0; index < extendedProtcolCount; ++index) {
    if (allExtendedProtocols[index].protocol == protocol) {
        resultIndex = index;
        break;
    }
}

if (resultIndex == SIZE_T_MAX) {
    allExtendedProtocols[extendedProtcolCount] = (PKExtendedProtocol){
        .protocol = protocol,
        .instanceMethods = NULL,
        .instanceMethodCount = 0,
        .classMethods = NULL,
        .classMethodCount = 0,
    };
    resultIndex = extendedProtcolCount;
    extendedProtcolCount++;
}

_pk_extension_merge(&(allExtendedProtocols[resultIndex]), containerClass);

這裏調用的 _pk_extension_merge 方法很是重要,不過在介紹 _pk_extension_merge 以前,首先要了解一個用於保存協議擴展信息的私有結構體 PKExtendedProtocol

typedef struct {
    Protocol *__unsafe_unretained protocol;
    Method *instanceMethods;
    unsigned instanceMethodCount;
    Method *classMethods;
    unsigned classMethodCount;
} PKExtendedProtocol;

PKExtendedProtocol 結構體中保存了協議的指針、實例方法、類方法、實例方法數以及類方法數用於框架記錄協議擴展的狀態。

回到 _pk_extension_merge 方法,它會將新的擴展方法追加到 PKExtendedProtocol 結構體的數組 instanceMethods 以及 classMethods 中:

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
    ...
}

由於類方法的追加與實例方法幾乎徹底相同,因此上述代碼省略了向結構體中的類方法追加方法的實現代碼。

實現中使用 class_copyMethodListcontainerClass 拉出方法列表以及方法數量;經過 _pk_extension_create_merged 返回一個合併以後的方法列表,最後在更新結構體中的 instanceMethods 以及 instanceMethodCount 成員變量。

_pk_extension_create_merged 只是從新 malloc 一塊內存地址,而後使用 memcpy 將全部的方法都複製到了這塊內存地址中,最後返回首地址:

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;
}

這一節的代碼從使用宏生成的類中抽取方法實現,而後以結構體的形式加載到內存中,等待以後的方法注入。

注入方法實現

注入方法的時間點在 main 函數執行以前議實現的注入並非在 + load 方法 + initialize 方法調用時進行的,而是使用的編譯器指令(compiler directive) __attribute__((constructor)) 實現的:

__attribute__((constructor)) static void _pk_extension_inject_entry(void);

使用上述編譯器指令的函數會在 shared library 加載的時候執行,也就是 main 函數以前,能夠看 StackOverflow 上的這個問題 How exactly does __attribute__((constructor)) work?

__attribute__((constructor)) static void _pk_extension_inject_entry(void) {
    #1:加鎖
    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);
            }
        }
    }
    #2:解鎖並釋放 allClasses、allExtendedProtocols
}

_pk_extension_inject_entry 會在 main 執行以前遍歷內存中的全部 Class(整個遍歷過程都是在一個自動釋放池中進行的),若是某個類遵循了allExtendedProtocols 中的協議,調用 _pk_extension_inject_class 向類中注射(inject)方法實現:

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);
    }
    
    #1: 注射類方法
}

若是類中沒有實現該實例方法就會經過 runtime 中的 class_addMethod 注射該實例方法;而類方法的注射有些不一樣,由於類方法都是保存在元類中的,而一些類方法因爲其特殊地位最好不要改變其原有實現,好比 + load+ initialize 這兩個類方法就比較特殊,若是想要了解這兩個方法的相關信息,能夠在 Reference 中查看相關的信息。

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);
}

實現上的不一樣僅僅在獲取元類、以及跳過 + load+ initialize 方法上。

總結

ProtocolKit 經過宏和 runtime 實現了相似協議擴展的功能,其實現代碼總共也只有 200 多行,仍是很是簡潔的;在另外一個叫作 libextobjc 的框架中也實現了相似的功能,有興趣的讀者能夠查看 EXTConcreteProtocol.h · libextobjc 這個文件。

Reference

Github Repo:iOS-Source-Code-Analyze

Follow: Draveness · Github

原文連接:http://draveness.me/protocol-...

相關文章
相關標籤/搜索