什么是Method Swizzling
-
Method Swizzling
就是方法交换
,俗称黑魔法
,主要是在运行时将一个方法的实现替换成另外一个方法的实现
- 在OC中我们可以利用
Method Swizzling
来实现AOP(切面编程)
-
OOP
:面向对象编程-->针对业务处理过程的实体及其属性和行为进行抽象封装,划分出更加清晰的逻辑单元 -
AOP
:面向切面编程-->针对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。主要的功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等。
-
- 每个类都有自己的一个
方法列表
,即methodList
,每个方法中都包含了方法的sel
和imp
,方法交换就是将原来sel和imp的关系断开,将sel和新的imp建立新的关系
常用的API
-
class_getInstanceMethod
:获取实例方法 -
class_getClassMethod
:获取类方法 -
method_getImplementation
:获取一个方法的实现 -
method_setImplementation
:设置一个方法的实现 -
method_getTypeEncoding
:获取方法实现的编码类型 -
class_addMethod
:添加方法 -
class_replaceMethod
:替换方法,用一个方法的实现,替换另一个方法的实现,即aIMP 指向 bIMP,但是bIMP不一定指向aIMP -
method_exchangeImplementations
:交换两个方法的实现,即 aIMP -> bIMP, bIMP -> aIMP
【问题1】method-swizzling使用过程中重复交换
假如在load
中实现了method-swizzling
,由于load
方法的多次调用,这样会导致方法的重复交换
,使方法的sel又重新指向原来的imp
解决方案
通过单例
来确保方法只被交换一次
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_bestMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
【问题2】交换的方法子类没有实现,而父类实现了
运行下面这段代码会出现什么问题?
//*********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];
}
我们通过分类来实现方法交换,和封装好的方法交换类
@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
//封装好的方法交换类
@implementation LGRuntimeTool
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(oriMethod, swiMethod);
}
运行上面代码,会发现在子类
调用personInstanceMethod
方法时会崩溃
LGStudent
子类调用不报错是因为:student
中的imp
交换成了lg_studentInstanceMethod
,在LGStudent分类中有lg_studentInstanceMethod
的实现,所以不会奔溃LGPerson
父类调用报错是因为:我们是在LGStudent分类中进行的方法交换,将person
的imp
交换成了LGStudent的lg_studentInstanceMethod
,这样我们就需要去LGPerson的方法列表中去查找lg_studentInstanceMethod
,但是LGPerson没有该方法(imp找不到
)奔溃
解决方案
我们可以通过class_addMethod
尝试添加你要交换的方法,来判断当前类是否有这样方法
-
添加成功
--类中没有这个方法,通过class_replaceMethod
进行方法替换
,因为class_replaceMethod
内部会自动调用class_addMethod
进行添加 -
添加失败
--类中有这个方法,通过method_exchangeImplementations
进行方法交换
+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
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);
}
}
下面是class_replaceMethod
、class_addMethod
、method_exchangeImplementations
的源码实现
其中class_replaceMethod
和class_addMethod
中都调用了addMethod
方法,区别在于bool
值的判断,下面是addMethod
的源码实现
【问题3】:子类和父类都没有实现改方法,下面的调用有什么问题?
//*********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_studentInstanceMethod
方法,然而lg_studentInstanceMethod
方法中又调用了lg_studentInstanceMethod
,但是现在lg_studentInstanceMethod
并没有指向oriMethod
,导致自己调用自己
直接递归死循环
解决方案
- 通过
class_addMethod
给oriSEL
添加swiMethod
方法 - 通过
method_setImplementation
将swiMethod
的IMP
指向空实现的方法
+ (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
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 --- 类方法
在分类的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的应用
最常用的应用是防止数组、字典等越界崩溃
在iOS 中NSNumber
、NSArray
、NSDictionary
这些类都是类簇,例如NSArray
的实现是由多个类组成的
,所有如果想对NSArray进行方法交换,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的
NSArray方法交换
创建NSArray的一个分类YP
@implementation NSArray (YP)
//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
+ (void)load{
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(yp_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
}
//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
- (id)yp_objectAtIndex:(NSUInteger)index{
//判断下标是否越界,如果越界就进入异常拦截
if (self.count-1 < index) {
// 这里做一下异常处理,不然都不知道出错了。
#ifdef DEBUG // 调试阶段
return [self yp_objectAtIndex:index];
#else // 发布阶段
@try {
return [self yp_objectAtIndex:index];
} @catch (NSException *exception) {
// 在崩溃后会打印崩溃信息,方便我们调试。
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
return nil;
} @finally {
}
#endif
}else{ // 如果没有问题,则正常进行方法调用
return [self yp_objectAtIndex:index];
}
}
@end