了解Runtime
的同学应该都听说过或者使用过Method-Swizzling
,今天我们就来一起了解下Method-Swizzling
的使用以及坑点。
一、Method-Swizzling
的使用
新建工程,创建一个工程,创建一个LPPerson
继承于NSObject
,和继承于LPPerson
的LPMan
类,再创建一个LPMan
的类别,和一个runtime
的工具类:
LPPerson
类:
@interface LPPerson : NSObject
- (void)personInstanceMetheod;
+ (void)personClassMetheod;
@end
@implementation LPPerson
- (void)personInstanceMetheod{
NSLog(@"%s",__func__);
}
+ (void)personClassMetheod{
NSLog(@"%s",__func__);
}
@end
LPMan
类:
/// LPPerson的子类
@interface LPMan : LPPerson
@end
@implementation LPMan
@end
LPMan
的类别:
////类别
@interface LPMan (MS)
@end
@implementation LPMan (MS)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LPRunTimeTool lp_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMetheod) swizzledSEL:@selector(lp_manInstanceMethod)];
}
- (void)lp_manInstanceMethod{
[self lp_manInstanceMethod]; //lg_studentInstanceMethod -/-> personInstanceMethod
NSLog(@"LPMan分类添加的lg对象方法:%s",__func__);
}
@end
工具类:
@interface LPRunTimeTool : NSObject
/**
交换方法
@param cls 交换对象
@param oriSEL 原始方法编号
@param swizzledSEL 交换的方法编号
*/
+ (void)lp_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL;
@end
#import <objc/runtime.h>
@implementation LPRunTimeTool
+ (void)lp_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
ok,接下来,我们在Viewcontroller
中完成调用:
- (void)viewDidLoad {
[super viewDidLoad];
// 黑魔法坑点二: 子类没有实现 - 父类实现
LPMan *s = [[LPMan alloc] init];
[s personInstanceMetheod];
}
运行查看结果:
2020-10-25 14:51:17.067194+0800 Method-SwizzlingTest[55258:1422381] -[LPPerson personInstanceMetheod]
2020-10-25 14:51:17.067288+0800 Method-SwizzlingTest[55258:1422381] LPMan分类添加的lg对象方法:-[LPMan(MS) lp_manInstanceMethod]
结果是正确的,我们来分析下为什么是正确的:
- 1:结合我们前面学习的知识,知道
dyld
链接过程中,会先调用LPMan
类别的load
方法,load
方法中执行LPRunTimeTool
的lp_methodSwizzlingWithClass:self
方法 - 2:
lp_methodSwizzlingWithClass:self
里面调用了系统的method_exchangeImplementations
方法,已经将LPPerson
的personInstanceMetheod
的imp
和LPMan
的lp_manInstanceMethod
的imp
实现了交换 - 3:
LPMan
的对象,调用父类的personInstanceMetheod
方法时,实际是执行的LPMan
的lp_manInstanceMethod
方法, - 4:
lp_manInstanceMethod
执行时,先自己调用了一次lp_manInstanceMethod
,此时实际上是执行的LPPerson
的personInstanceMetheod
方法,所以会打印第一次 - 5:然后再继续执行
lp_manInstanceMethod
里面,所以会打印第二次
二、坑点
坑点1:
我们在Viewcontroller
中创建一个LPPerson
,并且去调用他自己的personInstanceMetheod
:
- (void)viewDidLoad {
[super viewDidLoad];
LPMan *s = [[LPMan alloc] init];
[s personInstanceMetheod];
LPPerson *p = [[LPPerson alloc] init];
[p personInstanceMetheod];
}
运行查看结果:
2020-10-25 15:09:45.518448+0800 Method-SwizzlingTest[55464:1432664] -[LPPerson personInstanceMetheod]
2020-10-25 15:09:45.518581+0800 Method-SwizzlingTest[55464:1432664] LPMan分类添加的lg对象方法:-[LPMan(MS) lp_manInstanceMethod]
2020-10-25 15:09:45.518656+0800 Method-SwizzlingTest[55464:1432664] -[LPPerson lp_manInstanceMethod]: unrecognized selector sent to instance 0x600000004520
2020-10-25 15:09:45.518880+0800 Method-SwizzlingTest[55464:1432664] Failed to set (contentViewController) user defined inspected property on (NSWindow): -[LPPerson lp_manInstanceMethod]: unrecognized selector sent to instance 0x600000004520
可以看到,s
调用personInstanceMetheod
的时候,没有问题,但是p调用personInstanceMetheod
的时候就崩溃了,这是因为什么呢?
LPMan
的对象调用personInstanceMetheod
的时候,内部已经完成了方法交换,所以这一步是不会有问题的,但是LPPerson
的对调用personInstanceMetheod
的时候,因为已经完成了方法交换,实际上是调用的LPMan
的lp_manInstanceMethod
方法,但是对于LPPerson
本身来说,它并没有这个方法,这个时候LPPerson
就会沿着它的继承链一直到找到nil
,但是都没有,所以就会直接崩溃。
那么我们怎么解决呢?
既然报错是因为LPPerson
没有这个方法,那如果它有了不就可以了吗?所以我们就手动给它加上,在LPRuntimeTool
中添加方法:
+ (void)lp_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL;
+ (void)lp_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);
}
}
然后在LPMan
的分类中调用:
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LPRunTimeTool lp_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMetheod) swizzledSEL:@selector(lp_manInstanceMethod)];
});
}
运行,查看结果:
2020-10-25 15:23:46.527062+0800 Method-SwizzlingTest[55557:1439736] -[LPPerson personInstanceMetheod]
2020-10-25 15:23:46.527158+0800 Method-SwizzlingTest[55557:1439736] LPMan分类添加的lg对象方法:-[LPMan(MS) lp_manInstanceMethod]
2020-10-25 15:23:46.527203+0800 Method-SwizzlingTest[55557:1439736] -[LPPerson personInstanceMetheod]
dangdang,完美。我们再分析下lp_betterMethodSwizzlingWithClass:
方法:
- 1.首先判断当前类,如果类都不存在,后面完全没有必要进行了,所以直接
return
- 2.给当前类是添加原始
sel
,但是用的是交换的sel
的imp
,这一步主要是当前类是否存在原始的sel
,如果自己有,则添加不成功返回NO
,如果自己没有,但是父类有,或者其继承链的的其他类有,则添加成功,返回YES
。根据我们前面学到的知识,可以知道,子类重写父类的方法,在方法列表中会放在最前面。 - 3.判断添加的结果:
- 1).如果添加成功:说明之前类没有实现,所以我们直接使用
class_replaceMethod
,使用需要原始方法的imp
替换了需要交换方法的imp
。即oriSEL
和swiMethod
的imp
,swizzledSEL
和oriMethod
的imp
这样也就完成了方法交换。此时,LPPerson
调用personInstanceMetheod
,还是personInstanceMetheod
的imp
,所以不会报错。 - 2).如果添加不成功,说明自己本身有了,所以直接交换即可。此时,
LPPerson
调用personInstanceMetheod
就是我们之前分析的过程,也不会报错。
- 1).如果添加成功:说明之前类没有实现,所以我们直接使用
坑点2:
接下来,我们将LPPerson
中personInstanceMetheod
的实现注释掉,以及Viewcontroller
中LPPerson
的代码也注释掉:
@implementation LPPerson
//- (void)personInstanceMetheod{
// NSLog(@"%s",__func__);
//}
+ (void)personClassMetheod{
NSLog(@"%s",__func__);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
// 黑魔法坑点二: 子类没有实现 - 父类实现
LPMan *s = [[LPMan alloc] init];
[s personInstanceMetheod];
// personInstanceMethod -> lg_studentInstanceMethod
// LPPerson *p = [[LPPerson alloc] init];
// [p personInstanceMetheod];
}
再次运行看下结果:
image.png
可以看到,这个时候在lp_manInstanceMethod
方法发生了死循环,导致崩溃了,但是为什么会造成死循环呢?
这是因为在方法交换的时候,因为LPPerson
中没有实现personInstanceMetheod
,所以获取到的 oriMethod
就是nil
,这时候去交换方法,结果就是lp_manInstanceMethod
的imp
并没有交换给personInstanceMetheod
,所以在lp_manInstanceMethod
中再调用lp_manInstanceMethod
,实际就是递归了,所以就发生了死循环。
既然是因为oriMethod
没有,那我们就判断下,如果没有就给它添加上不就行了吗?在LPRunTimeTool
中继续添加如下方法,并在LPMan
中调用:
+ (void)lp_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){
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);
}
}
再次运行:
2020-10-25 16:08:42.852133+0800 Method-SwizzlingTest[56249:1537593] 来了一个空的 imp
2020-10-25 16:08:42.852205+0800 Method-SwizzlingTest[56249:1537593] LPMan分类添加的lg对象方法:-[LPMan(MS) lp_manInstanceMethod]
可以看到,死循环的问题,已经解决了。
我们来分析下lp_bestMethodSwizzlingWithClass
中代码:对比lp_betterMethodSwizzlingWithClass:
主要就是增加对oriMethod
的判断,如果为空,我们则手动添加一个方法,并且通过method_setImplementation
重设它的imp
。在这里,你可以完成很多的事情,比如说错误上报啊等等。
三、类方法的交换
其实通过实例方法的交换以及我们前面知道继承链关系,类方法的交换和实例方法的交换非常类似,唯一的区别是类方法存在元类中。所以,同样的,我们也可以构造一个类方法的交换方法:
+ (void)lp_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
可以做非常多的事情,比如崩溃拦截等等,感兴趣的同学可以自己去探索哦!
网友评论