『libextobjc』Objctive-C 協議的默認實現
繼續閱讀 libextobjc 的源碼,看到一個非常有趣的實現—— Objective-C 的 protocol 默認實現。當然,這不比 Swift 的 extension 默認實現,Objective-C 在這方面沒有 Swift 強大,并不能完全的實現 POP,但是這不妨給我們提供一種思路。
首先,列舉一下當面對這個問題時,都有哪些疑問:
- 會用到方法注入,但是什么時候注入?
- 以什么形式獲取默認實現的 SEL 與 IMP?
- 怎樣減少性能開銷?
然后,我們來一起看看源碼的實現思路( 因為更注重實現步驟和思想,所以在文章中不會出現大量的源碼,大家最好自行對照源碼進行閱讀 )。
用法
// MyProtocol.h
@protocol MyProtocol
@concrete
- (void)runTest;
@end
// MyProtocol.m
@concreteprotocol(MyProtocol)
- (void)runTest {
NSLog(@"%s", __FUNCTION__);
}
@end
在 EXTConcreteProtocol.h 中能找到 concrete 與 concreteprotocol 這對宏。 concrete 很簡單,也很巧妙,它就是用于修飾 protocol 方法的 optional 。用 concrete 宏的好處有兩個:
- 沿用 optional 的作用,防止遵循該 protocol 的類因為沒實現代理方法,而報警告。
- 語義更加清晰,能直接表情這以下的方法是已經默認實現了的。
然后來看看 concreteprotocol 的定義,首先, concreteprotocol 會將傳入的 protocol name 與字符串 _ProtocolMethodContainer 拼接,即 MyProtocol_ProtocolMethodContainer 。因為源碼不易閱讀,我將它簡化了一下(去掉注釋與報錯信息):

被框住的代碼就是 concreteprotocol(MyProtocol) 展開的部分。這段代碼能解答之前我們的兩個疑問:
- 什么時候注入:無論是在 +load 還是在被 __attribute__((constructor)) 修飾的函數中,至少能保證注入是發生在 main 函數之前的(關于 +load 與 __attribute__((constructor)) 的執行順序,請參考我之前的文章: 『Apple API』/ / attribute / )。
- 以怎樣的形式獲取 SEL 與 IMP:這個宏直接為 protocol 擴展了一個容器類,所以默認實現的方法都是存在這個類中的,之后要進行注入,方法的 SEL 與 IMP 也應該是從這個容器類中進行獲取。
所以,根據調用順序,我們接下來分成兩步來分析整個實現。
ext_addConcreteProtocol
首先被調用的是 +load 中的 ext_addConcreteProtocol 函數。這個函數接收兩個參數:當前的 protocol 對象,以及對應的容器類。實現中又調用了另外一個:
BOOL ext_addConcreteProtocol (Protocol *protocol, Class containerClass) {
return ext_loadSpecialProtocol(protocol, ^(Class destinationClass){
ext_injectConcreteProtocol(protocol, containerClass, destinationClass);
});
}
ext_loadSpecialProtocol 函數接收兩個參數:當前 protocol,以及一個參數為 Class destinationClass 的 block。我們先不看這個 block 的具體實現,先來看看 ext_loadSpecialProtocol 都做了些什么。
ext_loadSpecialProtocol
同樣,我簡化了 ext_loadSpecialProtocol 的實現,代碼大多用注釋描述來代替:
// protocol: 當前默認實現的 protocol
// void (^injectionBehavior)(Class destinationClass): 傳入的 block
BOOL ext_loadSpecialProtocol (Protocol *protocol, void (^injectionBehavior)(Class destinationClass)) {
// 判斷默認實現的 protocol 個數是否大于 SIZE_MAX
// specialProtocolCount 即默認實現的 protocol 的計數
if (specialProtocolCount == SIZE_MAX) {
return NO;
}
// specialProtocolCapacity 為數組總容量
if (specialProtocolCount >= specialProtocolCapacity) {
// 如果未超過 SIZE_MAX 則進行動態擴容
// 將動態擴容后的數組頭指針交給 specialProtocols
}
// 將參數的 block copy 到堆,并賦值給 copiedBlock
ext_specialProtocolInjectionBlock copiedBlock = [injectionBehavior copy];
// 將 protocol, block, 和 NO 組裝成 struct
// 然后將 struct 追加到數組中
specialProtocols[specialProtocolCount] = (EXTSpecialProtocol){
.protocol = protocol,
.injectionBlock = (__bridge_retained void *)copiedBlock,
.ready = NO
};
// 默認實現 protocol 的個數自增
++specialProtocolCount;
// success!
return YES;
}
所以,整個函數走下來,作用就是將 {protocol, block} 追加到數組中。
也就是說,在執行 __attribute__((constructor)) 修飾的方法以前,所有默認實現的 protocol,都會被加到這個數組中。

接下來,我們來看 +load 之后做了什么。
ext_loadConcreteProtocol
同樣, ext_loadConcreteProtocol 內部也調用了另一個函數:
void ext_loadConcreteProtocol (Protocol *protocol) {
ext_specialProtocolReadyForInjection(protocol);
}
整個函數的實現很簡單,用于確保 +load 中加入數組的所有 protocol 都能找到:
void ext_specialProtocolReadyForInjection (Protocol *protocol) {
// 循環遍歷數組
for (size_t i = 0;i < specialProtocolCount;++i) {
// 如果數組的 protocol 是當前 protocl
if (specialProtocols[i].protocol == protocol) {
// 并且這個 protocol 還未被遍歷過(也就是 ready 標識)
if (!specialProtocols[i].ready) {
// 則進行標記
specialProtocols[i].ready = YES;
// ready 標識計數自增
// !!! 當所有的 protocol 均 ready 之后
// 再調用 ext_injectSpecialProtocols
if (++specialProtocolsReady == specialProtocolCount)
ext_injectSpecialProtocols();
}
break;
}
}
}
在 ext_specialProtocolReadyForInjection 的實現中, if (++specialProtocolsReady == specialProtocolCount) 這個判斷比較有趣,它能回答我們的第三個問題(如何節省開銷):
- +load 與 __attribute__((constructor)) 的優先級能使得所有 protocol 加入完成以后,再進行處理。
- ready 計數 specialProtocolsReady 使得所有默認實現均判斷無誤后,再進行注入。
到此,好像已經完事具備,馬上就可以進行注入了。但蒼天饒過誰,我們還有很重要的一個問題沒有考慮。
ext_injectSpecialProtocols
優先級問題:如果 protocolA <ProtocolB> ,也就是 protocolA 遵循 protocolB ,那么誰的優先級更高呢?除此之外,如果遵循 protocol 的 class,自己也實現了默認方法呢?
這個問題,在 ext_injectSpecialProtocols 函數中能得到答案:
static void ext_injectSpecialProtocols (void) {
qsort_b(specialProtocols, specialProtocolCount, sizeof(EXTSpecialProtocol), ^(const void *a, const void *b){
// 根據 a 是否 comform b,對整個數組進行排序 (protocol_conformsToProtocol)
});
// 通過 objc_getClassList 獲得所有類列表
// 兩個 for 循環嵌套
// 對類列表以及 protocol 列表進行遍歷
// 如果 class comform protocol
// 則調用之前 struct 中的注入 block,進行注入
for (size_t i = 0;i < specialProtocolCount;++i) {
Protocol *protocol = specialProtocols[i].protocol;
for (unsigned classIndex = 0;classIndex < classCount;++classIndex) {
Class class = allClasses[classIndex];
if (!class_conformsToProtocol(class, protocol))
continue;
// 遵循 protocol 的 class 即為注入的目標 class
injectionBlock(class);
}
}
}
所以,整個方法的任務也很清晰:
- 對 protocol 進行優先級排序,給出具體注入的先后順序,防止方法覆蓋或無法注入。
- 獲取全部 class 列表。
- 兩層循環遍歷,將 class 與其遵循的 protocol 進行匹配。
- 調用 struct 中的 block,并將目標 class 傳出,進行注入。
接下來,終于到了最后一步,來看看 block 中的注入方法的實現。
ext_injectConcreteProtocol
block 是在 +load 中就已經賦值了,而 block 的實現,就是直接調用了 ext_injectConcreteProtocol 函數:
// 函數有三個參數
// protocol
// containerClass: 實現了 protocol 方法的 容器類
// class: 兩層循環嵌套中,找到的要注入的目標 class
static void ext_injectConcreteProtocol (Protocol *protocol, Class containerClass, Class class) {
// 獲取 容器類 中的實例方法列表
// 獲取 容器類 meta class 中的類方法列表
// 循環注入實例方法
for (unsigned methodIndex = 0;methodIndex < imethodCount;++methodIndex) {
// 獲取方法 SEL
// 獲取方法 IMP
// 判斷 目標類 是否存在該方法
// 進行注入
}
// 循環注入類方法同理
}
到此,方法注入就已經全部完成了。
總結
面向過程的過完了整個源碼,從頭再來梳理一下:
- 注入實現思路的重點在于,使用宏為 protocol 擴展了一個容器類。
- 容器類中,利用 +load 與 __attribute__((constructor)) 的特性,將注入流程分為了兩個部分。
- 在 +load 中,將 protocol,執行注入的 block 打包成 struct,然后將 struct 裝進數組。
- 當執行到 __attribute__((constructor)) 時,也就表示所有類的 +load 都已經執行過了,再對數組進行優先級排序。
- 排序完成后,兩層循環嵌套,查找遵循了 protocol 的 class。
- 調用 block 執行注入。
來自:http://www.saitjr.com/ios/『libextobjc』objctive-c-協議的默認實現.html