Foundation類簇類的method swizzling

?。。。。。?!此帖子內(nèi)容有待更深的考究!?。。。。。。?!

ios開發(fā)中,在很多情形下,會用到 method swizzling 方式,即hook某一類(一般是不開源框架類)的某方法,在方法的實現(xiàn)中添加自定義的邏輯。

比如,對于NSMutableDictionary類的setValue:forKey:方法(此方法實現(xiàn)在 NSMutableDictionary針對kvc支持的拓展中),添加key是否為空的判斷,以避免key為nil導(dǎo)致的崩潰等情況。
此時,需在NSMutableDictionary類的拓展類中添加重載load類方法的代碼,代碼套路如下:

+ (void)load {
        Class class = [self class];    //objc_getClass("__NSDictionaryM")  // line 1
        SEL selector_origin = @selector(setValue:forKey:);                         //line 2
        SEL selector_new = @selector(XXSetValue:forKey:);                      //line 3
        //XXSetValue:forKey: 方法為自定義方法,略

        Method method_origin = class_getInstanceMethod(class, selector_origin);  // line 4
        Method method_new = class_getInstanceMethod(class, selector_new);        //line 5
        
        method_exchangeImplementations(method_origin, method_new);                  //line 6
}

此時,當(dāng)在程序中其他地方,包含了此部分代碼的頭文件時,正常調(diào)用setValue: forKey:時,實際上已經(jīng)調(diào)用了自定義的方法 XXSetValue:forKey: 。以上代碼的作用就是將兩方法的實現(xiàn)代碼互換(method_exchangeImplementations方法)。

廢話說了這么多,重點在這,下圖為替換 setObject:forKey:方法的實現(xiàn)代碼:

        Class class = [self class];   //objc_getClass("__NSDictionaryM");         //line 0

        SEL selector_origin = @selector(setObject:forKey:);         //line 1
        SEL selector_new = @selector(XsetObj:forKey:);                //line 2
        
        Method method_origin = class_getInstanceMethod(class, selector_origin);   //line 3
        Method method_new = class_getInstanceMethod(class, selector_new);        //line 4
        
        method_exchangeImplementations(method_origin, method_new);            //line 5

注意,重點:在這里的hook實際上事很特殊的,因為如果此時swizzling的方法不是setValue:forKey:而是setObject:forKey:的話(代碼見以上),結(jié)果會大不相同(實際結(jié)果是后者的method swizzling不會成功);而如果想使setObject:forKey:的swizzling成功的話,需要在代碼 line 0 處,換成注釋的部分。(--而 對setValue方法時,使用objc_getClass("__NSDictionaryM")或者[self class]都是成功的)

問題來了:
1、為什么都是類的實例方法的 setobject和setvalue,實現(xiàn)method swizzling 的方式并不完全一樣呢??
2、 class_getInstanceMethod() 函數(shù)的第一參數(shù)的意義是什么??

/由于作者水平限制,以下解釋中,類簇的相關(guān)知識并不詳細(xì)且嚴(yán)謹(jǐn)。。/

首先,method_exchangeImplementations()函數(shù)的implementation交換是在類對象中完成,即問題2 中的函數(shù)參數(shù),所以問題2 的答案應(yīng)該是 第一參數(shù)是方法所在的類對象,或父類的類對象(稍后會解釋);當(dāng)selector為類方法而不是實例方法時,應(yīng)該使用class_getClassMethod()函數(shù),對應(yīng)的,問題2 的參數(shù)應(yīng)該是當(dāng)前類的元類對象,或者根元類(NSObject)的元類對象。

ps: 拓展一個知識點, [self class]無論在類方法中還是實例方法中,返回都是類對象。而object_getClass(self)函數(shù),當(dāng)在類方法中時,返回的是類對象的isa指針指向的類,即元類對象;在實例方法中,返回的才是類對象。-------因此要避免使用不合適的類對象做參數(shù)

其次,為什么setobject和setvalue方法的method swizzling 實現(xiàn)不一樣呢?首先,需要明白類簇的原理,(以下關(guān)于類簇的知識只是總結(jié)了一些碎塊化的知識點,然后瞎逼逼的。)簡單理解的話,類簇即根據(jù)不同的初始化方法,選擇多套子類代碼中某個作為當(dāng)前類,進(jìn)行實例的初始化,如[[NSNumber alloc] initWithFloat: 1.2f]和[[NSNumber alloc] initWithDouble: 1.3]會產(chǎn)生不同的類,所以可以理解為不同初始化方式得到的類可能不是同一個類,不同用途的類也可能不是同一個類----這里的setvalue對應(yīng)的類,即Method結(jié)構(gòu) method_origin的類對象參數(shù),和setobject對應(yīng)的類不是同一個個類----雖然都屬于NSMutableDictionary類(繼承自公共基類"__NSDictionaryM")。

想要實現(xiàn)正確的method swizzling,需要自當(dāng)前類對象,沿著繼承關(guān)系,依次向上替換method(考慮繼承類中有方法的重載現(xiàn)象--例如自定義UIViewController類重寫viewDidLoad方法);在不考慮繼承類重寫方法的時候,使用類簇的根基類(即implementation了當(dāng)前方法的&&最上層的類):

這里引用牛逼的人 的一個方法,獲取某類的所有直接子類列表:

//打印出 某類的所有直接子類
+ (NSArray *)findAllOf:(Class)defaultClass {
    int count = objc_getClassList(NULL, 0);
    if (count <= 0) {
        @throw@"Couldn't retrieve Obj-C class-list";
        return @[defaultClass];
    }
    NSMutableArray * output = @[].mutableCopy;
    Class * classes = (Class *) malloc(sizeof(Class) * count);
    objc_getClassList(classes, count);
    for (int i = 0; i < count; ++i) {
        if (defaultClass == class_getSuperclass(classes[i]))//子類
        {
            [output addObject: classes[i]];
        }   
    }
    free(classes);
    return output.copy;  
}

正確的方法替換如下:

while (class_getInstanceMethod(currentClass, @selector(setObject:forKey:))) {
            Class superClass = [currentClass superclass];
            IMP classResumeIMP = method_getImplementation(class_getInstanceMethod(currentClass, @selector(setObject:forKey:)));
            IMP superclassResumeIMP = method_getImplementation(class_getInstanceMethod(superClass, @selector(setObject:forKey:)));
            if (classResumeIMP != superclassResumeIMP &&
                originalAFResumeIMP != classResumeIMP) {
                [self swizzleMethodForClass:currentClass];
// swizzleMethodForClass 方法是 完成方法交換
            }
            currentClass = [currentClass superclass];
        }

最終通過setObject和setValue的例子,也可以得出牛逼的人 一篇帖子中的一個說法,子類若未重載基類的方法,在class_getInstanceMethod調(diào)用后,會copy一份基類的方法到自己的方法列表中;而實際進(jìn)行方法交換的method就是這一份拷貝。因此,就會導(dǎo)致在類簇類進(jìn)行method swizzling時,class參數(shù)的選取不當(dāng),導(dǎo)致swizzling失敗。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容