Method Swizzling 是什么?
-
Runtime 中的 Method Swizzling 被称为黑魔法,它是 OC 的
运行时特性,主要作用包括:交换类中某两个方法的实现、重新添加或替换某个方法的具体实现。 -
在OC中就是
利用method-swizzling实现AOP,其中AOP(Aspect Oriented Programming,面向切面编程)是一种编程的思想,区别于OOP(面向对象编程)- OOP和AOP都是一种编程的思想
OOP编程思想更加倾向于对业务模块的封装,划分出更加清晰的逻辑单元;- 而
AOP是面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性。
-
每个类都维护着一个
方法列表,即methodList,methodList中有不同的方法即Method,每个方法中包含了方法的sel和IMP,方法交换就是将sel和imp原本的对应断开,并将sel和新的IMP生成对应关系
如下图所示,交换前后的sel和IMP的对应关系:
运行时的几种特殊类型:
Class: 类名通过类的 class 类方法获得,例如:[UIButton class]。SEL: 选择器也就是方法名,通过@selector(方法名:)获得,例如:@selector(buttonClicked:)。Method: 方法即运行时类中定义的方法,包括方法名(SEL)和方法实现(IMP)两部分,通过运行时方法class_getInstanceMethod或class_getClassMethod获得。IMP: 方法实现类型指方法的实现部分,通过运行时方法class_getMethodImplementation或method_getImplementation获得。
Method Swizzling 涉及的相关API
- 通过
sel获取方法Methodclass_getInstanceMethod:获取实例方法class_getClassMethod:获取类方法
method_getImplementation:获取一个方法的实现method_setImplementation:设置一个方法的实现method_getTypeEncoding:获取方法实现的编码类型class_addMethod:添加新方法实现class_replaceMethod:用一个方法的实现,替换另一个方法的实现,即aIMP 指向 bIMP,但是bIMP不一定指向aIMP。(注:这种方法只能替换实例方法实现,而不能替换类方法的实现)method_exchangeImplementations:交换两个方法的实现,即 aIMP -> bIMP,bIMP -> aIMP
通过下面这个 Demo 可以了解下 Method Swizzling 相关 API 的使用。
@interface MPTest : NSObject
- (void)instanceMethod1;
- (void)instanceMethod2;
- (void)newInsMethod;
+ (void)classMethod1;
+ (void)classMethod2;
@end
#import <objc/runtime.h>
@implementation MPTest
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 获取当前类名
Class class = [self class];
// 获取方法名(选择器)
SEL selInsMethod1 = @selector(instanceMethod1);
SEL selInsMethod2 = @selector(instanceMethod2);
SEL selClassMethod1 = @selector(classMethod1);
SEL selClassMethod2 = @selector(classMethod2);
SEL selNewInsMethod = @selector(newInsMethod);
// 根据方法名获取方法对象
Method insMethod1 = class_getInstanceMethod(class, selInsMethod1);
Method insMethod2 = class_getInstanceMethod(class, selInsMethod2);
Method classMethod1 = class_getClassMethod(class, selClassMethod1);
Method classMethod2 = class_getClassMethod(class, selClassMethod2);
// 获取方法的实现
IMP impInsMethod1 = method_getImplementation(insMethod1);
IMP impInsMethod2 = method_getImplementation(insMethod2);
IMP impClassMethod1 = method_getImplementation(classMethod1);
IMP impClassMethod2 = method_getImplementation(classMethod2);
// 获取方法编码类型
const char *typeInsMethod1 = method_getTypeEncoding(insMethod1);
const char *typeInsMethod2 = method_getTypeEncoding(insMethod1);
const char *typeClassMethod1 = method_getTypeEncoding(classMethod1);
const char *typeClassMethod2 = method_getTypeEncoding(classMethod2);
// 判断是否为空
if (!insMethod1 || !insMethod2) {
NSLog(@"实例方法实现运行时交换 fail");
return;
}
// 交换实例方法的实现
// method_exchangeImplementations(insMethod1, insMethod2);
// 重新设置方法的实现
// method_setImplementation(insMethod1, impInsMethod2);
// method_setImplementation(insMethod2, impInsMethod1);
// 实例方法有效
class_replaceMethod(class, selInsMethod1, impInsMethod2, typeInsMethod2);
class_replaceMethod(class, selInsMethod2, impInsMethod1, typeInsMethod1);
// 判断是否为空
if (!classMethod1 || !classMethod2) {
NSLog(@"类方法实现运行时交换 fail");
return;
}
// 交换类方法的实现
// method_exchangeImplementations(classMethod1, classMethod2);
// method_setImplementation(classMethod1, impClassMethod2);
// method_setImplementation(classMethod2, impClassMethod1);
// 类方法无效
class_replaceMethod(class, selClassMethod1, impClassMethod2, typeClassMethod2);
class_replaceMethod(class, selClassMethod2, impClassMethod1, typeClassMethod1);
BOOL isInsAdded = class_addMethod(class, selNewInsMethod, impInsMethod1, typeInsMethod1);
if (!isInsAdded) {
NSLog(@"新实例方法添加失败");
}
});
}
- (void)instanceMethod1 {
NSLog(@"instanceMethod1");
}
- (void)instanceMethod2 {
NSLog(@"instanceMethod2");
}
+ (void)classMethod1 {
NSLog(@"classMethod1");
}
+ (void)classMethod2 {
NSLog(@"classMethod2");
}
@end
测试代码:
// [MSTest classMethod1];
// [MSTest classMethod2];
MSTest *test = [[MSTest alloc] init];
// [test instanceMethod1];
// [test instanceMethod2];
[test newInsMethod];
坑点1:method-swizzling 使用过程中的一次性问题
所谓的一次性就是:mehod-swizzling写在load方法中,而load方法会主动调用多次,这样会导致方法的重复交换,使方法sel的指向又恢复成原来的imp的问题。
解决方案
可以通过单例设计原则,使方法交换只执行一次,在OC中可以通过dispatch_once实现单例。
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self kc_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(myDealloc)];
});
}
坑点2:子类没有实现,父类实现了。
在下面这段代码中,LGPerson中实现了personInstanceMethod,而LGStudent继承自LGPerson,没有实现personInstanceMethod,运行下面这段代码会出现什么问题?
//*********LGPerson类*********
@interface LGPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LGPerson
- (void)personInstanceMethod{
NSLog(@"person对象方法:%s",__func__);
}
@end
//*********LGStudent类*********
@interface LGStudent : LGPerson
- (void)helloword;
+ (void)sayHello;
@end
@implementation LGStudent
@end
//*********调用*********
- (void)viewDidLoad {
[super viewDidLoad];
// 黑魔法坑点二: 子类没有实现 - 父类实现
LGStudent *s = [[LGStudent alloc] init];
[s personInstanceMethod];
LGPerson *p = [[LGPerson alloc] init];
[p personInstanceMethod];
}
其中,方法交换代码如下,是通过LGStudent的分类LG实现。
@implementation LGStudent (LG)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
// personInstanceMethod 我需要父类的这个方法的一些东西
// 给你加一个personInstanceMethod 方法
// imp
- (void)lg_studentInstanceMethod{
////是否会产生递归?--不会产生递归,原因是lg_studentInstanceMethod 会走 oriIMP,即personInstanceMethod的实现中去
[self lg_studentInstanceMethod];
NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);
}
@end
下面是封装好的method-swizzling方法。
@implementation LGRuntimeTool
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) {
NSLog(@"传入的交换类不能为空");
return;
}
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(oriMethod, swiMethod);
}
通过实际代码的调试,发现会在 p 调用personInstanceMethod方法时崩溃.
[s personInstanceMethod];中不报错是因为student中的imp交换成lg_studentInstanceMethod,而LGStudent中有这个方法(在LG分类中),所以不会报错。- 崩溃的点在于
[p personInstanceMethod];,其本质原因:LGStudent的分类LG中进行了方法交换,将person中imp交换成了LGStudent中的lg_studentInstanceMethod,然后需要去LGPerson中的找lg_studentInstanceMethod,但是LGPerson中没有lg_studentInstanceMethod方法,即相关的imp找不到,所以就崩溃啦。
解决方案
优化:避免 IMP 找不到
通过class_addMethod尝试添加你要交换的方法
- 如果
添加成功,即类中没有这个方法,则通过class_replaceMethod进行替换,其内部会调用class_addMethod进行添加。 - 如果添加不成功,即类中有这个方法,则通过
method_exchangeImplementations进行交换。
+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
if (!cls) {
NSLog(@"传入的交换类不能为空");
return;
}
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
// 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
// 交换自己没有实现的方法:
// 首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
// 然后再将父类的IMP给swizzle personInstanceMethod(imp) -> swizzledSEL
BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) {// 自己没有 - 交换 - 没有父类进行处理 (重写一个)
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else { // 自己有
method_exchangeImplementations(oriMethod, swiMethod);
}
}
坑点3:子类没有实现,父类也没有实现,下面的调用有什么问题?
在调用personInstanceMethod方法时,父类LGPerson中只有声明,没有实现,子类LGStudent中既没有声明,也没有实现。
//*********LGPerson类*********
@interface LGPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LGPerson
@end
//*********LGStudent类*********
@interface LGStudent : LGPerson
- (void)helloword;
+ (void)sayHello;
@end
@implementation LGStudent
@end
//*********调用*********
- (void)viewDidLoad {
[super viewDidLoad];
// 黑魔法坑点二: 子类没有实现 - 父类实现
LGStudent *s = [[LGStudent alloc] init];
[s personInstanceMethod];
LGPerson *p = [[LGPerson alloc] init];
[p personInstanceMethod];
}
经过调试,发现运行代码会崩溃,报错结果如下所示:
原因是栈溢出,递归死循环,那么为什么会发生递归呢?
主要是因为 personInstanceMethod没有实现,然后在方法交换时,始终都找不到oriMethod,即交换失败,当我们调用personInstanceMethod(oriMethod)时,也就是oriMethod会进入 LG 中lg_studentInstanceMethod方法,然后这个方法中又调用了lg_studentInstanceMethod,此时的lg_studentInstanceMethod并没有指向oriMethod ,然后导致了自己调自己,即递归死循环。
解决方案
优化:避免递归死循环
- 如果
oriMethod为空,为了避免方法交换没有意义,而被废弃,需要做一些事情- 通过
class_addMethod给oriSEL添加swiMethod方法 - 通过
method_setImplementation将swiMethod的IMP指向不做任何事的空实现
- 通过
+ (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) {
NSLog(@"传入的交换类不能为空");
return;
}
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!oriMethod) {
// 在oriMethod为nil时,替换后将 swizzledSEL 复制一个不做任何事的空实现,代码如下:
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}
// 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
// 交换自己没有实现的方法:
// 首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
// 然后再将父类的IMP给swizzle personInstanceMethod(imp) -> swizzledSEL
//oriSEL:personInstanceMethod
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
}
method-swizzling - 类方法
类方法和实例方法的method-swizzling的原理是类似的,唯一的区别是类方法存在元类中,所以可以做如下操作
LGStudent中只有类方法sayHello的声明,没有实现。
@interface LGStudent : LGPerson
- (void)helloword;
+ (void)sayHello;
@end
@implementation LGStudent
@end
- 在
LGStudent的分类的load方法中实现类方法的方法交换。
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_bestClassMethodSwizzlingWithClass:self oriSEL:@selector(sayHello) swizzledSEL:@selector(lg_studentClassMethod)];
});
}
+ (void)lg_studentClassMethod{
NSLog(@"LGStudent分类添加的lg类方法:%s",__func__);
[[self class] lg_studentClassMethod];
}
封装的类方法的方法交换如下:
- 需要通过
class_getClassMethod方法获取类方法 - 在调用
class_addMethod和class_replaceMethod方法添加和替换时,需要传入的类是元类,元类可以通过object_getClass方法获取类的元类。
//封装的method-swizzling方法
+ (void)lg_bestClassMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getClassMethod([cls class], oriSEL);
Method swiMethod = class_getClassMethod([cls class], swizzledSEL);
if (!oriMethod) { // 避免动作没有意义
// 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"来了一个空的 imp");
}));
}
// 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
// 交换自己没有实现的方法:
// 首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
// 然后再将父类的IMP给swizzle personInstanceMethod(imp) -> swizzledSEL
//oriSEL:personInstanceMethod
BOOL didAddMethod = class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(object_getClass(cls), swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
}
Method Swizzling 的应用
method-swizzling 最常用的应用是防止数组、字典等越界崩溃
在 iOS 中NSNumber、NSArray、NSDictionary等这些类都是类簇,一个NSArray的实现可能由多个类组成。所以如果想对 NSArray 进行 Swizzling,必须获取到其“真身”进行 Swizzling,直接对 NSArray 进行操作是无效的。
下面列举了 NSArray 和 NSDictionary 本类的类名,可以通过 Runtime 函数取出本类。
| 类名 | 真身 |
|---|---|
| NSArray | __NSArrayI |
| NSMutableArray | __NSArrayM |
| NSDictionary | __NSDictionaryI |
| NSMutableDictionary | __NSDictionaryM |
以 NSArray 为例
- 创建 NSArray 的一个分类
MPArray
@implementation NSArray (MPArray)
// 如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
+ (void)load{
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(mp_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
}
// 如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
- (id)mp_objectAtIndex:(NSUInteger)index{
// 判断下标是否越界,如果越界就进入异常拦截
if (self.count-1 < index) {
// 这里做一下异常处理,不然都不知道出错了。
@try {
return [self mp_objectAtIndex:index];
} @catch (NSException *exception) {
// 在崩溃后会打印崩溃信息,方便我们调试。
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
return nil;
} @finally {
}
} else{ // 如果没有问题,则正常进行方法调用
return [self mp_objectAtIndex:index];
}
}
@end
- 测试代码:
NSArray *array = @[@"1", @"2", @"3"];
[array objectAtIndex:3];
- 打印结果如下,会输出崩溃的日志,但是实际并不会崩溃。