!D汲选5懊!7踩琛=渲啊!此帖子內容有待更深的考究M盖:樵铩!H槲凇E踉稀!:翰佟T倮础!
ios開發(fā)中磷瘤,在很多情形下芒篷,會用到 method swizzling 方式,即hook某一類(一般是不開源框架類)的某方法采缚,在方法的實現(xiàn)中添加自定義的邏輯针炉。
比如,對于NSMutableDictionary類的setValue:forKey:方法(此方法實現(xiàn)在 NSMutableDictionary針對kvc支持的拓展中)扳抽,添加key是否為空的判斷篡帕,以避免key為nil導致的崩潰等情況殖侵。
此時,需在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
}
此時,當在程序中其他地方怔鳖,包含了此部分代碼的頭文件時茉唉,正常調用setValue: forKey:時,實際上已經調用了自定義的方法 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:的話(代碼見以上)斜姥,結果會大不相同(實際結果是后者的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ù)的意義是什么?禁筏?
/由于作者水平限制持钉,以下解釋中,類簇的相關知識并不詳細且嚴謹篱昔。每强。/
首先,method_exchangeImplementations()函數(shù)的implementation交換是在類對象中完成州刽,即問題2 中的函數(shù)參數(shù)空执,所以問題2 的答案應該是 第一參數(shù)是方法所在的類對象,或父類的類對象(稍后會解釋)穗椅;當selector為類方法而不是實例方法時辨绊,應該使用class_getClassMethod()函數(shù),對應的房待,問題2 的參數(shù)應該是當前類的元類對象邢羔,或者根元類(NSObject)的元類對象。
ps: 拓展一個知識點桑孩, [self class]無論在類方法中還是實例方法中拜鹤,返回都是類對象。而object_getClass(self)函數(shù)流椒,當在類方法中時敏簿,返回的是類對象的isa指針指向的類,即元類對象宣虾;在實例方法中惯裕,返回的才是類對象。-------因此要避免使用不合適的類對象做參數(shù)
其次绣硝,為什么setobject和setvalue方法的method swizzling 實現(xiàn)不一樣呢蜻势?首先,需要明白類簇的原理鹉胖,(以下關于類簇的知識只是總結了一些碎塊化的知識點握玛,然后瞎逼逼的。)簡單理解的話甫菠,類簇即根據(jù)不同的初始化方法挠铲,選擇多套子類代碼中某個作為當前類,進行實例的初始化寂诱,如[[NSNumber alloc] initWithFloat: 1.2f]和[[NSNumber alloc] initWithDouble: 1.3]會產生不同的類拂苹,所以可以理解為不同初始化方式得到的類可能不是同一個類,不同用途的類也可能不是同一個類----這里的setvalue對應的類痰洒,即Method結構 method_origin的類對象參數(shù)瓢棒,和setobject對應的類不是同一個個類----雖然都屬于NSMutableDictionary類(繼承自公共基類"__NSDictionaryM")。
想要實現(xiàn)正確的method swizzling丘喻,需要自當前類對象音羞,沿著繼承關系,依次向上替換method(考慮繼承類中有方法的重載現(xiàn)象--例如自定義UIViewController類重寫viewDidLoad方法)仓犬;在不考慮繼承類重寫方法的時候嗅绰,使用類簇的根基類(即implementation了當前方法的&&最上層的類):
這里引用牛逼的人 的一個方法,獲取某類的所有直接子類列表:
//打印出 某類的所有直接子類
+ (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調用后叽躯,會copy一份基類的方法到自己的方法列表中财边;而實際進行方法交換的method就是這一份拷貝。因此点骑,就會導致在類簇類進行method swizzling時酣难,class參數(shù)的選取不當谍夭,導致swizzling失敗。