Method-Swizzling
- Method-Swizzling即方法的交换,其实现方式是
在App运行时将一个方法的实现替换成另一个方法的实现,这就是我们常说的iOS黑魔法; - 每个类都维护着一个方法类表即
class_rw_ext中的method_array_t二维数组,在method_array_t中存储中许多不同的方法即Method,在iOS底层系列16 -- 类的加载机制已经探索了在运行时,这些主类的方法集合和分类的方法集合是如何加载进入主类class_rw_ext rwe结构中的method_array_t当中的; - 每个Method都包含
sel和IMP两个部分,分别表示method的名称和method的实现,方法交换就是将sel和imp原本的对应断开,并将sel和新的IMP生成对应关系;

Snip20210312_114.png
Method-Swizzling常见函数
-
class_getInstanceMethod:获取实例方法; -
class_getClassMethod:获取类方法; -
method_getImplementation:获取方法的实现; -
method_setImplementation:设置方法的实现; -
method_getTypeEncoding:获取方法实现的编码类型; -
class_addMethod:给方法添加实现; -
class_replaceMethod:用一个方法的实现,替换另一个方法的实现,即IMP1指向 IMP2,但是IMP2不一定指向IMP1; -
method_exchangeImplementations:交换两个方法的实现,即IMP1指向IMP2,IMP2指向IMP1; - 下面通过几个案例来阐述在使用Method-Swizzling过程中需要注意的事项;
- 准备工作:新建两个类
父类YYPerson,子类YYStudent,Method-Swizzling帮助类YYRuntimeHelper,YYStudent分类(Add)
#import <Foundation/Foundation.h>
@interface YYPerson : NSObject
- (void)walk;
@end
#import "YYPerson.h"
@implementation YYPerson
- (void)walk{
NSLog(@"%s",__func__);
}
@end
#import <Foundation/Foundation.h>
@interface YYStudent : YYPerson
- (void)read;
- (void)write;
@end
#import "YYStudent.h"
@implementation YYStudent
- (void)read{
NSLog(@"%s",__func__);
}
- (void)write{
NSLog(@"%s",__func__);
}
@end
#import "YYStudent.h"
@interface YYStudent (Add)
@end
#import "YYStudent+Add.h"
#import "YYRuntimeHelper.h"
@implementation YYStudent (Add)
//在运行时,进行方法的交换
+ (void)load{
[YYRuntimeHelper yy_instanceMethodSwizzlingithClass:self originalSEL:@selector(read) swizzledSEL:@selector(yy_read)];
}
- (void)yy_read{
NSLog(@" 使用yy_read的方法去实现read");
}
@end
#import <Foundation/Foundation.h>
@interface YYRuntimeHelper : NSObject
+ (void)yy_instanceMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL;
@end
#import "YYRuntimeHelper.h"
#import <objc/runtime.h>
@implementation YYRuntimeHelper
+ (void)yy_instanceMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL{
Method originalMethod = class_getInstanceMethod(cls, originalSEL);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSEL);
//方法交换
method_exchangeImplementations(originalMethod, swizzledMethod);
}
@end
案例一:外界主动调用目标类的load方法,导致Method-Swizzling失效

Snip20210312_116.png
- 上面的截图是正常情况,但如果在
AppDelegate.m文件中,主动调用[YYStudent load],那么当代码执行到控制器时,Method-Swizzling就失效了,因为前后交换了两次还原了;

Snip20210312_118.png
- 解决方案:保证
YYStudent(Add)中load方法中的Method-Swizzling只执行一次,加入dispatch_once,YYStudent(Add)修改后的代码如下:
#import "YYStudent+Add.h"
#import "YYRuntimeHelper.h"
@implementation YYStudent (Add)
//在运行时,进行方法的交换
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[YYRuntimeHelper yy_instanceMethodSwizzlingithClass:self originalSEL:@selector(read) swizzledSEL:@selector(yy_read)];
});
}
- (void)yy_read{
NSLog(@" 使用yy_read的方法去实现read");
}
@end
案例二:子类没有实现目标方法,但是父类实现了目标方法,在子类的分类中交换目标方法的实现时,最终交换的是父类的实现
- 例如上面的YYPerson类有walk的实现,但在子类YYStudent中没有walk的实现,YYStudent(Add)分类中的交换实现如下:
#import "YYStudent+Add.h"
#import "YYRuntimeHelper.h"
@implementation YYStudent (Add)
//在运行时,进行方法的交换
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[YYRuntimeHelper yy_instanceMethodSwizzlingithClass:self originalSEL:@selector(walk) swizzledSEL:@selector(yy_walk)];
});
}
- (void)yy_walk{
//这里不会出现循环调用,因为已经实现了方法的交换
[self yy_walk];
NSLog(@" 使用yy_walk的方法去实现walk");
}
@end
- 外界调用及LLDB调试如下:

Snip20210315_122.png

Snip20210315_121.png
- 出现了崩溃,首先因为子类YYStudent没有walk方法实现,所以Method-Swizzling交换的是父类的walk,见下图所示:
-
[student walk]由于student没有walk方法实现,会调用父类YYPerson的walk方法,而父类的walk在运行时进行了Method-Swizzling,所以会调用YYStudent(Add)中的yy_walk方法实现,其内部首先调用[self yy_walk],此时的self是student,调用的则是父类YYPerson的walk方法实现;

image.png
-
[person walk]则调用了YYStudent(Add)中的yy_walk方法实现,其内部首先调用[self yy_walk]此时的self是person,而YYPerson类中并没有yy_walk方法,所以会出现崩溃;

image.png
- 本意是交换子类的walk实现,不想将父类的walk实现也交换了,且会发生崩溃,为了Method-Swizzling不影响父类该怎么做?
- 解决方案:在进行方法交换时,首先判断当前类是否有目标方法,如果有直接进行交换,如果没有调用
class_addMethod函数,给当前类动态添加一个方法(目标方法walk),其实现为交换方法的实现,若添加成功后,再调用class_replaceMethod将交换方法的实现替换成目标方法的实现,这样才完整的实现了Method-Swizzling; -
YYRuntimeHelper修改后的代码如下:
#import "YYRuntimeHelper.h"
#import <objc/runtime.h>
@implementation YYRuntimeHelper
+ (void)yy_instanceMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) {
return;
}
Method original_method = class_getInstanceMethod(cls, originalSEL);
Method swizzled_method = class_getInstanceMethod(cls, swizzledSEL);
IMP original_imp = method_getImplementation(original_method);
IMP swizzled_imp = method_getImplementation(swizzled_method);
//主要用来检测当前类是否有original_method,避免替换了父类的实现
//originalSEL --> swizzled_imp
BOOL success = class_addMethod(cls, originalSEL, swizzled_imp, method_getTypeEncoding(original_method));
//添加成功,表示当前类没有original_method
if (success) {
//swizzledSEL --> original_imp
//因为当前类没有original_method 这里的original_imp是当前类父类的实现 (消息的慢速查找机制决定的)
class_replaceMethod(cls, swizzledSEL, original_imp, method_getTypeEncoding(original_method));
}else{//添加失败,表示当前类有original_method
//方法交换
method_exchangeImplementations(original_method,swizzled_method);
}
}
@end
- 注意这里的
original_imp是父类的实现,因为当前类没有original_method,根据消息的慢速查找机制,若当前类没有方法实现,会在当前类的继承链上寻找方法实现;

image.png
- LLDB调试结果如下:

Snip20210312_120.png

image.png
案例三:子类没有实现目标方法,且父类也没有实现目标方法
- 外界调用及LLDB调试结果如下:

Snip20210315_124.png

Snip20210315_125.png
- Method-Swizzling的代码实现:

Snip20210315_126.png
- 首先运行时进行Method-Swizzling,由于当前类YYStudent没有walk方法,所以会调用
class_addMethod函数,给YYStudent添加walk方法其实现为yy_walk,添加成功后会执行class_replaceMethod函数,但由于original_imp不存在,所以会交换失败,那么Method-Swizzling只实现了单方的交换,如下图所示:

image.png
- 当
[student walk]时,会来到yy_walk方法,当执行[self yy_walk]时,由于yy_walk并没有交换成功,所以调用的实现还是自己本身,则出现循环调用,最后导致内存溢出应用崩溃; - 解决方案:在进行Method-Swizzling之前,对原始方法进行检测,若为空调用
class_addMethod函数,添加原始方法walk,方法实现为交换方法实现yy_walk,然后将交换方法yy_walk的实现置为{},即为空方法什么也不做,接着再调用class_addMethod函数,此时上面已经添加过了,所以再次添加会不成功,最后走method_exchangeImplementations函数,但由于original_method是空的,所以不会发生交换,最终的效果如下所示:

image.png
-
YYRuntimeHelper修过之后的代码如下:
+ (void)yy_instanceMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) {
return;
}
Method original_method = class_getInstanceMethod(cls,originalSEL);
Method swizzled_method = class_getInstanceMethod(cls,swizzledSEL);
IMP original_imp = method_getImplementation(original_method);
IMP swizzled_imp = method_getImplementation(swizzled_method);
if (!original_method) {
//在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现
class_addMethod(cls,originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
method_setImplementation(swizzled_method, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}
BOOL didAddMethod = class_addMethod(cls,originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
if (didAddMethod) {
class_replaceMethod(cls,swizzledSEL,original_imp, method_getTypeEncoding(original_method));
}else{
method_exchangeImplementations(original_method,swizzled_method);
}
}
-
method_exchangeImplementations源码实现如下:

Snip20210315_131.png
Method-Swizzling之类方法
- 上面介绍的是实例方法的交换,类方法的交换原理是类似的,只不过类方法存储在元类,具体实现代码如下:
+ (void)yy_classMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) {
return;
}
class_getClassMethod(cls,originalSEL);
Method original_method = class_getClassMethod([cls class],originalSEL);
Method swizzled_method = class_getClassMethod([cls class],swizzledSEL);
IMP original_imp = method_getImplementation(original_method);
IMP swizzled_imp = method_getImplementation(swizzled_method);
if (!original_method) {
//在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现
//object_getClass(cls) 获取元类
class_addMethod(object_getClass(cls),originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
method_setImplementation(swizzled_method,
imp_implementationWithBlock(^(id self,SEL _cmd){
NSLog(@"来了一个空的 imp");
}));
}
BOOL didAddMethod = class_addMethod(object_getClass(cls),originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
if (didAddMethod) {
class_replaceMethod(object_getClass(cls),swizzledSEL,original_imp, method_getTypeEncoding(original_method));
}else{
method_exchangeImplementations(original_method,swizzled_method);
}
}