如何在 Objective-C 中實(shí)現(xiàn)協(xié)議擴(kuò)展

Swift 中的協(xié)議擴(kuò)展為 iOS 開發(fā)帶來(lái)了非常多的可能性,它為我們提供了一種類似多重繼承的功能,幫助我們減少一切可能導(dǎo)致重復(fù)代碼的地方。

關(guān)于 Protocol Extension

在 Swift 中比較出名的 Then 就是使用了協(xié)議擴(kuò)展為所有的 AnyObject 添加方法,而且不需要調(diào)用 runtime 相關(guān)的 API,其實(shí)現(xiàn)簡(jiǎn)直是我見過(guò)最簡(jiǎn)單的開源框架之一:

public protocol Then {}

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

extension NSObject: Then {}

只有這么幾行代碼,就能為所有的 NSObject 添加下面的功能:

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

這里沒(méi)有調(diào)用任何的 runtime 相關(guān) API,也沒(méi)有在 NSObject 中進(jìn)行任何的方法聲明,甚至 protocol Then {} 協(xié)議本身都只有一個(gè)大括號(hào),整個(gè) Then 框架就是基于協(xié)議擴(kuò)展來(lái)實(shí)現(xiàn)的。

在 Objective-C 中同樣有協(xié)議,但是這些協(xié)議只是相當(dāng)于接口,遵循某個(gè)協(xié)議的類只表明實(shí)現(xiàn)了這些接口,每個(gè)類都需要對(duì)這些接口有單獨(dú)的實(shí)現(xiàn),這就很可能會(huì)導(dǎo)致重復(fù)代碼的產(chǎn)生。

而協(xié)議擴(kuò)展可以調(diào)用協(xié)議中聲明的方法,以及 where Self: AnyObject 中的 AnyObject 的類/實(shí)例方法,這就大大提高了可操作性,便于開發(fā)者寫出一些意想不到的擴(kuò)展。

如果讀者對(duì) Protocol Extension 興趣或者不了解協(xié)議擴(kuò)展,可以閱讀最后的 Reference 了解相關(guān)內(nèi)容。

ProtocolKit

其實(shí)協(xié)議擴(kuò)展的強(qiáng)大之處就在于它能為遵循協(xié)議的類添加一些方法的實(shí)現(xiàn),而不只是一些接口,而今天為各位讀者介紹的 ProtocolKit 就實(shí)現(xiàn)了這一功能,為遵循協(xié)議的類添加方法。

ProtocolKit 的使用

我們先來(lái)看一下如何使用 ProtocolKit,首先定義一個(gè)協(xié)議:

@protocol TestProtocol

@required

- (void)fizz;

@optional

- (void)buzz;

@end

在協(xié)議中定義了兩個(gè)方法,必須實(shí)現(xiàn)的方法 fizz 以及可選實(shí)現(xiàn) buzz,然后使用 ProtocolKit 提供的接口 defs 來(lái)定義協(xié)議中方法的實(shí)現(xiàn)了:

@defs(TestProtocol)

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

@end

這樣所有遵循 TestProtocol 協(xié)議的對(duì)象都可以調(diào)用 buzz 方法,哪怕它們沒(méi)有實(shí)現(xiàn):

protocol-demo

上面的 XXObject 雖然沒(méi)有實(shí)現(xiàn) buzz 方法,但是該方法仍然成功執(zhí)行了。

ProtocolKit 的實(shí)現(xiàn)

ProtocolKit 的主要原理仍然是 runtime 以及宏的;通過(guò)宏的使用來(lái)隱藏類的聲明以及實(shí)現(xiàn)的代碼,然后在 main 函數(shù)運(yùn)行之前,將類中的方法實(shí)現(xiàn)加載到內(nèi)存,使用 runtime 將實(shí)現(xiàn)注入到目標(biāo)類中。

如果你對(duì)上面的原理有所疑惑也不是太大的問(wèn)題,這里只是給你一個(gè) ProtocolKit 原理的簡(jiǎn)單描述,讓你了解它是如何工作的。

ProtocolKit 中有兩條重要的執(zhí)行路線:

  • _pk_extension_load 將協(xié)議擴(kuò)展中的方法實(shí)現(xiàn)加載到了內(nèi)存
  • _pk_extension_inject_entry 負(fù)責(zé)將擴(kuò)展協(xié)議注入到實(shí)現(xiàn)協(xié)議的類

加載實(shí)現(xiàn)

首先要解決的問(wèn)題是如何將方法實(shí)現(xiàn)加載到內(nèi)存中,這里可以先了解一下上面使用到的 defs 接口,它其實(shí)只是一個(gè)調(diào)用了其它宏的超級(jí)宏這名字是我編的

#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 作為接口的是因?yàn)樗且粋€(gè)保留的 keyword,Xcode 會(huì)將它渲染成與 @property 等其他關(guān)鍵字相同的顏色。

上面的這一坨宏并不需要一個(gè)一個(gè)來(lái)分析,只需要看一下最后展開會(huì)變成什么:

@protocol TestProtocol; 

@interface __PKContainer_TestProtocol_0 : NSObject <TestProtocol>

@end

@implementation __PKContainer_TestProtocol_0

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

根據(jù)上面宏的展開結(jié)果,這里可以介紹上面的一坨宏的作用:

  • defs 這貨沒(méi)什么好說(shuō)的,只是 _pk_extension 的別名,為了提供一個(gè)更加合適的名字作為接口
  • _pk_extension_pk_extension_imp 中傳入 $protocol_pk_get_container_class($protocol) 參數(shù)
    • _pk_get_container_class 的執(zhí)行生成一個(gè)類名,上面生成的類名就是 __PKContainer_TestProtocol_0,這個(gè)類名是 __PKContainer_$protocol__COUNTER__ 拼接而成的(__COUNTER__ 只是一個(gè)計(jì)數(shù)器,可以理解為每次調(diào)用時(shí)加一)
  • _pk_extension_imp 會(huì)以傳入的類名生成一個(gè)遵循當(dāng)前 $protocol 協(xié)議的類,然后在 + load 方法中執(zhí)行 _pk_extension_load 加載擴(kuò)展協(xié)議

通過(guò)宏的運(yùn)用成功隱藏了 __PKContainer_TestProtocol_0 類的聲明以及實(shí)現(xiàn),還有 _pk_extension_load 函數(shù)的調(diào)用:

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 來(lái)保證靜態(tài)變量 allExtendedProtocols 以及 extendedProtcolCount extendedProtcolCapacity 不會(huì)因?yàn)榫€程競(jìng)爭(zhēng)導(dǎo)致問(wèn)題:

  • allExtendedProtocols 用于保存所有的 PKExtendedProtocol 結(jié)構(gòu)體
  • 后面的兩個(gè)變量確保數(shù)組不會(huì)越界,并在數(shù)組滿的時(shí)候,將內(nèi)存占用地址翻倍

方法的后半部分會(huì)在靜態(tài)變量中尋找或創(chuàng)建傳入的 protocol 對(duì)應(yīng)的 PKExtendedProtocol 結(jié)構(gòu)體:

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

這里調(diào)用的 _pk_extension_merge 方法非常重要,不過(guò)在介紹 _pk_extension_merge 之前,首先要了解一個(gè)用于保存協(xié)議擴(kuò)展信息的私有結(jié)構(gòu)體 PKExtendedProtocol

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

PKExtendedProtocol 結(jié)構(gòu)體中保存了協(xié)議的指針、實(shí)例方法、類方法、實(shí)例方法數(shù)以及類方法數(shù)用于框架記錄協(xié)議擴(kuò)展的狀態(tài)。

回到 _pk_extension_merge 方法,它會(huì)將新的擴(kuò)展方法追加到 PKExtendedProtocol 結(jié)構(gòu)體的數(shù)組 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
    ...
}

因?yàn)轭惙椒ǖ淖芳优c實(shí)例方法幾乎完全相同,所以上述代碼省略了向結(jié)構(gòu)體中的類方法追加方法的實(shí)現(xiàn)代碼。

實(shí)現(xiàn)中使用 class_copyMethodListcontainerClass 拉出方法列表以及方法數(shù)量;通過(guò) _pk_extension_create_merged 返回一個(gè)合并之后的方法列表,最后在更新結(jié)構(gòu)體中的 instanceMethods 以及 instanceMethodCount 成員變量。

_pk_extension_create_merged 只是重新 malloc 一塊內(nèi)存地址,然后使用 memcpy 將所有的方法都復(fù)制到了這塊內(nèi)存地址中,最后返回首地址:

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

這一節(jié)的代碼從使用宏生成的類中抽取方法實(shí)現(xiàn),然后以結(jié)構(gòu)體的形式加載到內(nèi)存中,等待之后的方法注入。

注入方法實(shí)現(xiàn)

注入方法的時(shí)間點(diǎn)在 main 函數(shù)執(zhí)行之前議實(shí)現(xiàn)的注入并不是在 + load 方法 + initialize 方法調(diào)用時(shí)進(jìn)行的,而是使用的編譯器指令(compiler directive) __attribute__((constructor)) 實(shí)現(xiàn)的:

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

使用上述編譯器指令的函數(shù)會(huì)在 shared library 加載的時(shí)候執(zhí)行,也就是 main 函數(shù)之前,可以看 StackOverflow 上的這個(gè)問(wèn)題 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 會(huì)在 main 執(zhí)行之前遍歷內(nèi)存中的所有 Class(整個(gè)遍歷過(guò)程都是在一個(gè)自動(dòng)釋放池中進(jìn)行的),如果某個(gè)類遵循了allExtendedProtocols 中的協(xié)議,調(diào)用 _pk_extension_inject_class 向類中注射(inject)方法實(shí)現(xiàn):

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: 注射類方法
}

如果類中沒(méi)有實(shí)現(xiàn)該實(shí)例方法就會(huì)通過(guò) runtime 中的 class_addMethod 注射該實(shí)例方法;而類方法的注射有些不同,因?yàn)轭惙椒ǘ际潜4嬖谠愔械模恍╊惙椒ㄓ捎谄涮厥獾匚蛔詈貌灰淖兤湓袑?shí)現(xiàn),比如 + load+ initialize 這兩個(gè)類方法就比較特殊,如果想要了解這兩個(gè)方法的相關(guān)信息,可以在 Reference 中查看相關(guān)的信息。

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

實(shí)現(xiàn)上的不同僅僅在獲取元類、以及跳過(guò) + load+ initialize 方法上。

總結(jié)

ProtocolKit 通過(guò)宏和 runtime 實(shí)現(xiàn)了類似協(xié)議擴(kuò)展的功能,其實(shí)現(xiàn)代碼總共也只有 200 多行,還是非常簡(jiǎn)潔的;在另一個(gè)叫做 libextobjc 的框架中也實(shí)現(xiàn)了類似的功能,有興趣的讀者可以查看 EXTConcreteProtocol.h · libextobjc 這個(gè)文件。

Reference

Github Repo:iOS-Source-Code-Analyze

Follow: Draveness · Github

原文鏈接:http://draveness.me/protocol-extension/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評(píng)論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,324評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,417評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,783評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,960評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,522評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,267評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,471評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,698評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評(píng)論 1 294
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,204評(píng)論 3 398
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,436評(píng)論 2 378

推薦閱讀更多精彩內(nèi)容

  • Objective-C 1. import的用法 拷貝文件內(nèi)容可以自動(dòng)防止文件的內(nèi)容被重復(fù)拷貝(#define宏定...
    馬文濤閱讀 5,354評(píng)論 3 17
  • 文中的實(shí)驗(yàn)代碼我放在了這個(gè)項(xiàng)目中。 以下內(nèi)容是我通過(guò)整理[這篇博客] (http://yulingtianxia....
    茗涙閱讀 937評(píng)論 0 6
  • 【Amy Yao閱讀記錄 】D179,從果果生病就沒(méi)記錄了,其實(shí)這幾天果果每天都在看書。《武則天》自傳已看完,目前...
    JessicaHu閱讀 249評(píng)論 0 0
  • 數(shù)學(xué)的套路是口訣、公式與方程式;物理學(xué)的套路是公式、定理和定律;化學(xué)的套路是分子式、反應(yīng)式與方程式。如果研究中國(guó)功...
    行者潘閱讀 1,150評(píng)論 0 6