什么是Method Swizzling
实际上是方法交换。OC是个运行时语言,允许我们运行时修改方法,可以进行方法替换、交换等操作。方法交换实际就是互相替换方法的实现。以下是方法Method的底层结构:
struct method_t {
SEL name;
IMP imp;
const char *types;
}
方法交换实际就是底层imp互相替换。关于方法替换可以参考官方文档。接下来要讨论的是方法交换在使用过程中应该注意的一些问题。
子类方法和父类方法替换导致父类调用异常
首先创建一个demo。自定义一个类Animal和继承自Animal的子类Dog。父类有个实例方法parentInstanceMethod,子类有个方法childInstanceMethod,对这两个方法进行替换,从中总结出一些问题。先来看一下demo。
interface部分:
@interface Animal : NSObject
- (void)parentInstanceMethod;
@end
@interface Dog : Animal
@end
@interface Dog(Addition)
- (void)childInstanceMethod;
@end
implementation部分:
@implementation Animal
- (void)parentInstanceMethod
{
NSLog(@"%s", __func__);
}
@end
@implementation Dog
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[Util my_instanceMethodSwizzlingWithClass:[self class] oriSel:@selector(parentInstanceMethod) swizzSel:@selector(childInstanceMethod)];
});
}
@end
@implementation Dog(Addition)
- (void)childInstanceMethod
{
[self childInstanceMethod];
NSLog(@"%s", __func__);
}
@end
方法交换(Method Swizzling)代码逻辑:
+ (void)my_instanceMethodSwizzlingWithClass:(Class)cls oriSel:(SEL)oriSelecter swizzSel:(SEL)swizzSlecter
{
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSelecter);
Method swizzMethod = class_getInstanceMethod(cls, swizzSlecter);
method_exchangeImplementations(oriMethod, swizzMethod);
}
方法交换之后调用parentInstanceMethod方法:
Animal *animal = [[Animal alloc] init];
[animal parentInstanceMethod];
Dog *dog = [[Dog alloc] init];
[dog parentInstanceMethod];
但是问题来了,当运行的时候子类Dog调用parentInstanceMethod没有崩溃,父类Animal调用parentInstanceMethod崩溃了,为什么?
代码解析:首先因为Dog继承了Animal,所以相当于说Dog两个方法childInstanceMethod和parentInstanceMethod都有,但是Animal没有方法childInstanceMethod,所以在方法替换的时候,子类方法指向了父类方法parentInstanceMethod的实现,父类方法parentInstanceMethod指向了子类方法childInstanceMethod的实现,因此父类在调用parentInstanceMethod方法时,实际调用的是子类方法childInstanceMethod的实现,而此时子类中通过childInstanceMethod调用原先的父类方法,根据消息发送流程,实际上是向父类发送childInstanceMethod消息,但是父类方法列表中并没有childInstanceMethod方法,而在消息发送流程中,方法寻找过程是由子类向父类移动的,而方法childInstanceMethod存在于子类,所以就出现崩溃。那这个方法怎么解决呢?
方法交换前先尝试为当前类添加要被替换的方法
针对上面的问题,对原先的方法交换代码逻辑进行一下优化:
+ (void)my_instanceMethodSwizzlingWithClass:(Class)cls oriSel:(SEL)oriSelecter swizzSel:(SEL)swizzSlecter
{
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSelecter);
Method swizzMethod = class_getInstanceMethod(cls, swizzSlecter);
BOOL isSucceed = class_addMethod(cls, oriSelecter, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
if (isSucceed) {//
class_replaceMethod(cls, swizzSlecter, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swizzMethod);
}
}
在方法交换前,我们先尝试添加一个新的方法,如果添加成功,则直接替换,如果添加不成功则交换。
代码解析:这里oriMethod是parentInstanceMethod,swizzMethod是childInstanceMethod。首先会为clss添加一个新的方法,新的方法名与oriMethod相同,但是方法实现是swizzMethod的,如果添加成功,表示当前cls没有这个方法,然后只需要做个替换就可以了,如果当前类已经有这个方法,则进行交换。这样的做的结果就是不会修改父类的parentInstanceMethod实现。因为addMethod中判断cls是否有这个方法时只判断本类的而不会判断父类的。
但是这样就完美了吗?接下来还有一个问题。如果父类和子类都没没有实现parentInstanceMethod方法会有什么问题呢?
要替换的两个方法都判空,没有的话就先添加一个
针对上面的问题进一步优化,如下:
+ (void)my_instanceMethodSwizzlingWithClass:(Class)cls oriSel:(SEL)oriSelecter swizzSel:(SEL)swizzSlecter
{
if (!cls) {
NSLog(@"传入的交换类不能为空");
return;
};
if(!oriSelecter && !swizzSlecter) {
NSLog(@"传入两个空Slecter");
return;
};
Method oriMethod = class_getInstanceMethod(cls, oriSelecter);
Method swizzMethod = class_getInstanceMethod(cls, swizzSlecter);
if(!oriMethod && !swizzMethod) {
NSLog(@"两个方法不存在");
return;
};
if (!oriMethod) {
class_addMethod(cls, oriSelecter, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
// method_setImplementation(swizzMethod, &parentMetodIMP);
method_setImplementation(swizzMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"object:%@, _cmd:%p", self, _cmd);
}));
}else if (!swizzMethod) {
class_addMethod(cls, swizzSlecter, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
// method_setImplementation(oriMethod, &childMetodIMP);
method_setImplementation(oriMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"object:%@, _cmd:%p", self, _cmd);
}));
}
BOOL isSucceed = class_addMethod(cls, oriSelecter, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
if (isSucceed) {//
class_replaceMethod(cls, swizzSlecter, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swizzMethod);
}
}
这里面会先判断两个method是否为空,如果method为空则先添加一个,然后在进行交换。因不管是在添加或者交换的时候,如果方法为空是操作无效的。
总结
方法交换涉及到Method底层结构(SEL和IMP的关系),已经方法查找流程点击了解方查找流程,类的加载等内容。除了上面提到的要点,Method Swizzling使用过程中呢还有一些地方要注意,那就是如果是在+load方法实现,那意味着这个类要变成非懒加载类(详情参考类的加载流程),使用过多会影响启动速度(点击了解app启动优化),现在一般推荐在initialize方法里面实现,但是在使用initialize方法时得注意,如果类和分类同时实现的话只会执行分类的initialize。不管是写在哪里,首先都要保证在使用前进行交换,而且要保证只交换一次,以免错乱。