iOS獲取類的所有子類/所有實現協議的類的最佳實踐

最近遇到個需求,在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就好了~

如果特別特別巧,別的動態庫里也有實現了協議的,那么我們只好將APPFrameworks目錄全都遍歷了...

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];
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容