runtime
的 method swizzing
其本质是方法交换,即 imp 的交换。但其使用过程中可能出现一些问题,本文对method swizzing
进行简单的探究。
一、方法交换的问题场景与处理方案
1、+load
方法中交换的问题
任意创建一个iOS
工程,代码准备. 子类MySubPerson
添加分类,在分类中的+load
调方法交换的方法:
// 1、+load 中交换方法
+ (void)load {
NSLog(@"%s",__func__);
[My_RunTimeTool my_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceOne) swizzledSEL:@selector(my_subPersonInstanceOne)];
}
/// My_RunTimeTool 工具类
// 1、
+ (void)my_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);
}
ViewController
:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
MySubPerson *subPerson = [[MySubPerson alloc] init];
[subPerson personInstanceOne];
[MySubPerson load];
[subPerson personInstanceOne];
MyPerson *person = [[MyPerson alloc] init];
[person personInstanceOne];
}
运行工程,输出如下:
Demo_appload[25656:2481009] +[ViewController load]
Demo_appload[25656:2481009] +[MySubPerson(forSwizz) load]
Demo_appload[57169:6022736] 来了 C++ : myFunc
Demo_appload[25656:2481009] main 函数进入
Demo_appload[25656:2481009] -[MySubPerson personInstanceOne]
Demo_appload[25656:2481009] MySubPerson 分类添加的对象方法:-[MySubPerson(forSwizz) my_subPersonInstanceOne]
Demo_appload[25656:2481009] +[MySubPerson(forSwizz) load]
Demo_appload[25656:2481009] -[MySubPerson personInstanceOne]
Demo_appload[25656:2481009] -[MyPerson personInstanceOne]
这里除了个问题,我们其实要实现MySubPerson
执行方法:my_subPersonInstanceOne
,但是load
的多次调用,使方法多次交换,交换成了并非我们所需要的。so 要进行 once
处理:
+ (void)load {
NSLog(@"%s",__func__);
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[My_RunTimeTool my_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceOne) swizzledSEL:@selector(my_subPersonInstanceOne)];
});
}
再次运行工程,MySubPerson
每次都是执行my_subPersonInstanceOne
,实现需求:
Demo_appload[25656:2481009] -[MySubPerson personInstanceOne]
Demo_appload[25656:2481009] MySubPerson 分类添加的对象方法:-[MySubPerson(forSwizz) my_subPersonInstanceOne]
Demo_appload[25656:2481009] +[MySubPerson(forSwizz) load]
Demo_appload[25656:2481009] MySubPerson 分类添加的对象方法:-[MySubPerson(forSwizz) my_subPersonInstanceOne]
Demo_appload[25656:2481009] -[MyPerson personInstanceOne]
但是,我们一直+load
方法的实现,会使类提前加载,对启动造成慢。
so 使用在+initialize
而非+load
进行一系列处理。
+ (void)initialize
{
if (self == [super class]) {
NSLog(@"%s",__func__);
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[My_RunTimeTool my_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceOne) swizzledSEL:@selector(my_subPersonInstanceOne)];
});
}
}
2、父类实现 - 子类中不实现oriMethod
方法的场景
运行工程,crash 了,信息如下:
Demo_appload[26037:2504023] -[MyPerson personInstanceOne]
Demo_appload[26037:2504023] MySubPerson 分类添加的对象方法:-[MySubPerson(forSwizz) my_subPersonInstanceOne]
Demo_appload[25970:2499447] -[MyPerson my_subPersonInstanceOne]: unrecognized selector sent to instance 0x600002b3b6c0
Demo_appload[25970:2499447] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MyPerson my_subPersonInstanceOne]: unrecognized selector sent to instance 0x600002b3b6c0'
这是什么原因呢?
还记得方法查找流程
吗?子类未实现便会一直找向父类,所以,方法交换时,父类MyPerson
的imp
变了,它指向了my_subPersonInstanceOne
,子类中有实现正常执行,但父类中并未实现my_subPersonInstanceOne
,so 找不到方法-->崩溃。
处理方案:
/// My_RunTimeTool 工具类
// 2、父类实现 子类未实现方法
+ (void)my_resolve_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
// 尝试添加我们要交换的方法 swiMethod: my_subPersonInstanceOne
BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) {// 加成功了,那么就是自己没有方法:swiMethod - 用父类的自己的方法替换
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{ // 自己有,正常交换
method_exchangeImplementations(oriMethod, swiMethod);
}
}
再次运行工程,成功执行:
来了 C++ : myFunc
Demo_appload[26168:2512856] main 函数进入
Demo_appload[26168:2512856] +[MySubPerson(forSwizz) initialize]
Demo_appload[26168:2512856] -[MyPerson personInstanceOne]
Demo_appload[26168:2512856] MySubPerson 分类添加的对象方法:-[MySubPerson(forSwizz) my_subPersonInstanceOne]
Demo_appload[26168:2512856] -[MyPerson personInstanceOne]
3、父类子类都未实现oriMethod
运行工程,执行到下面位置:
来了 C++ : myFunc
Demo_appload[26233:2520202] main 函数进入
Demo_appload[26233:2520202] +[MySubPerson(forSwizz) initialize]
过了一会儿崩溃了在了下图位置:
image.png递归循环溢出了,但是为何上面2中场景未造成 crash 呢?
--> 1、文章上面的场景 1、2
中,my_subPersonInstanceOne
的imp
是指向personInstanceOne
的,方法查找时找的是personInstanceOne
,找到与否都会抛出结果;
--> 2、但场景3
中personInstanceOne
并未实现,没有imp
,my_subPersonInstanceOne
一直自己指自己,递归至溢出崩溃。
如何处理这中场景呢?代码如下:
// 3、父类子类未实现 oriSEL
+ (void)my_best_methodSwizzlingWithClass:(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 导致无意义交换
// 在 oriMethod 为 nil 时,替换后将 swizzledSEL 赋值一个不做任何事的空实现(这里做个打印),代码如下:
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"来了一个空的 imp");
}));
}
// 尝试添加我们要交换的方法 swiMethod: my_subPersonInstanceOne
BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) {// 加成功了,那么就是自己没有方法:swiMethod - 用自己替换
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{ // 自己有,正常交换
method_exchangeImplementations(oriMethod, swiMethod);
}
}
判断一下oriMethod
,若不存在,给swiMethod
一个空的实现,再次运行结果如下,崩在MyPerson
找不到personInstanceOne
:
Demo_appload[26619:2549842] main 函数进入
Demo_appload[26619:2549842] +[MySubPerson(forSwizz) initialize]
Demo_appload[26619:2549842] -[MySubPerson(forSwizz) my_subPersonInstanceOne]
Demo_appload[26619:2549842] 来了一个空的 imp
Demo_appload[26619:2549842] MySubPerson 分类添加的对象方法:-[MySubPerson(forSwizz) my_subPersonInstanceOne]
Demo_appload[26619:2549842] -[MyPerson personInstanceOne]: unrecognized selector sent to instance 0x6000021843b0
Demo_appload[26619:2549842] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MyPerson personInstanceOne]: unrecognized selector sent to instance 0x6000021843b0'
二、 api 源码
1、method_exchangeImplementations()
源码:
void method_exchangeImplementations(Method m1, Method m2)
{
if (!m1 || !m2) return;
mutex_locker_t lock(runtimeLock);
// imp 交换
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);
adjustCustomFlagsForMethodChange(nil, m1);
adjustCustomFlagsForMethodChange(nil, m2);
}
2、class_replaceMethod()
源码:
IMP
class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return nil;
mutex_locker_t lock(runtimeLock);
// 添加 Method
return addMethod(cls, name, imp, types ?: "", YES);
}
addMethod()
:
/**********************************************************************
* addMethod
* fixme
* Locking: runtimeLock must be held by the caller
**********************************************************************/
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;
if ((m = getMethodNoSuper_nolock(cls, name))) {
// already exists
if (!replace) {
result = m->imp;
} else {
// 已存在,要替换
// m: my_subPersonInstanceOne
// imp: personInstanceOne
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;
}
_method_setImplementation()
/***********************************************************************
* method_setImplementation
* Sets this method's implementation to imp.
* The previous implementation is returned.
**********************************************************************/
static IMP
_method_setImplementation(Class cls, method_t *m, IMP imp)
{
runtimeLock.assertLocked();
if (!m) return nil;
if (!imp) return nil;
// m: my_subPersonInstanceOne
// imp: personInstanceOne
IMP old = m->imp;
m->imp = imp;// 使 m 指向 personInstanceOne 的 imp
// Cache updates are slow if cls is nil (i.e. unknown)
// RR/AWZ updates are slow if cls is nil (i.e. unknown)
// fixme build list of classes whose Methods are known externally?
flushCaches(cls);
adjustCustomFlagsForMethodChange(cls, m);
return old;
}
class_replaceMethod
执行流程:
class_replaceMethod()
--> addMethod()
--> _method_setImplementation()
- 使 swizzMethod 的 imp
指向 personInstanceOne 的 imp
.
网友评论