Method Swizzling
Method-Swizzling實(shí)際就是更換方法所對(duì)應(yīng)的實(shí)現(xiàn)函數(shù)(IMP),其主要作用是在運(yùn)行時(shí)將一個(gè)方法的實(shí)現(xiàn)替換成另一個(gè)方法的實(shí)現(xiàn)钮蛛,這就是我們常說(shuō)的 iOS黑魔法剖膳。Method Swizzling標(biāo)準(zhǔn)用法示例
+(void)load{
//dispatch_once一次行方法
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//這里獲取類的時(shí)候,需要注意
//在類方法中吱晒,使用[self class]獲取到當(dāng)前類的信息。交換實(shí)例方法時(shí)叹话,使用[self class]
//在類方法中墩瞳,使用獲取到當(dāng)前類的信息object_getClass(self),獲取的是元類的信息喉酌。交換類方法時(shí),使用object_getClass(self)
//以下是交換實(shí)例方法的舉例
Class class = [self class];
//獲取到方法的SEL
SEL originalSEL = @selector(swizzle_Orginal_Method);
SEL swizzledSEL = @selector(swizzle_Swizzled_Method);
//從類及其父類中查詢方法method_t般妙。(如果在類中不存在歪架,就會(huì)遍歷該類的父類)
/*
method_t *m = nil;
while (cls && ((m = getMethodNoSuper_nolock(cls, sel))) == nil) {
cls = cls->getSuperclass();
}
return m;
*/
Method originalMethod = class_getInstanceMethod(class, originalSEL);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);
//使用 class_addMethod方法,先添加到類中和蚪。添加的時(shí)候SEL是原來(lái)的SEL烹棉,但是IMP是新的IMP
//該方法怯疤,如果類中已經(jīng)存在方法,就返回false集峦;不存在該方法,把方法插入到方法列表摘昌,并返回true
/*
if ((m = getMethodNoSuper_nolock(cls, name))) {
// already exists
result = m->imp(false);
} else {
method_list_t *newlist;
addMethods_finish(cls, newlist);
result = nil;
}
*/
BOOL didAddMethod = class_addMethod(class,
originalSEL,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
//如果方法添加成功高蜂,證明originalSEL和swizzledIMP添加到了方法列表
//接下來(lái)只處理swizzledSEL和originalIMP即可
//使用class_replaceMethod方法,用originalIMP 替換掉swizzledSEL的IMP
//class_replaceMethod方法稿饰,存在I方法露泊,就直接把新的IMP賦值給老的SEL
/*
if ((m = getMethodNoSuper_nolock(cls, name))) {
// already exists
result = _method_setImplementation(cls, m, imp);
} else {
// fixme optimize
method_list_t *newlist;
addMethods_finish(cls, newlist);
result = nil;
}
*/
class_replaceMethod(class,
swizzledSEL,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
//如果方法列表已經(jīng)存在了該方法,及addMethod方法返回的是NO惭笑。直接交換方法
/******************
*m1->setImp(imp2);
*m2->setImp(imp1);
******************/
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
Method Swizzling涉及的相關(guān)方法
1、類方法中的class
與object_getClass
在方法交換的過(guò)程中铺敌,需要判斷交換的方法類型屁擅,是實(shí)例方法還是類方法。方法交換的過(guò)程一般是在類的+(void)load;
方法中進(jìn)行派歌。這樣的話,怎樣獲取類是很重要的胶果。
在類方法中調(diào)用+ (Class)class
,其實(shí)在+ (Class)class
方法內(nèi)部是直接返回了self霎烙,也就是當(dāng)前類。而如果在類方法中調(diào)用object_getClass
悬垃,其實(shí)返回的是元類。所以在類方法中烘豌,使用這兩個(gè)函數(shù),是完全不同的廊佩。
+ (Class)class {
return self;
}
示例代碼:從打印結(jié)果和地址的輸出靖榕,打印出來(lái)的類是一樣但是兩者地址是不同的。
- (void)instanceMethod{
Class cls_Clazz = [self class];
Class cls_Objc = object_getClass(self);
NSLog(@"cls_Clazz is %@ -- cls_Objc is %@",cls_Clazz,cls_Objc);
}
//打印結(jié)果為
2022-08-02 17:36:09.385057+0800 schemeUse[6795:292600] cls_Clazz is MethodSend -- cls_Objc is MethodSend
//對(duì)cls_Clazz和cls_Objc 輸出:
(lldb) p/x cls_Clazz
(Class) $0 = 0x0000000102ed5380 MethodSend
(lldb) p/x cls_Objc
(Class) $1 = 0x0000000102ed5358
所以鸯绿,如果要交換的方法是類方法,在獲取類的時(shí)候需要使用object_getClass(self)
; 要交換的方法是實(shí)例方法的話,獲取類的方法是[self class]
2毒返、獲取方法Method_t
獲取方法使用的是:Method class_getInstanceMethod(Class cls, SEL sel)
租幕。
方法參數(shù)如下:
-
Class cls
:要查詢方法的所在的類 -
SEL sel
:要查詢方法的Selector拧簸,可認(rèn)為是方法的名字
該方法會(huì)按照消息發(fā)送機(jī)制的慢查詢機(jī)制(lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
),查一遍方法的實(shí)現(xiàn)(IMP)信息贾富。然后從類的方法列表中查詢方法實(shí)現(xiàn)(search_method_list_inline
) 牺六。如果在當(dāng)前類中查詢不到方法的實(shí)現(xiàn),會(huì)遍歷該類的父類
淑际,查詢父類的方法列表(cls = cls->getSuperclass();
)。
查詢代碼如下:
3盗胀、為類cls添加方法class_addMethod
為類添加一個(gè)方法锄贼,使用到的函數(shù)是BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
。
方法參數(shù)如下:
-
Class cls
:要添加方法的所在的類 -
SEL sel
:要添加方法的Selector,可認(rèn)為是方法的名字 -
IMP imp
:要添加方法的實(shí)現(xiàn)浸策,是個(gè)函數(shù)指針 -
const char *types
:是一個(gè)常量指針屈糊,表示方法的簽名信息。
該方法內(nèi)部調(diào)用的是static IMP addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
逻锐。
addMethod
方法首先對(duì)類的方法列表進(jìn)行遍歷(不會(huì)遍歷父類的方法列表
),判斷是否已經(jīng)存在這個(gè)方法昧诱,如果存在的話,就取出方法的實(shí)現(xiàn)(IMP)返回凶掰;如果不存在這個(gè)方法蜈亩,就會(huì)把該方法插入到方法列表,然后返回一個(gè) nil
稚配。
4、替換掉原來(lái)的方法實(shí)現(xiàn):class_replaceMethod
使用運(yùn)行時(shí)的方法來(lái)替換掉原方法的IMP午衰,使用class_replaceMethod
冒萄。
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
參數(shù)如下:
-
Class cls
:要替換方法的所在的類 -
SEL sel
:要替換方法的Selector,可認(rèn)為是方法的名字 -
IMP imp
:要替換方法的實(shí)現(xiàn)尊流,是個(gè)函數(shù)指針 -
const char *types
:是一個(gè)常量指針,表示方法的簽名信息蜘澜。
該方法會(huì)在方法列表中查詢一下是否有方法的實(shí)現(xiàn)(不會(huì)遍歷父類的方法列表
)响疚。如果沒(méi)有方法的實(shí)現(xiàn),就把方法插入到方法列表忿晕,返回的數(shù)據(jù)為nil;方法列表中已經(jīng)存在方法,就把新的IMP替換掉原來(lái)的IMP宾巍,并返回原來(lái)的IMP渔伯。
該方法的實(shí)現(xiàn)如下:
5、方法的交換:class_replaceMethod
在類中锣吼,把方法的IMP交換,可以使用void method_exchangeImplementations(Method m1_gen, Method m2_gen)
.
參數(shù)如下:
-
Method m1_gen
:需要交換的一個(gè)方法 -
Method m2_gen
:需要交換的另一個(gè)方法
使用查詢方法古徒,獲取到方法的結(jié)構(gòu)(Method
)后读恃,可以調(diào)用這個(gè)方法,實(shí)現(xiàn)方法的交換寺惫。
該方法可實(shí)現(xiàn)同類中,不同方法交換IMP西雀;也可以實(shí)現(xiàn)不同類的方法交換。
同類中的方法交換:
+(void)load{
//以下是交換實(shí)例方法的舉例
Class class = [self class];
//獲取到方法的SEL
SEL originalSEL = @selector(swizzle_Orginal_Method);
SEL swizzledSEL = @selector(swizzle_Swizzled_Method);
//獲取到方法
Method originalMethod = class_getInstanceMethod(class, originalSEL);
Method swizzled_Dog_Method = class_getInstanceMethod(class, swizzledSEL);
//實(shí)現(xiàn)方法交換
method_exchangeImplementations(originalMethod, swizzled_Dog_Method);
}
兩個(gè)類中的方法交換:
+(void)load{
//original Method方法所在的類
Class class = [self class];
//獲取到方法的SEL
SEL originalSEL = @selector(swizzle_Orginal_Method);
SEL swizzled_Dog_SEL = @selector(swizzling_DogMethod);
//獲取到方法
Method originalMethod = class_getInstanceMethod(class, originalSEL);
Method swizzled_Dog_Method = class_getInstanceMethod([Dog class], swizzled_Dog_SEL);
//實(shí)現(xiàn)方法交換
method_exchangeImplementations(originalMethod, swizzled_Dog_Method);
}
Method Swizzling需要注意的問(wèn)題
1判莉、需要確保該流程只能執(zhí)行一次
method-swizzling 多次調(diào)用會(huì)導(dǎo)致方法的重復(fù)交換豆挽,無(wú)法保證是否達(dá)到了最終的交換效果。
如何解決這個(gè)問(wèn)題呢帮哈?
在+load
方法中執(zhí)行锰镀,且使用 dispatch_once包裹;這樣就可以保證方法交換只執(zhí)行一次泳炉。
使用 dispatch_once包裹的原因在于,防止后續(xù)有手動(dòng)的調(diào)用+load
方法的情況氧腰,從而導(dǎo)致再次交換了方法。
2古拴、子類沒(méi)有方法的實(shí)現(xiàn),在父類的方法列表中查到
前面介紹獲取Method
的方法class_getInstanceMethod
時(shí)黄痪,在其方法實(shí)現(xiàn)中,可以看出是嗜,在查找方法時(shí)油额,如果在類中沒(méi)有查到叠纷。會(huì)繼續(xù)在類的父類中進(jìn)行查詢潦嘶。當(dāng)我們要交換方法實(shí)現(xiàn)時(shí),并沒(méi)有交換當(dāng)前類的方法航厚,而是交換了當(dāng)前類的父類的實(shí)現(xiàn)锰蓬。
這樣當(dāng)其他地方調(diào)用這個(gè)父類的方法時(shí)幔睬,也會(huì)調(diào)用我們所替換的方法芹扭,這顯然使我們不想要的。
/*======================================*/
/************--Man的類實(shí)現(xiàn)--*************/
/*=====================================*/
@interface Man : NSObject
- (void)stu_SuperMethod;
@end
@implementation Man
- (void)stu_SuperMethod{
NSLog(@"我是 Student 的父類--Man");
}
/*======================================*/
/**********--Student的類實(shí)現(xiàn)--***********/
/*=====================================*/
@interface Student : Man
-(void)stu_instanceMethod;
@end
@implementation Student
-(void)stu_instanceMethod{
NSLog(@"我是student類中的 stu_instanceMethod");
}
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
//獲取到方法的SEL
SEL originalSEL = @selector(stu_instanceMethod);
SEL swizzledSEL = @selector(stu_SuperMethod);
Method originalMethod = class_getInstanceMethod(class, originalSEL);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
//方法調(diào)用
- (instancetype)init{
if (self = [super init]) {
//父類的方法調(diào)用哦
Man * m = [[Man alloc] init];
[m stu_SuperMethod];
//調(diào)用自己的房啊
[self stu_instanceMethod];
//父類的方法調(diào)用哦
Man * m1 = [[Man alloc] init];
[m1 stu_SuperMethod];
}
return self;
}
@end
打印結(jié)果如下:
2022-08-08 11:01:27.085961+0800 schemeUse[3098:115122] 我是student類中的 stu_instanceMethod
2022-08-08 11:01:27.086018+0800 schemeUse[3098:115122] 我是 Student 的父類--Man
2022-08-08 11:01:27.086063+0800 schemeUse[3098:115122] 我是student類中的 stu_instanceMethod
解決方案:
先通過(guò) class_addMethod 嘗試給自己添加要交換的方法辅肾,如果添加成功轮锥,即表明類中沒(méi)有這個(gè)方法,則通過(guò) class_replaceMethod 進(jìn)行替換新娜,如果添加失敗則說(shuō)明類中有這個(gè)方法,正常進(jìn)行 method_exchangeImplementations概龄。