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]);
运行结果