最近遇到個(gè)需求,在APP加載的時(shí)候,動(dòng)態(tài)獲取所有實(shí)現(xiàn)了XXXListener協(xié)議的類,初始化并添加到listenerArray中,這樣,每次有新的業(yè)務(wù)模塊需要監(jiān)聽的時(shí)候,就不需要手動(dòng)addListener了。
當(dāng)然,除了這種方案,還有很多其他的方案可選,但其他方案都需要手動(dòng)添加listener。
確定了方案,開始實(shí)現(xiàn)功能,實(shí)現(xiàn)思路比較簡單,用runtime動(dòng)態(tài)獲取所有的類,再根據(jù)自己的需求進(jìn)行篩選,代碼大致如下:
- (NSArray<Class> *)classesConformsToProtocol:(Protocol *)protocol{
//注冊(cè)類的總數(shù)
int count = objc_getClassList(NULL,0);
NSMutableArray *array = [NSMutableArray arrayWithCapacity:0];
//獲取所有已注冊(cè)的類
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;
}
//調(diào)用
NSArray *array = [self classesConformsToProtocol:@protocol(UITableViewDataSource)];
如果是想獲取某個(gè)類的所有子類,只需要修改下篩選邏輯即可:
if (superClass == class_getSuperclass(clazz)) {
[array addObject: clazz];
}
好了,功能實(shí)現(xiàn)了,看上去很完美,但作為一個(gè)優(yōu)秀的程序員,我們要將眼光放遠(yuǎn),不能只滿足于功能實(shí)現(xiàn)。
發(fā)現(xiàn)問題
話不多說,先運(yùn)行下程序,看看方法的執(zhí)行時(shí)間:

20毫秒,怎么說呢,雖然不算很長,但我這只是個(gè)Demo啊,所有的類加起來也不到10個(gè),需要執(zhí)行這么久么?
但是我突然想到,雖然Demo里只定義了幾個(gè)類,但我們獲取的是所有已加載的類,系統(tǒng)的類也是類??!
于是我打印了下count:

emmm....兩萬多個(gè)類,怪不得要執(zhí)行這么久...
然后我順便看了下我們公司的項(xiàng)目,將近5.6萬個(gè)類,這么查可不太合適啊...
我覺得,優(yōu)化的方法肯定是有的,可是不論是百度還是google,都沒有發(fā)現(xiàn)更好的寫法,沒辦法,只能自己研究了。
我想,如果有優(yōu)化的方法,那一定是在runtime.h文件中,果然,我找到了這三個(gè)方法:

Image是Executable(可執(zhí)行文件),Dylib或Bundle中的一種,所以同一個(gè)庫中的所有類的image都相同。
先針對(duì)Demo的情況,因?yàn)閷?shí)際需要遍歷的類都是我們?cè)陧?xiàng)目中創(chuàng)建的,所以我們只需要遍歷當(dāng)前類的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];
}
}
運(yùn)行時(shí)間如下:

可以看到,查找效率提升了30倍左右,非常nice。
順便貼一下image的信息:
/private/var/containers/Bundle/Application/60C80966-7B72-42BB-A441-A906DDE8DECB/CycleListenerDemo.app/CycleListenerDemo
更加復(fù)雜的情況
上邊只是針對(duì)Demo,但實(shí)際的使用場景中,需要實(shí)現(xiàn)的Protocol可能在其他的庫中,需要實(shí)現(xiàn)Protocol的類可能也在不同的庫中,這樣,我們就沒辦法只在當(dāng)前的image中尋找了。
但我們肯定也不需要遍歷所有的image,于是我打印了一下項(xiàng)目中的image,發(fā)現(xiàn)有551個(gè)之多!我隨便截了一部分,如下圖所示:

經(jīng)過分析發(fā)現(xiàn),/System/Library/Frameworks/,/System/Library/PrivateFrameworks/,/usr/lib/路徑下的image都是系統(tǒng)的庫,可以不用去遍歷。
去掉這些之后的內(nèi)容如下:

剩下的image已經(jīng)不多了,進(jìn)一步分析后發(fā)現(xiàn),我們需要處理的image全部都在/private/var/containers/Bundle/Application/Your Application ID/xxx.app/目錄下,其他的image均為系統(tǒng)的。
這個(gè)目錄下有一個(gè)xxx可執(zhí)行文件和一個(gè)Frameworks目錄,目錄下的image是項(xiàng)目中引入的第三方或我們自己的framework。
但是仔細(xì)觀察會(huì)發(fā)現(xiàn),并不是項(xiàng)目中所有的framework都會(huì)對(duì)應(yīng)一個(gè)單獨(dú)的image,比如我們常用的Bugly,就沒有出現(xiàn)在剩余的image列表里:

經(jīng)過分析,我猜測(cè),Frameworks目錄下的,應(yīng)該都是動(dòng)態(tài)庫,而所有的靜態(tài)庫,應(yīng)該都在xxx可執(zhí)行文件中。
下面我們來驗(yàn)證下,我從列表中隨機(jī)找了一個(gè)framework,然后進(jìn)入到對(duì)應(yīng)目錄:
cd .../.../Flutter.framework
然后查看文件信息:
file Flutter
打印的信息如下:

然后我們?cè)倏纯?code>Bugly的信息:

果然是靜態(tài)庫,看來我們猜的沒錯(cuò),接下來我們打印下可執(zhí)行文件
image里所有的類,看看bugly在不在里邊:

驗(yàn)證完畢,Frameworks目錄下確實(shí)都是動(dòng)態(tài)庫的image。
一般來說,會(huì)封裝成動(dòng)態(tài)庫的都不會(huì)耦合業(yè)務(wù)邏輯,所以,Frameworks目錄下的image我們也不需要遍歷了~
那么我們需要遍歷的image,就只有一個(gè)了,就是APP可執(zhí)行文件的image,就是當(dāng)前類的image~
如果真的那么巧,Protocol放到了動(dòng)態(tài)庫里了,那么就遍歷當(dāng)前和APP這兩個(gè)image就好了~
如果特別特別巧,別的動(dòng)態(tài)庫里也有實(shí)現(xiàn)了協(xié)議的,那么我們只好將APP和Frameworks目錄全都遍歷了...
iOS 16新增加的API
在runtime.h文件中,我又找到了這個(gè)方法:

估摸著是蘋果看到開發(fā)者遍歷所有類的需求比較多,但是遍歷的效率又太差,所以提供了一個(gè)官方的遍歷方法。
不過,這個(gè)方法是iOS 16才加的,之前的版本是用不了這個(gè)api的,我們用這個(gè)最新的api,查找一下可執(zhí)行文件image中的類,看看使用這個(gè)方法能提高多少效率(image參數(shù)傳null代表在調(diào)用者的image中查找):

0.3毫秒,比我們自己寫的查找快了1倍多,不得不說,系統(tǒng)的方法就是棒~
接下來我們?cè)贉y(cè)試一下在所有image中查找:

54毫秒,反而比我們最開始的方法還要久,經(jīng)過分析發(fā)現(xiàn),這個(gè)新的api的入?yún)?code>image,是需要用dlopen()這個(gè)函數(shù)將imageName進(jìn)行轉(zhuǎn)化的,并不能直接傳入image的字符串,而多出的時(shí)間就是消耗在dlopen()這個(gè)方法上的。
而直接在當(dāng)前image中查找,并不需要轉(zhuǎn)化image,只需要傳入null即可,所以,新增加的這個(gè)api,只適合在當(dāng)前的image中查找的情況。
到這里,我們可能會(huì)想到,既然新api適合在當(dāng)前的image中查找,那么我們可以做個(gè)版本判斷,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;
}
運(yùn)行了一下,結(jié)果如下:

我運(yùn)行的系統(tǒng)確實(shí)是iOS 16+,但為什么這次的運(yùn)行時(shí)間是之前的4倍之多呢?
答案其實(shí)不難猜,因?yàn)?code>@available(iOS 16.0, *)這個(gè)判斷,也相對(duì)比較耗時(shí),多出來的時(shí)間是在這里的。
結(jié)論&&最終方案
結(jié)論就是,新的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;
}
//調(diào)用
[self classesConformsToProtocol:@protocol(UITableViewDataSource) forImage:nil];