Method Swizzling
Method Swizzling是Objective-C中的黑魔法,它能在运行时
对一个方法进行调整
。
method是什么?
struct method_t {
//略
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
//略
};
从源码中可以看到,方法包含SEL
、types
、imp
三要素,这三者共同构成了一个Method,也就是说,SEL
和imp
是一一以应的。而Method Swizzling
是将这种旧的对应关系打破,重新生成一组新的SEL
和imp
对应关系。
Method Swizzling
涉及的API:
//根据SEL获取实例方法
Method class_getInstanceMethod(Class cls, SEL sel);
//根据SEL获取类方法
Method class_getClassMethod(Class cls, SEL sel);
//获取一个方法的IMP
IMP method_getImplementation(Method m);
//设置一个方法的IMP
IMP method_setImplementation(Method m, IMP imp);
//获取方法实现的编码类型
const char * method_getTypeEncoding(Method m);
//添加一个方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
//将参数SEL、IMP、types替换当前的方法中的SEL、IMP、types
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);
//将m1的IMP与m2的IMP进行交换
void method_exchangeImplementations(Method m1, Method m2);
Method Swizzling
实战
将HQPerson
中的saySomething
方法与doSomething
进行交换。实现如下:
@implementation HQPerson
+ (void)load{
NSLog(@"%s",__func__);
Method m1 = class_getInstanceMethod(self, @selector(saySomething));
Method m2 = class_getInstanceMethod(self, @selector(doSomething));
method_exchangeImplementations(m1, m2);
}
- (void)saySomething{
NSLog(@"%s",__func__);
}
- (void)doSomething{
NSLog(@"%s",__func__);
}
@end
在main.m中进行方法调用
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
HQPerson* person = [[HQPerson alloc] init];
[person saySomething];
NSLog(@"end~");
}
return 0;
}
结果:
2021-03-11 17:51:37.922315+0800 HQObjc[50392:1371412] Hello, World!
2021-03-11 17:51:37.922536+0800 HQObjc[50392:1371412] -[HQPerson doSomething]
从结果中可以看到:在+load方法中对saySomething
方法和doSomething
方法进行交换,因此在main.m中向person
发送saySomething
消息时,其实发送了doSomething
消息。
接下来通过lldb查看一下,调用method_exchangeImplementations
方法前后,方法列表中的方法的变化。
//调用之前
(lldb) p $16->get(0).big()
(method_t::big) $17 = {
name = "saySomething"
types = 0x0000000100003f6f "v16@0:8"
imp = 0x00000001000038c0 (HQObjc`-[HQPerson saySomething] at HQPerson.m:22)
}
(lldb) p $16->get(1).big()
(method_t::big) $18 = {
name = "doSomething"
types = 0x0000000100003f6f "v16@0:8"
imp = 0x00000001000038f0 (HQObjc`-[HQPerson doSomething] at HQPerson.m:26)
}
//调用之后
(lldb) p $40->get(0).big()
(method_t::big) $41 = {
name = "saySomething"
types = 0x0000000100003f6f "v16@0:8"
imp = 0x00000001000038f0 (HQObjc`-[HQPerson doSomething] at HQPerson.m:26)
}
(lldb) p $40->get(1).big()
(method_t::big) $42 = {
name = "doSomething"
types = 0x0000000100003f6f "v16@0:8"
imp = 0x00000001000038c0 (HQObjc`-[HQPerson saySomething] at HQPerson.m:22)
}
通过lldb可以看出,经过方法交换之后,method1的IMP修改成method2的IMP。因此当向person发送saySomething
消息时,在方法查找后,返回的IMP为0x00000001000038f0 (HQObjc -[HQPerson doSomething] at HQPerson.m:26)
。
Method Swizzling
注意事项
- 【1】
method_exchangeImplementations
函数。
首先来看源码
void method_exchangeImplementations(Method m1, Method m2)
{
if (!m1 || !m2) return;
mutex_locker_t lock(runtimeLock);
IMP imp1 = m1->imp(false);
IMP imp2 = m2->imp(false);
SEL sel1 = m1->name();
SEL sel2 = m2->name();
//交换rw中的两个方法的imp。将m1方法的imp修改成m2方法的imp,将m2方法的imp修改成m1方法的imp。
m1->setImp(imp2);
m2->setImp(imp1);
//查看需要交换的两个方法是否存在缓存中,如果存在缓存中,则清除缓存
flushCaches(nil, __func__, [sel1, sel2, imp1, imp2](Class c){
return c->cache.shouldFlush(sel1, imp1) || c->cache.shouldFlush(sel2, imp2);
});
//对一些标识位进行调整
adjustCustomFlagsForMethodChange(nil, m1);
adjustCustomFlagsForMethodChange(nil, m2);
}
当调用method_exchangeImplementations
时,会查看当前cache
中是否存在需要交换的方法,如果有存的话则清空cache
缓存。因此,在下一次方法查找时,会将交换后的SEL和IMP
存入cache
中。
- 【2】
class_addMethod
和class_replaceMethod
方法
先来看这两个函数的源码,从源码中可得知这两个函数都是在内部调用addMethod
方法。
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);
}
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);
}
-
addMethod
方法
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;
//通过cls->data()->methods()找到SEL=name的方法
if ((m = getMethodNoSuper_nolock(cls, name))) {//此部分是表示方法列表中已经存在SEL=name的方法
// already exists
//如果是不是替换,比如当前是class_addMethod,则返回方法中的IMP
if (!replace) {
result = m->imp(false);
} else {
//如果当前是替换,则将方法m的IMP替换成参数imp,并刷新cache
result = _method_setImplementation(cls, m, imp);
}
} else {//此部分是表示方法列表中不存在SEL=name的方法
// fixme optimize
//新建一个方法列表,开辟内存空间,设置初始值
method_list_t *newlist;
newlist = (method_list_t *)calloc(method_list_t::byteSize(method_t::bigSize, 1), 1);
newlist->entsizeAndFlags =
(uint32_t)sizeof(struct method_t::big) | fixed_up_method_list;
newlist->count = 1;
//将SEL和IMP组成的方法加入新创建的方法列表中
auto &first = newlist->begin()->big();
first.name = name;
first.types = strdupIfMutable(types);
first.imp = imp;
//将方法列表添加进类的rwe中,同时刷新cache
addMethods_finish(cls, newlist);
result = nil;
}
return result;
}
addMethod
方法分为两种情况:
【1】添加的
SEL
在原来的方法列表
中已经存在
。
若当前的操作不是替换
,则直接返回方法列表中对应SEL的IMP
。
若当前的操作是替换
,则将SEL对应的IMP设置成新的IMP,并刷新cache
。-
【2】添加的SEL在原来的方法列表中不存在。
- 新建一个方法列表
newlist
。 - 将
SEL和IMP
组成的method
赋值给newlist
中的第一个元素。 - 将这个新建的方法列表
newlist
添加至rwe
中。 - 刷新
cache
。
- 新建一个方法列表
【3】在子类中替换父类的方法
假设有以下代码
@interface HQSon : HQPerson
@end
@implementation HQSon
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod(self, @selector(saySomething));
Method swiMethod = class_getInstanceMethod(self, @selector(doSomething3));
method_exchangeImplementations(oriMethod, swiMethod);
});
}
- (void)doSomething3{
NSLog(@"Son添加的对象方法:%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
HQSon * son = [[HQSon alloc] init];
[son saySomething];
HQPerson* person = [[HQPerson alloc] init];
[person saySomething];
NSLog(@"end~");
}
return 0;
}
代码说明:
【1】定义一个子类HQSon继承于HQPerson。
【2】在HQSon类中,将父类HQPerson中的saySomething方法替换成HQSon子类中定义的doSomething3方法。
【3】在main函数中分别定义HQSon对象和HQPerson对象,并使用各自的对象去调用saySomething方法。
一、请问将父类的方法
交换成子类的方法
,父类还能否调用成功?
答:可以调用成功的
,因为经过方法交换之后,父类SEL= saySomething
,而其IMP为子类的doSomething3方法
,在进行方法查找时,能为其找到IMP,并执行IMP所指向的函数。
二、在子类中增加如下代码会有什么结果?
- (void)doSomething3{
NSLog(@"Son添加的对象方法:%s",__func__);
[self doSomething3];
}
答:此时会发生崩溃,崩溃描述如下:
2021-03-15 14:54:50.503505+0800 HQObjc[30636:3810833] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[HQPerson doSomething3]: unrecognized selector sent to instance 0x10064c5d0'
)
这是因为,经过方法交换之后,向person
发送saySomething
信息时,会跳转至HQSon中的doSomething3
方法,在doSomething3方法中,self为HQPerson
。因为在doSomething3中又通过self调用doSomething3方法,即:向HQPerson发送doSomething3消息
,而在HQPerosn
中找不到doSomething3方法
,因此发生崩溃。
- 【3】类方法的方法交换
【1】和【2】都是针对对象方法交换
的情况,那是否能对类方法
进行方法交换呢?
答:可以的。
一、由于方法的本质
就是消息
,通过SEL
找到其IMP
并通过汇编跳转至IMP所指向的函数
执行。因此,只要能SEL找到对应的IMP,就能完成方法调用。
二、获取类方法
需要调用class_getClassMethod
函数。
三、使用class_addMethod
和class_replaceMethod
这两个函数添加类方法
或替换类方法
时,需要传入元类
。