方法交换
1. 方法交换的原理
每个继承于NSObject
的类都可以获得runtime
的支持,在这样一个类中,有一个isa指针,指针指向该类定义的结构体,这个结构体中包括,指向其父类定义的指针以及Dispatch table
,Dispatch table
是一张SEL
和IMP
的对应表。
也就是说需要通过方法编号SEL
最后还有通过Dispatch table
寻找对应的IMP
,IMP
是函数指针,然后执行这个方法。
方法编号SEL和实现方法IMP的对应关系:
方法交换后的对应关系:
oriSEL
的方法实现变成了swiIMP
;
swiSEL
的方法实现变成了oriIMP
;
也就是调用oriSEL
方法,最终方法实现是swiIMP
。
方法交换的方式:
objective-c
// 类中获取oriSEL对应的方法实现
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
// 获取swiSEL对应的方法实现
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
// 将两个方法实现进行交换,
method_exchangeImplementations(oriMethod, swiMethod);
再进行方法交换操作,建议在单例模式下进行,避免重复调用交换。
2. 方法交换案例
1)递归问题分析
创建一个LGStudent类,类中有两个方法lg_studentInstanceMethod
和studentInstanceMethod
,在load方法对这两个方法进行交换,同时lg_studentInstanceMethod
的实现中再次调用lg_studentInstanceMethod
方法。
objective-c
#import "ViewController.h"
#import "LGStudent.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
LGStudent *s = [[LGStudent alloc] init];
[s studentInstanceMethod];
}
@end
#import "LGStudent.h"
#import <objc/runtime.h>
@implementation LGStudent
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"方法交换---:%s", __func__);
Method oriIMP = class_getInstanceMethod(self, @selector(studentInstanceMethod));
Method swiIMP = class_getInstanceMethod(self, @selector(lg_studentInstanceMethod));
method_exchangeImplementations(oriIMP, swiIMP);
});
}
- (void)lg_studentInstanceMethod {
[self lg_studentInstanceMethod];
NSLog(@"LGStudent对象方法:%s", __func__);
}
- (void)studentInstanceMethod {
NSLog(@"LGStudent对类方法:%s", __func__);
}
@end
在lg_studentInstanceMethod
中再次调用该方法,是否会引起递归调用?
运行结果:
**分析:**没有引起递归,因为进行了方法交换,所以调用对象方法studentInstanceMethod
,会找到lg_studentInstanceMethod
的方法实现,而lg_studentInstanceMethod
中调用lg_studentInstanceMethod
,而此时该方法的指向已经指向studentInstanceMethod
。如下图所示:
2)交换父类方法
创建一个LGSstudent
类,类中有一个实例方法,lg_studentInstanceMethod
,其父类LGPerson
中有一个实例方法personInstanceMethod
,在LGStudent
类的load
方法中对进行方法交换,将lg_studentInstanceMethod
方法交换成父类中的personInstanceMethod
方法。
objective-c
#import "ViewController.h"
#import "LGStudent.h"
#import "LGPerson.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
LGPerson *p = [[LGPerson alloc] init];
[p personInstanceMethod];
}
@end
#import "LGPerson.h"
@implementation LGPerson
- (void)personInstanceMethod{
NSLog(@"person对象方法:%s",__func__);
}
@end
#import "LGStudent.h"
#import <objc/runtime.h>
@implementation LGStudent
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"方法交换---:%s", __func__);
Method oriIMP = class_getInstanceMethod(self, @selector(personInstanceMethod));
Method swiIMP = class_getInstanceMethod(self, @selector(lg_studentInstanceMethod));
method_exchangeImplementations(oriIMP, swiIMP);
});
}
- (void)lg_studentInstanceMethod {
[self lg_studentInstanceMethod];
NSLog(@"LGStudent对象方法:%s", __func__);
}
运行结果:
**分析:**成功调用,子类调用父类的方法personInstanceMethod
,通过消息发送的原理,会进行慢速查找,找到父类方法,此时父类方法的实现变成了子类的lg_studentInstanceMethod
方法,在子类中调用lg_studentInstanceMethod
方法,最终的实现的方法为父类的personInstanceMethod
。
如果调用父类对象的personInstanceMethod
方法?
objective-c
#import "ViewController.h"
#import "LGStudent.h"
#import "LGPerson.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
LGPerson *p = [[LGPerson alloc] init];
[p personInstanceMethod];
}
@end
#import "LGPerson.h"
@implementation LGPerson
- (void)personInstanceMethod{
NSLog(@"person对象方法:%s",__func__);
}
@end
#import "LGStudent.h"
#import <objc/runtime.h>
@implementation LGStudent
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"方法交换---:%s", __func__);
Method oriIMP = class_getInstanceMethod(self, @selector(personInstanceMethod));
Method swiIMP = class_getInstanceMethod(self, @selector(lg_studentInstanceMethod));
method_exchangeImplementations(oriIMP, swiIMP);
});
}
- (void)lg_studentInstanceMethod {
[self lg_studentInstanceMethod];
NSLog(@"LGStudent对象方法:%s", __func__);
}
运行结果:
**分析:**报错,父类调用personInstanceMethod
方法,执行的是子类lg_studentInstanceMethod
实现,并且同时调用lg_studentInstanceMethod
方法,但是父类中并没有lg_studentInstanceMethod
的实现,找不到这个方法,所以报错。
如果进行方法交换,一定要确保方法已经实现,否则会出现本例中啃爹的现象(方法交换,而父类没有方法的实现,导致报错)。所以在进行相关方法交换时,尽量避免涉及到其父类或者其子类的方法。
3. 方法交换思路
为避免上面案例的问题,总结以下实现思路:
为避免上面案例的问题,总结以下实现思路:
objective-c
+ (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时,添加oriSEL的方法,实现为swiMethod
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
// 替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"来了一个空的 imp");
}));
}
// 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
// 交换自己没有实现的方法:
// 首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
// 然后再将父类的IMP给swizzle personInstanceMethod(imp) -> swizzledSEL
//oriSEL:personInstanceMethod
// 向类中添加oriSEL方法,方法实现为swiMethod
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
// 自己有意味添加方法失败-所以这里会是false
if (didAddMethod) {
// 如果添加成功,表示原本没有oriMethod方法,此时将swizzledSEL的方法实现,替换成oriMethod实现
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
// 方法交换
method_exchangeImplementations(oriMethod, swiMethod);
}
}