文章涉及依賴注入方案基於
EXTConcreteProtocol
實現,GitHub連接在這裏。laravel
若是基於 Cocopods 和 Git Submodules 來作組件化的時候,咱們的依賴關係是這樣的:git
這裏依賴路徑有兩條:github
這種單向的依賴關係,決定了從組件到項目的通信是單向的,即主項目能夠主動向組件發起通信,可是組件卻沒有辦法主動和主項目通信。緩存
你可能說不對,能夠發通知啊?是的,是能夠發通知,可是這一點都不優雅,也很差維護和拓展。bash
有沒有一種更加優雅、更加方便平常開發的拓展和維護的方式呢?答案是有的,名字叫作「依賴注入」。微信
依賴注入有另一個名字,叫作「控制反轉」,像上面的組件化的例子,主項目依賴組件,如今有一個需求,組件須要依賴主項目,這種狀況就叫作「控制反轉」。app
能把這部分「控制反轉」的代碼統一塊兒來解耦維護,方便往後拓展和維護的服務,咱們就能夠叫作依賴注入。框架
因此依賴注入有兩個比較重要的點:jsp
不是我自身的,倒是我須要的,都是我所依賴的。一切須要外部提供的,都是須要進行依賴注入的。函數
這句話出自這篇文章:理解依賴注入與控制反轉 | Laravel China 社區 - 高品質的 Laravel 開發者社區
若是對概念性的東西有更加深刻的理解,歡迎谷歌搜索「依賴注入」。
iOS 平臺實現依賴注入功能的開源項目有兩個大頭:
詳細對比發現這兩個框架都是嚴格遵循依賴注入的概念來實現的,並無將 Objective-C 的 runtime 特性發揮到極致,因此使用起來很麻煩。
還有一點,這兩個框架使用繼承的方式實現注入功能,對項目的侵入性不容小視。若是你以爲這個侵入性不算什麼,那等到你項目大到必定程度,發現以前選擇的技術方案有考慮不周,你想切換到其餘方案的時候,你必定會後悔當時沒選擇那個不侵入項目的方案。
那有沒有其餘沒那麼方案呢?
GitHub - jspahrsummers/libextobjc: A Cocoa library to extend the Objective-C programming language. 裏有一個 EXTConcreteProtocol
雖然沒有直接叫作依賴注入,而是叫作混合協議,可是充分使用了 OC 動態語言的特性,不侵入項目,高度自動化,框架十分輕量,使用很是簡單。
輕量到什麼地步?就只有一個 .h
一個 .m
文件。 簡單到什麼地步?就只須要一個 @conreteprotocol
關鍵字,你就已經注入好了。
從一個評價開源框架的方方面面都甩開上面兩個框架好幾條街。
可是他也有致命的缺點,魚和熊掌不可兼得,這個咱們等會說。
有兩個比較重要的概念須要提早明白才能繼續往下將。
__attribute__()
這是一個 GNU 編譯器語法,被 constructor
這個關鍵字修飾的方法會在全部類的 +load
方法以後,在 main
函數以前被調用。詳見:Clang Attributes 黑魔法小記 · sunnyxx的技術博客如上圖,用一句話來描述注入的過程:將待注入的容器中的方法在 load
方法以後 main
函數以前注入指定的類中。
比方說有一個協議 ObjectProtocol
。咱們只要這樣寫就已經實現了依賴注入。
@protocol ObjectProtocol<NSObject>
+ (void)sayHello;
- (int)age;
@end
@concreteprotocol(ObjectProtocol)
+ (void)sayHello {
NSLog(@"Hello");
}
- (int)age {
return 18;
}
@end
複製代碼
以後比方說一個 Person
類想要擁有這個注入方法,就只須要遵照這個協議就能夠了。
@interface Person : NSObject<ObjectProtocol>
@end
複製代碼
咱們接下來就能夠對 Person
調用注入的方法。
int main(int argc, char * argv[]) {
Person *p = [Person new];
NSLog(@"%@", [p age]);
[p.class sayHello];
}
輸出:
>>>18
>>>Hello
複製代碼
是否是很神奇?想不想探一下究竟?
先來看一下頭文件:
#define concreteprotocol(NAME) \ // 定義一個容器類.
interface NAME ## _ProtocolMethodContainer : NSObject < NAME > {} \
@end \
\
@implementation NAME ## _ProtocolMethodContainer \
// load 方法添加混合協議.
+ (void)load { \
if (!ext_addConcreteProtocol(objc_getProtocol(metamacro_stringify(NAME)), self)) \
fprintf(stderr, "ERROR: Could not load concrete protocol %s\n", metamacro_stringify(NAME)); \
} \
// load 以後, main 以前執行方法注入.
__attribute__((constructor)) \
static void ext_ ## NAME ## _inject (void) { \
ext_loadConcreteProtocol(objc_getProtocol(metamacro_stringify(NAME))); \
}
// load 方法添加混合協議.
BOOL ext_addConcreteProtocol (Protocol *protocol, Class methodContainer);
// load 以後, main 以前執行方法注入.
void ext_loadConcreteProtocol (Protocol *protocol);
複製代碼
能夠在源碼中清楚看到 concreteprotocol
這個宏定義爲咱們的協議添加了一個容器類,咱們主要注入的好比 +sayHello
和 -age
方法都被定義在這個容器類之中。
而後在 +load
方法中調用了 ext_addConcreteProtocol
方法。
typedef struct {
// 用戶定義的協議.
__unsafe_unretained Protocol *protocol;
// 在 __attribute__((constructor)) 時往指定類裏注入方法的 block.
void *injectionBlock;
// 對應的協議是否已經準備好注入.
BOOL ready;
} EXTSpecialProtocol;
BOOL ext_addConcreteProtocol (Protocol *protocol, Class containerClass) {
return ext_loadSpecialProtocol(protocol, ^(Class destinationClass){
ext_injectConcreteProtocol(protocol, containerClass, destinationClass);
});
}
BOOL ext_loadSpecialProtocol (Protocol *protocol, void (^injectionBehavior)(Class destinationClass)) {
@autoreleasepool {
NSCParameterAssert(protocol != nil);
NSCParameterAssert(injectionBehavior != nil);
// 加鎖
if (pthread_mutex_lock(&specialProtocolsLock) != 0) {
fprintf(stderr, "ERROR: Could not synchronize on special protocol data\n");
return NO;
}
// specialProtocols 是一個鏈表,每一個協議都會被組織成爲一個 EXTSpecialProtocol,這個 specialProtocols 裏存放了了這些 specialProtocols.
if (specialProtocolCount >= specialProtocolCapacity) {
...
}
#ifndef __clang_analyzer__
ext_specialProtocolInjectionBlock copiedBlock = [injectionBehavior copy];
// 將協議保存爲一個 EXTSpecialProtocol 結構體.
specialProtocols[specialProtocolCount] = (EXTSpecialProtocol){
.protocol = protocol,
.injectionBlock = (__bridge_retained void *)copiedBlock,
.ready = NO
};
#endif
++specialProtocolCount;
pthread_mutex_unlock(&specialProtocolsLock);
}
return YES;
}
複製代碼
咱們的 ext_loadSpecialProtocol
方法裏傳進去一個 block,這個 block 裏調用了 ext_injectConcreteProtocol
這個方法。
ext_injectConcreteProtocol
這個方法接受三個參數,第一個是協議,就是咱們要注入的方法的協議;第二個是容器類,就是框架爲咱們添加的那個容器;第三個參數是目標註入類,就是咱們要把這個容器裏的方法注入到哪一個類。
static void ext_injectConcreteProtocol (Protocol *protocol, Class containerClass, Class class) {
// 獲取容器類裏全部的實例方法.
unsigned imethodCount = 0;
Method *imethodList = class_copyMethodList(containerClass, &imethodCount);
// 獲取容器類裏全部的類方法方法.
unsigned cmethodCount = 0;
Method *cmethodList = class_copyMethodList(object_getClass(containerClass), &cmethodCount);
// 拿到要注入方法的類的元類.
Class metaclass = object_getClass(class);
// 注入實例方法.
for (unsigned methodIndex = 0;methodIndex < imethodCount;++methodIndex) {
Method method = imethodList[methodIndex];
SEL selector = method_getName(method);
// 若是該類已經實現了這個方法,就跳過注入,不至於覆蓋用戶自定義的實現.
if (class_getInstanceMethod(class, selector)) {
continue;
}
IMP imp = method_getImplementation(method);
const char *types = method_getTypeEncoding(method);
if (!class_addMethod(class, selector, imp, types)) {
fprintf(stderr, "ERROR: Could not implement instance method -%s from concrete protocol %s on class %s\n",
sel_getName(selector), protocol_getName(protocol), class_getName(class));
}
}
// 注入類方法.
for (unsigned methodIndex = 0;methodIndex < cmethodCount;++methodIndex) {
Method method = cmethodList[methodIndex];
SEL selector = method_getName(method);
// +initialize 不能被注入.
if (selector == @selector(initialize)) {
continue;
}
// 若是該類已經實現了這個方法,就跳過注入,不至於覆蓋用戶自定義的實現.
if (class_getInstanceMethod(metaclass, selector)) {
continue;
}
IMP imp = method_getImplementation(method);
const char *types = method_getTypeEncoding(method);
if (!class_addMethod(metaclass, selector, imp, types)) {
fprintf(stderr, "ERROR: Could not implement class method +%s from concrete protocol %s on class %s\n",
sel_getName(selector), protocol_getName(protocol), class_getName(class));
}
}
// 管理內存
free(imethodList); imethodList = NULL;
free(cmethodList); cmethodList = NULL;
// 容許用戶在容器類裏複寫 +initialize 方法,這裏調用是保證用戶複寫的實現可以被執行.
(void)[containerClass class];
}
複製代碼
咱們再看一下在 +load
以後 main
以前調用的 ext_loadConcreteProtocol
方法。
void ext_loadConcreteProtocol (Protocol *protocol) {
ext_specialProtocolReadyForInjection(protocol);
}
void ext_specialProtocolReadyForInjection (Protocol *protocol) {
@autoreleasepool {
NSCParameterAssert(protocol != nil);
// 加鎖
if (pthread_mutex_lock(&specialProtocolsLock) != 0) {
fprintf(stderr, "ERROR: Could not synchronize on special protocol data\n");
return;
}
// 檢查要對應的 protocol 是否已經加載進上面的鏈表中了,若是找到了,就將對應的 EXTSpecialProtocol 結構體的 ready 置爲 YES.
for (size_t i = 0;i < specialProtocolCount;++i) {
if (specialProtocols[i].protocol == protocol) {
if (!specialProtocols[i].ready) {
specialProtocols[i].ready = YES;
assert(specialProtocolsReady < specialProtocolCount);
if (++specialProtocolsReady == specialProtocolCount)
// 若是全部的 EXTSpecialProtocol 結構體都準備好了,就開始執行注入.
ext_injectSpecialProtocols();
}
break;
}
}
pthread_mutex_unlock(&specialProtocolsLock);
}
}
複製代碼
上面都是準備工做,接下來開始進入核心方法進行注入。
static void ext_injectSpecialProtocols (void) {
// 對協議進行排序.
// 比方說 A 協議繼承自 B 協議,可是不必定是 B 協議對應的容器類的 load 方法先執行,A 的後執行. 因此若是 B 協議的類方法中複寫了 A 協議中的方法,那麼應該保證 B 協議複寫的方法被注入,而不是 A 協議的容器方法的實現.
// 爲了保證這個循序,因此要對協議進行排序,上面說的 A 繼承自 B,那麼循序應該是 A 在 B 前面.
qsort_b(specialProtocols, specialProtocolCount, sizeof(EXTSpecialProtocol), ^(const void *a, const void *b){
if (a == b)
return 0;
const EXTSpecialProtocol *protoA = a;
const EXTSpecialProtocol *protoB = b;
int (^protocolInjectionPriority)(const EXTSpecialProtocol *) = ^(const EXTSpecialProtocol *specialProtocol){
int runningTotal = 0;
for (size_t i = 0;i < specialProtocolCount;++i) {
if (specialProtocol == specialProtocols + i)
continue;
if (protocol_conformsToProtocol(specialProtocol->protocol, specialProtocols[i].protocol))
runningTotal++;
}
return runningTotal;
};
return protocolInjectionPriority(protoB) - protocolInjectionPriority(protoA);
});
// 獲取項目中全部的類 😭😭😭.
unsigned classCount = objc_getClassList(NULL, 0);
if (!classCount) {
fprintf(stderr, "ERROR: No classes registered with the runtime\n");
return;
}
Class *allClasses = (Class *)malloc(sizeof(Class) * (classCount + 1));
if (!allClasses) {
fprintf(stderr, "ERROR: Could not allocate space for %u classes\n", classCount);
return;
}
classCount = objc_getClassList(allClasses, classCount);
@autoreleasepool {
// 遍歷全部的要注入的協議結構體.
for (size_t i = 0;i < specialProtocolCount;++i) {
Protocol *protocol = specialProtocols[i].protocol;
// 使用 __bridge_transfer 把對象的內存管理交給 ARC.
ext_specialProtocolInjectionBlock injectionBlock = (__bridge_transfer id)specialProtocols[i].injectionBlock;
specialProtocols[i].injectionBlock = NULL;
// 遍歷全部的類 😭😭😭.
for (unsigned classIndex = 0;classIndex < classCount;++classIndex) {
Class class = allClasses[classIndex];
// 若是這個類遵照了要注入的協議,那麼就執行注入.
// 注意: 這裏是 continue 不是 break,由於一個類能夠注入多個協議的方法.
if (!class_conformsToProtocol(class, protocol))
continue;
injectionBlock(class);
}
}
}
// 管理內存.
free(allClasses);
free(specialProtocols); specialProtocols = NULL;
specialProtocolCount = 0;
specialProtocolCapacity = 0;
specialProtocolsReady = 0;
}
複製代碼
這一路看下來,原理看的明明白白,是否是也沒什麼特別的,都是 runtime 的知識。可是這個思路確實是 666。
這不挺好的嗎?別人也分析過這個框架的源碼,我再寫一遍有什麼意義?
這問題挺好,確實是這樣,若是一切順利,我這篇文章沒有存在的意義。接下來看一下問題出如今哪?
看到我剛纔的註釋了嗎?這個笑臉很燦爛。若是項目不大,好比項目只有幾百個類,這些都沒有問題的,可是咱們項目有接近 30000 個類,沒錯,是三萬。咱們使用注入的地方有幾十上百處,兩套 for 循環算下來是一個百萬級別的。並且 objc_getClassList
這個方法是很是耗時的並且沒有緩存。
// 獲取項目中全部的類 😭😭😭.
// 遍歷全部的類 😭😭😭.
複製代碼
在貝聊項目上,這個方法在個人 iPhone 6s Plus 上要耗時一秒,在更老的 iPhone 6 上耗時要 3 秒,iPhone 5 能夠想象要更久。並且隨着項目迭代,項目中的類會愈來愈多, 這個耗時也會愈來愈長。
這個耗時是 pre-main 耗時,就是用戶看那個白屏啓動圖的時候在作這個操做,嚴重影響用戶體驗。咱們的產品就由於這個點致使閃屏廣告展現出現問題,直接影響業務。
從上面的分析能夠知道,致使耗時的緣由就是原框架獲取全部的類進行遍歷。其實這是一個自動化的牛逼思路,這也是這個框架高於前面兩個框架的核心緣由。可是由於項目規模的緣由致使這個點成爲了實踐中的短板,這也是做者始料未及的。
那咱們怎麼優化這個點呢?由於要注入方法的類沒有作其餘的標記,只能掃描全部的類,找到那些遵照了這個協議的再進行注入,這是要注入的類和注入行爲的惟一聯繫點。從設計的角度來講,若是要主動實現注入,確實是這樣的,沒有更好方案來實現相同的功能。
可是有一個下策,能顯著提升這部分性能,就是退回到上面兩個框架所作的那樣,讓用戶本身去標識哪些類須要注入。這樣我把這些須要注入的類放到一個集合裏,遍歷注入,這樣作性能是最好的。若是我從頭設計一個方案,這也是不錯的選擇。
可是我如今作不了這些,我項目裏有好幾百個地方用了注入,若是我採用上面的方式,我要改好幾百個地方。這樣作很低效,並且我也不能保證我眼睛不會花出個錯。我只能選擇自動化去作這個事。
若是換個思路,我不主動注入,我懶加載,等你調用注入的方法我再執行注入操做呢?若是能實現這個,那問題就解決了。
+load
方法中作準備工做,和原有的實現同樣,把全部的協議都存到鏈表中。__attribute__((constructor))
中仍然作是否能執行注入的檢查。NSObject
的 +resolveInstanceMethod:
和 +resolveClassMethod:
。下面這個連接是我全部文章的一個集合目錄。這些文章凡是涉及實現的,每篇文章中都有 Github 地址,Github 上都有源碼。
你這一讚助,我寫的就更來勁了!
微信贊助掃碼
支付寶贊助掃碼