Swift 中的協議擴展爲 iOS 開發帶來了很是多的可能性,它爲咱們提供了一種相似多重繼承的功能,幫助咱們減小一切可能致使重複代碼的地方。html
在 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,首先定義一個協議:jsp
@protocol TestProtocol @required - (void)fizz; @optional - (void)buzz; @end
在協議中定義了兩個方法,必須實現的方法 fizz
以及可選實現 buzz
,而後使用 ProtocolKit 提供的接口 defs
來定義協議中方法的實現了:函數
@defs(TestProtocol) - (void)buzz { NSLog(@"Buzz"); } @end
這樣全部遵循 TestProtocol
協議的對象均可以調用 buzz
方法,哪怕它們沒有實現:
上面的 XXObject
雖然沒有實現 buzz
方法,可是該方法仍然成功執行了。
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_copyMethodList
從 containerClass
拉出方法列表以及方法數量;經過 _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 這個文件。
Github Repo:iOS-Source-Code-Analyze
Follow: Draveness · Github