Method Swizzling需要class_addMetho
理论:Method Swizzling本质上就是对IMP和SEL的交换。
在OC语言的runtime特性中,调用一个对象的方法就是给这个对象发送消息。是通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL,SEL对应一个IMP(一个IMP可以对应多个SEL),通过IMP调用方法。
每个类中都有一个Dispatch Table,作用就是将类中的SEL和IMP对应。而我们Method Swizzling就是对这个Dispatch Table进行操作。
1、Method Swizzling源码分析,核心代码就是交换两个Method的IMP函数指针。
void method_exchangeImplementations(Method m1, Method m2)
{
if (!m1 || !m2) return;
//加锁
mutex_locker_t lock(runtimeLock);
//指针指向交换
IMP m1_imp = m1->imp;
m1->imp = m2->imp;
m2->imp = m1_imp;
// RR/AWZ updates are slow because class is unknown
// Cache updates are slow because class is unknown
// fixme build list of classes whose Methods are known externally?
//清空该方法缓存,刷新
flushCaches(nil);
//看方法名是根据方法变更 适配class 中 custom flag状态
// 当方法更改其IMP时,更新自定义RR和AWZ
adjustCustomFlagsForMethodChange(nil, m1);
adjustCustomFlagsForMethodChange(nil, m2);
}
2、不加class_addMethod的奔溃
#import "Person.h"
@implementation Person
- (void)eatAction {
NSLog(@"Person eat");
}
@end
Son是Person的子类
#import "Son.h"
#import <objc/runtime.h>
@implementation Son
+(void)load {
Method method1 = class_getInstanceMethod(self, @selector(eatAction));
Method method2 = class_getInstanceMethod(self, @selector(sonEatAction));
method_exchangeImplementations(method1, method2);
}
- (void)sonEatAction {
NSLog(@"sonEatAction");
[self sonEatAction];
}
@end
调用
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
Person *person = [[Person alloc] init];
[person eatAction];
}
奔溃日志
2020-10-28 17:52:27.077942+0800 Test[91632:3868212] sonEatAction
2020-10-28 17:52:27.078362+0800 Test[91632:3868212] -[Person sonEatAction]: unrecognized selector sent to instance 0x600002fcc030
2020-10-28 17:52:27.098770+0800 Test[91632:3868212] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person sonEatAction]: unrecognized selector sent to instance 0x600002fcc030'
分析:
eatAction在子类Son中没有实现,在父类Person中实现。
class_getInstanceMethod是按照继承链查找方法。
eatAction为父类方法,而本类没有。直接交换后,Person类的IMP为sonEatAction。调用奔溃。
3、正确写法 - 需要class_addMethod
#import "Son.h"
#import <objc/runtime.h>
@implementation Son
+(void)load {
Method method1 = class_getInstanceMethod(self, @selector(eatAction));
Method method2 = class_getInstanceMethod(self, @selector(sonEatAction));
BOOL result = class_addMethod(self, @selector(eatAction), method_getImplementation(method2), method_getTypeEncoding(method2));
if (result) {
class_replaceMethod(self, @selector(sonEatAction), method_getImplementation(method1), method_getTypeEncoding(method1));
}else {
method_exchangeImplementations(method1, method2);
}
}
- (void)sonEatAction {
NSLog(@"sonEatAction");
[self sonEatAction];
}
@end
分析:
1、若eatAction为父类方法,先调用class_addMethod将其添加到本类中。
2、若添加成功,则eatAction本来不存在于本class。调用class_replaceMethod替换。
3、若添加不成功,则eatAction存在于本class。直接交换。
源码分析:class_addMethod和class_replaceMethod都是调用addMethod方法。区别:replace=YES。
源码分析
BOOL
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return NO;
mutex_locker_t lock(runtimeLock);
return ! addMethod(cls, name, imp, types ?: "", NO);
}
IMP
class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return nil;
mutex_locker_t lock(runtimeLock);
return addMethod(cls, name, imp, types ?: "", YES);
}
static IMP
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
IMP result = nil;
runtimeLock.assertLocked();
checkIsKnownClass(cls);
ASSERT(types);
ASSERT(cls->isRealized());
method_t *m;
//判断本类是否有此方法。NoSuper说明不会沿着继承链查找,只在本类查找
if ((m = getMethodNoSuper_nolock(cls, name))) {
// already exists
if (!replace) {
// replace=NO 直接获取结果
result = m->imp;
} else {
// replace=YES 将实现覆盖
result = _method_setImplementation(cls, m, imp);
}
} else {
auto rwe = cls->data()->extAllocIfNeeded();
// fixme optimize
// 本类中不存在此方法
// 创建一个新的方法列表,赋值属性
method_list_t *newlist;
newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
newlist->entsizeAndFlags =
(uint32_t)sizeof(method_t) | fixed_up_method_list;
newlist->count = 1;
newlist->first.name = name;
newlist->first.types = strdupIfMutable(types);
newlist->first.imp = imp;
//准备添加方法工作
prepareMethodLists(cls, &newlist, 1, NO, NO);
//插入现有的方法列表
rwe->methods.attachLists(&newlist, 1);
//刷新缓存
flushCaches(cls);
result = nil;
}
return result;
}
4、总结
1、Method Swizzling时需要class_addMethod。
作用:防止父类和子类方法实现的交换。遵循设计模式:里氏替换原则。
2、class_addMethod和class_replaceMethod都是调用addMethod方法。
区别:
addMethod -> replace=NO -> 方法直接返回。
replaceMethod -> replace=YES -> 方法覆盖。