Method Swizzling
Method-Swizzling实际就是更换方法所对应的实现函数(IMP),其主要作用是在运行时将一个方法的实现替换成另一个方法的实现,这就是我们常说的 iOS黑魔法。Method Swizzling标准用法示例
+(void)load{
//dispatch_once一次行方法
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//这里获取类的时候,需要注意
//在类方法中,使用[self class]获取到当前类的信息。交换实例方法时,使用[self class]
//在类方法中,使用获取到当前类的信息object_getClass(self),获取的是元类的信息。交换类方法时,使用object_getClass(self)
//以下是交换实例方法的举例
Class class = [self class];
//获取到方法的SEL
SEL originalSEL = @selector(swizzle_Orginal_Method);
SEL swizzledSEL = @selector(swizzle_Swizzled_Method);
//从类及其父类中查询方法method_t。(如果在类中不存在,就会遍历该类的父类)
/*
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方法,先添加到类中。添加的时候SEL是原来的SEL,但是IMP是新的IMP
//该方法,如果类中已经存在方法,就返回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添加到了方法列表
//接下来只处理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 {
//如果方法列表已经存在了该方法,及addMethod方法返回的是NO。直接交换方法
/******************
*m1->setImp(imp2);
*m2->setImp(imp1);
******************/
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
Method Swizzling涉及的相关方法
1、类方法中的class
与object_getClass
在方法交换的过程中,需要判断交换的方法类型,是实例方法还是类方法。方法交换的过程一般是在类的+(void)load;
方法中进行。这样的话,怎样获取类是很重要的。
在类方法中调用+ (Class)class
,其实在+ (Class)class
方法内部是直接返回了self,也就是当前类。而如果在类方法中调用object_getClass
,其实返回的是元类。所以在类方法中,使用这两个函数,是完全不同的。
+ (Class)class {
return self;
}
示例代码:从打印结果和地址的输出,打印出来的类是一样但是两者地址是不同的。
- (void)instanceMethod{
Class cls_Clazz = [self class];
Class cls_Objc = object_getClass(self);
NSLog(@"cls_Clazz is %@ -- cls_Objc is %@",cls_Clazz,cls_Objc);
}
//打印结果为
2022-08-02 17:36:09.385057+0800 schemeUse[6795:292600] cls_Clazz is MethodSend -- cls_Objc is MethodSend
//对cls_Clazz和cls_Objc 输出:
(lldb) p/x cls_Clazz
(Class) $0 = 0x0000000102ed5380 MethodSend
(lldb) p/x cls_Objc
(Class) $1 = 0x0000000102ed5358
所以,如果要交换的方法是类方法,在获取类的时候需要使用object_getClass(self)
; 要交换的方法是实例方法的话,获取类的方法是[self class]
2、获取方法Method_t
获取方法使用的是:Method class_getInstanceMethod(Class cls, SEL sel)
。
方法参数如下:
-
Class cls
:要查询方法的所在的类 -
SEL sel
:要查询方法的Selector,可认为是方法的名字
该方法会按照消息发送机制的慢查询机制(lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
),查一遍方法的实现(IMP)信息。然后从类的方法列表中查询方法实现(search_method_list_inline
) 。如果在当前类中查询不到方法的实现,会遍历该类的父类
,查询父类的方法列表(cls = cls->getSuperclass();
)。
查询代码如下:
3、为类cls添加方法class_addMethod
为类添加一个方法,使用到的函数是BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
。
方法参数如下:
-
Class cls
:要添加方法的所在的类 -
SEL sel
:要添加方法的Selector,可认为是方法的名字 -
IMP imp
:要添加方法的实现,是个函数指针 -
const char *types
:是一个常量指针,表示方法的签名信息。
该方法内部调用的是static IMP addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
。
addMethod
方法首先对类的方法列表进行遍历(不会遍历父类的方法列表
),判断是否已经存在这个方法,如果存在的话,就取出方法的实现(IMP)返回;如果不存在这个方法,就会把该方法插入到方法列表,然后返回一个 nil
。
4、替换掉原来的方法实现:class_replaceMethod
使用运行时的方法来替换掉原方法的IMP,使用class_replaceMethod
。
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
参数如下:
-
Class cls
:要替换方法的所在的类 -
SEL sel
:要替换方法的Selector,可认为是方法的名字 -
IMP imp
:要替换方法的实现,是个函数指针 -
const char *types
:是一个常量指针,表示方法的签名信息。
该方法会在方法列表中查询一下是否有方法的实现(不会遍历父类的方法列表
)。如果没有方法的实现,就把方法插入到方法列表,返回的数据为nil;方法列表中已经存在方法,就把新的IMP替换掉原来的IMP,并返回原来的IMP。
该方法的实现如下:
5、方法的交换:class_replaceMethod
在类中,把方法的IMP交换,可以使用void method_exchangeImplementations(Method m1_gen, Method m2_gen)
.
参数如下:
-
Method m1_gen
:需要交换的一个方法 -
Method m2_gen
:需要交换的另一个方法
使用查询方法,获取到方法的结构(Method
)后,可以调用这个方法,实现方法的交换。
该方法可实现同类中,不同方法交换IMP;也可以实现不同类的方法交换。
同类中的方法交换:
+(void)load{
//以下是交换实例方法的举例
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);
//实现方法交换
method_exchangeImplementations(originalMethod, swizzled_Dog_Method);
}
两个类中的方法交换:
+(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);
//实现方法交换
method_exchangeImplementations(originalMethod, swizzled_Dog_Method);
}
Method Swizzling需要注意的问题
1、需要确保该流程只能执行一次
method-swizzling 多次调用会导致方法的重复交换,无法保证是否达到了最终的交换效果。
如何解决这个问题呢?
在+load
方法中执行,且使用 dispatch_once包裹;这样就可以保证方法交换只执行一次。
使用 dispatch_once包裹的原因在于,防止后续有手动的调用+load
方法的情况,从而导致再次交换了方法。
2、子类没有方法的实现,在父类的方法列表中查到
前面介绍获取Method
的方法class_getInstanceMethod
时,在其方法实现中,可以看出,在查找方法时,如果在类中没有查到。会继续在类的父类中进行查询。当我们要交换方法实现时,并没有交换当前类的方法,而是交换了当前类的父类的实现。
这样当其他地方调用这个父类的方法时,也会调用我们所替换的方法,这显然使我们不想要的。
/*======================================*/
/************--Man的类实现--*************/
/*=====================================*/
@interface Man : NSObject
- (void)stu_SuperMethod;
@end
@implementation Man
- (void)stu_SuperMethod{
NSLog(@"我是 Student 的父类--Man");
}
/*======================================*/
/**********--Student的类实现--***********/
/*=====================================*/
@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);
});
}
//方法调用
- (instancetype)init{
if (self = [super init]) {
//父类的方法调用哦
Man * m = [[Man alloc] init];
[m stu_SuperMethod];
//调用自己的房啊
[self stu_instanceMethod];
//父类的方法调用哦
Man * m1 = [[Man alloc] init];
[m1 stu_SuperMethod];
}
return self;
}
@end
打印结果如下:
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
解决方案:
先通过 class_addMethod 尝试给自己添加要交换的方法,如果添加成功,即表明类中没有这个方法,则通过 class_replaceMethod 进行替换,如果添加失败则说明类中有这个方法,正常进行 method_exchangeImplementations。