method-swizzling 是什么?
Method Swizzling本质上就是对方法的IMP和SEL进行交换,也是我们常说的黑魔法。
方法交换的原理
Method Swizzing是发生在运行时的,在运行时将一个方法的实现替换成另一个方法的实现;每个类都维护着一个方法列表,即
methodList,methodList中有不同的方法,每个方法中包含了方法的SEL和IMP,方法交换就是将原本的SEL和IMP对应断开,并将SEL和新的IMP生成对应关系;Method Swizzling也是iOS中AOP(面相切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。
method-swizzling涉及的相关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 多次调用的混乱问题
mehod-swizzling写在load方法中,而load方法会被系统主动调用多次,这样会导致方法的重复交换,第1次交换,第2次又还原,第3次又交换,这不就乱套了吗?所以,我们得保证方法交换的触发有且仅有1次
解决方案
可以通过单例,使方法交换只执行一次
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//TODO:这里进行你的方法交换
});
}
坑点2:子类无声明无实现,父类有声明有实现
part1: 新建一个LBHPerson类继承与LBHPerson类,在LBHPerson类中有一个personInstanceMethod方法的声明与实现
/**LBHPerson**/
//.h
@interface LBHPerson : NSObject
- (void)personInstanceMethod;
@end
//.m
@implementation LBHPerson
- (void)personInstanceMethod
{
NSLog(@"%s",__func__);
}
@end
/**LBHStudent**/
//.h
@interface LBHStudent : LBHPerson
@end
//.m
@implementation LBHStudent
@end
/**调用**/
//*********调用*********
- (void)viewDidLoad {
[super viewDidLoad];
// 黑魔法坑点二: 子类没有实现 - 父类实现
LGStudent *s = [[LGStudent alloc] init];
[s personInstanceMethod];
LGPerson *p = [[LGPerson alloc] init];
[p personInstanceMethod];
}
part2: 新建一个LBHStudent+Safe
/**LBHStudent分类**/
//.h
@interface LBHStudent (Safe)
@end
//.m
@implementation LBHStudent (Safe)
+ (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);
}
@end
part3: 运行

问题:奔溃了 为啥?
lg_studentInstanceMethod是分类LBHStudent(Safe)的方法,它对应的实现IMP是lg_studentInstanceMethodIMP(仅已这种方法表示) ,在LBHStudent(Safe)的load方法中将方法personInstanceMethod的实现IMP与方法lg_studentInstanceMethod的实现IMP进行交换,即personInstanceMethodSEL -->lg_studentInstanceMethodIMP,lg_studentInstanceMethodSEL -- > personInstanceMethodIMP。
在LBHPerson类的实例对象p调用[p personInstanceMethod]时,此时实际上LBHPerson类的方法实现是lg_studentInstanceMethodIMP,lg_studentInstanceMethodIMP属于子类的分类,所以无法找到。
解决方案
上述崩溃的根本原因是找不到方法的实现imp,假如我们在方法交换时,先判断交换的方法是否有实现imp,未实现则先实现,这样不就解决问题了。
+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
// oriSEL personInstanceMethod
// swizzledSEL lg_studentInstanceMethod
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
// 尝试添加你要交换的方法 - lg_studentInstanceMethod
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);
}
}
在load方法中修改下封装的交换方法
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
解决思路
LBHPerson类的实例对象p,调用方法[p personInstanceMethod]却找不到IMP,所以解决思路是不改变LBHPerson类中实例方法personInstanceMethod的IMP

为了不影响父类方法,可以直接在子类中添加同名方法
// 尝试添加你要交换的方法 - lg_studentInstanceMethod
BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
case1: 添加成功,如下:

子类LBHStudent及其分类中存在两个SEL,指向同一个IMP。
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
使用method_exchangeImplementations会交换失败,用class_replaceMethod会保持原样.

【注意】 oriMethod是临时变量还是最原始的sel和imp,所以在替换方法是传的IMP参数是personInstanceMethodIMP
问题: 替换时IMP指向真的是这样吗?
可以在添加方法前后、替换方法执行后打印下方法IMP的指向
【添加方法前】

oriMethod和swiMethod都是临时变量
【添加方法后,替换方法前】

添加方法成功后,需要重新通过类 cls和方法名 oriSEL获取新的方法,新方法的实现 IMP指向被交换方法swimethod的IMP
【替换方法后】

替换后swiMethod的IMP指向原oriMethod的IMP
case2: 添加不成功,说明已经存在这个方法,可以交换

坑点3:父类有声明无实现,子类无声明无实现
step1: 直接将LBHPerson类中的实现方法注释掉,此时personInstanceMethod在LBHPerson中只有声明没有实现
step2: 运行

递归循环,堆栈溢出
为什么会出现递归?
看下它几个阶段的IMP指向变化
【添加方法前】

【添加方法后,替换方法前】

【替换方法后】

根据IMP几个阶段的变化可以得出SEL和IMP关系图:
【添加方法前】

【添加方法后,替换方法前】

【替换方法后】

在lg_studentInstanceMethod方法的实现里,调用了它本身,由于lg_studentInstanceMethodSEL指向lg_studentInstanceMethodIMP,自己调用自己会产生递归。
解决方案
如果是调用[p personInstanceMethod], 类LBHPerson的实例方法personInstanceMethod没有IMP,必定会崩溃,可以在类及其分类中添加实现、 交换方法或通过消息转发防止崩溃
如果是调用[s personInstanceMethod]
+ (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复制一个不做任何事的空实现,代码如下:
BOOL didAddMethod = class_addMethod(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(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);
}
}
//load方法修改
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_bestMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
看下几个阶段IMP指向变化:
【初始】

【!oriMethod 添加方法】

【设置IMP】

【交换 IMP】

由于oriMethod没有IMP,所以交换方法失败,方法的SEL指向的IMP都没有变化。
根据IMP几个阶段的变化可以得出SEL和IMP关系图:
【初始】

【!oriMethod 添加方法】

【设置IMP】

【交换 IMP】

为什么要给
swiMethod lg_studentInstanceMethod设置一个IMP?
会产生递归
如果不设置一个IMP,lg_studentInstanceMethod还是指向原始IMP 即lg_studentInstanceMethod--> lg_studentInstanceMethodIMP ,lg_studentInstanceMethod方法实现中会调用自己产生递归。

method-swizzling - 类方法
类方法和实例方法的method-swizzling的原理是类似的,唯一的区别是类方法存在元类中,所以可以做如下操作
step1: 在LBHStudent中添加一个类方法,只声明bu实现
//.h
@interface LBHStudent : LBHPerson
+ (void)classMethod;
@end
//.m
@implementation LBHStudent
@end
step2: LBHStudent的分类需要修改
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_bestClassMethodSwizzlingWithClass:self oriSEL:@selector(classMethod) swizzledSEL:@selector(lg_studentClassMethod)];
});
}
+ (void)lg_studentClassMethod{
NSLog(@"LGStudent分类添加的lg类方法:%s",__func__);
[[self class] lg_studentClassMethod];
}
step3: 封装方法修改
//封装的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的应用
在项目开发过程中,经常碰到NSArray数组越界或者NSDictionary的key或者value值为nil等问题导致的crash,对于这些问题苹果爸爸并不会报一个警告,而是直接崩溃,不给你任何机会补救。
因此,我们可以根据这个方法交换,对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类进行Method Swizzling,发现越界或值为nil时,做一些补救措施,实现方式还是按照上面的例子来做。但是,你发现Method Swizzling根本就不起作用。
为什么?因为它们是类簇,Method Swizzling对类簇不起作用
下面列举了NSArray和NSDictionary本类的类名,可以通过Runtime函数取出本类。
| 类名 | 真身 |
|---|---|
| NSArray | __NSArrayI |
| NSMutableArray | __NSArrayM |
| NSDictionary | __NSDictionaryI |
| NSMutableDictionary | __NSDictionaryM |
测试
以NSArray为例
@implementation NSArray (Safe)
//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
+ (void)load{
// Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lbh_objectAtIndexedSubscript:));
method_exchangeImplementations(fromMethod, toMethod);
}
//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
- (id)lbh_objectAtIndexedSubscript:(NSUInteger)index{
//判断下标是否越界,如果越界就进入异常拦截
if (self.count-1 < index) {
// 这里做一下异常处理,不然都不知道出错了。
//#ifdef DEBUG // 调试阶段
// return [self lbh_objectAtIndexedSubscript:index];
//#else // 发布阶段
@try {
return [self lbh_objectAtIndexedSubscript: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 lbh_objectAtIndexedSubscript:index];
}
}
@end
调用
NSArray *array = @[@"1",@"2",@"3"];
NSLog(@"== %@",array[3]);
运行结果
