最近遇到個需求,在APP加載的時候,動態獲取所有實現了XXXListener
協議的類,初始化并添加到listenerArray
中,這樣,每次有新的業務模塊需要監聽的時候,就不需要手動addListener
了。
當然,除了這種方案,還有很多其他的方案可選,但其他方案都需要手動添加listener
。
確定了方案,開始實現功能,實現思路比較簡單,用runtime
動態獲取所有的類,再根據自己的需求進行篩選,代碼大致如下:
- (NSArray<Class> *)classesConformsToProtocol:(Protocol *)protocol{
//注冊類的總數
int count = objc_getClassList(NULL,0);
NSMutableArray *array = [NSMutableArray arrayWithCapacity:0];
//獲取所有已注冊的類
Class *classes = (Class *)malloc(sizeof(Class) * count);
count = objc_getClassList(classes, count);
for (int i = 0; i < count; i++) {
Class clazz = classes[i];
if (class_conformsToProtocol(clazz, protocol)) {
[array addObject:clazz];
}
}
free(classes);
return array;
}
//調用
NSArray *array = [self classesConformsToProtocol:@protocol(UITableViewDataSource)];
如果是想獲取某個類的所有子類,只需要修改下篩選邏輯即可:
if (superClass == class_getSuperclass(clazz)) {
[array addObject: clazz];
}
好了,功能實現了,看上去很完美,但作為一個優秀的程序員,我們要將眼光放遠,不能只滿足于功能實現。
發現問題
話不多說,先運行下程序,看看方法的執行時間:
20
毫秒,怎么說呢,雖然不算很長,但我這只是個Demo啊,所有的類加起來也不到10個,需要執行這么久么?
但是我突然想到,雖然Demo里只定義了幾個類,但我們獲取的是所有已加載的類,系統的類也是類??!
于是我打印了下count
:
emmm....兩萬多個類,怪不得要執行這么久...
然后我順便看了下我們公司的項目,將近5.6萬個類,這么查可不太合適啊...
我覺得,優化的方法肯定是有的,可是不論是百度還是google,都沒有發現更好的寫法,沒辦法,只能自己研究了。
我想,如果有優化的方法,那一定是在runtime.h
文件中,果然,我找到了這三個方法:
Image是Executable(可執行文件),Dylib或Bundle中的一種,所以同一個庫中的所有類的image都相同。
先針對Demo
的情況,因為實際需要遍歷的類都是我們在項目中創建的,所以我們只需要遍歷當前類的image
中的所有類即可,大致代碼如下:
const char *imageName = class_getImageName(self.class);
unsigned int count;
const char **classNames = objc_copyClassNamesForImage(imageName, &count);
for (int i = 0; i < count; i++) {
Class clazz = objc_getClass(classNames[i]);
if (class_conformsToProtocol(clazz, protocol)) {
[array addObject:clazz];
}
}
運行時間如下:
可以看到,查找效率提升了30
倍左右,非常nice。
順便貼一下image
的信息:
/private/var/containers/Bundle/Application/60C80966-7B72-42BB-A441-A906DDE8DECB/CycleListenerDemo.app/CycleListenerDemo
更加復雜的情況
上邊只是針對Demo,但實際的使用場景中,需要實現的Protocol
可能在其他的庫中,需要實現Protocol
的類可能也在不同的庫中,這樣,我們就沒辦法只在當前的image
中尋找了。
但我們肯定也不需要遍歷所有的image
,于是我打印了一下項目中的image
,發現有551
個之多!我隨便截了一部分,如下圖所示:
經過分析發現,/System/Library/Frameworks/
,/System/Library/PrivateFrameworks/
,/usr/lib/
路徑下的image
都是系統的庫,可以不用去遍歷。
去掉這些之后的內容如下:
剩下的image
已經不多了,進一步分析后發現,我們需要處理的image
全部都在/private/var/containers/Bundle/Application/Your Application ID/xxx.app/
目錄下,其他的image
均為系統的。
這個目錄下有一個xxx
可執行文件和一個Frameworks
目錄,目錄下的image
是項目中引入的第三方或我們自己的framework
。
但是仔細觀察會發現,并不是項目中所有的framework
都會對應一個單獨的image
,比如我們常用的Bugly
,就沒有出現在剩余的image
列表里:
經過分析,我猜測,Frameworks
目錄下的,應該都是動態庫,而所有的靜態庫,應該都在xxx
可執行文件中。
下面我們來驗證下,我從列表中隨機找了一個framework
,然后進入到對應目錄:
cd .../.../Flutter.framework
然后查看文件信息:
file Flutter
打印的信息如下:
然后我們再看看Bugly
的信息:
果然是靜態庫,看來我們猜的沒錯,接下來我們打印下可執行文件
image
里所有的類,看看bugly
在不在里邊:
驗證完畢,Frameworks
目錄下確實都是動態庫的image
。
一般來說,會封裝成動態庫的都不會耦合業務邏輯,所以,Frameworks
目錄下的image
我們也不需要遍歷了~
那么我們需要遍歷的image
,就只有一個了,就是APP可執行文件的image
,就是當前類的image
~
如果真的那么巧,Protocol
放到了動態庫里了,那么就遍歷當前
和APP
這兩個image
就好了~
如果特別特別巧,別的動態庫里也有實現了協議的,那么我們只好將APP
和Frameworks
目錄全都遍歷了...
iOS 16新增加的API
在runtime.h
文件中,我又找到了這個方法:
估摸著是蘋果看到開發者遍歷所有類的需求比較多,但是遍歷的效率又太差,所以提供了一個官方的遍歷方法。
不過,這個方法是iOS 16
才加的,之前的版本是用不了這個api的,我們用這個最新的api,查找一下可執行文件image
中的類,看看使用這個方法能提高多少效率(image
參數傳null
代表在調用者的image
中查找):
0.3
毫秒,比我們自己寫的查找快了1倍
多,不得不說,系統的方法就是棒~
接下來我們再測試一下在所有image
中查找:
54
毫秒,反而比我們最開始的方法還要久,經過分析發現,這個新的api的入參image
,是需要用dlopen()
這個函數將imageName
進行轉化的,并不能直接傳入image
的字符串,而多出的時間就是消耗在dlopen()
這個方法上的。
而直接在當前image
中查找,并不需要轉化image
,只需要傳入null
即可,所以,新增加的這個api,只適合在當前的image
中查找的情況。
到這里,我們可能會想到,既然新api適合在當前的image中查找
,那么我們可以做個版本判斷,iOS 16
之前用老方法,之后用新方法,代碼大概如下:
- (NSArray<Class> *)classesConformsToProtocol:(Protocol *)protocol{
NSMutableArray *array = [NSMutableArray arrayWithCapacity:0];
if (@available(iOS 16.0, *)) {
objc_enumerateClasses(nil, nil, protocol, nil, ^(Class _Nonnull aClass, BOOL * _Nonnull stop){
[array addObject:aClass];
});
return array;
}
const char *imageName = class_getImageName(self.class);
unsigned int count;
const char **classNames = objc_copyClassNamesForImage(imageName, &count);
for (int i = 0; i < count; i++) {
Class clazz = objc_getClass(classNames[i]);
if (class_conformsToProtocol(clazz, protocol)) {
[array addObject:clazz];
}
}
return array;
}
運行了一下,結果如下:
我運行的系統確實是iOS 16+
,但為什么這次的運行時間是之前的4倍
之多呢?
答案其實不難猜,因為@available(iOS 16.0, *)
這個判斷,也相對比較耗時,多出來的時間是在這里的。
結論&&最終方案
結論就是,新的api目前不適合在任何場景使用。
最終代碼:
- (NSArray<Class> *)classesConformsToProtocol:(Protocol *)protocol forImage:(const char *)imageName{
NSMutableArray *array = [NSMutableArray arrayWithCapacity:0];
if (!imageName) {
imageName = class_getImageName(self.class);
}
unsigned int count;
const char **classNames = objc_copyClassNamesForImage(imageName, &count);
for (int i = 0; i < count; i++) {
Class clazz = objc_getClass(classNames[i]);
if (class_conformsToProtocol(clazz, protocol)) {
[array addObject:clazz];
}
}
return array;
}
//調用
[self classesConformsToProtocol:@protocol(UITableViewDataSource) forImage:nil];