method-swizzling 是什么?
method-swizzling
的含义是 方法交换
,其主要作用是 在运行时将一个方法的实现替换成另一个方法的实现
,这就是我们常说的 iOS黑魔法
在OC中就是 利用method-swizzling实现AOP
,其中 AOP
(Aspect Oriented Programing,面向切面编程),区别于 OOP
(面向对象编程)
OOP
和 AOP
都是一种编程的思想
OOP
编程思想更加倾向于对业务模块的封装,划分出更加清晰的逻辑单元
- 而
AOP
是面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性
每个类
都维护者一个 方法列表
,即 method_list
,method_list
中有不同的 方法
即 Method
,每个方法中包含了方法的 sel
和 IMP
,方法交换就是 将sel和IMP原本的对应断开,并将sel和新的IMP生成对应关系
如下图所示,交换前后的 sel
和 IMP
的对应关系:
method-swizzling 涉及的相关API
通过 sel
获取方法 Method
class_getInstanceMethod
:获取实例方法
class_getClassMethod
:获取类方法
method_getImplementation
:获取一个方法的实现
method_setImplementation
:设置一个方法的实现
method_getTypeEncoding
:获取方法实现的编码类型
class_addMethod
:添加方法实现
class_replaceMethod
:用一个方法的实现,替换另一个方法的实现,即 aIMP
指向 bIMP
,但是 bIMP
不一定指向 aIMP
method_exchangeImplementations
:交换两个方法的实现,即 aIMP -> bIMP
,bIMP -> aIMP
坑点1:method-swizzling 使用过程中一次性的问题
所谓的一次性就是:method-swizzling
写在 load
方法中,而 load
方法会主动调用多次,这样会 导致方法的重复交换
,使方法 sel
的指向又恢复成原来的 imp
的问题
【解决方案】
可以通过 单例设计
原则,使方法交换 只执行一次
,在 OC
中可以通过 dispatch_once
实现单例
1 2 3 4 5 6
| + (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [ZJRuntimeTool zj_methodSwizzlingWithClass:self oriSEL:@selector(helloworld) swizzledSEL:@selector(sayHello)]; }); }
|
坑点2:子类没有实现,父类实现了
在下面的这段代码中,ZJPerson
中实现了 personInstanceMethod
,而 ZJStudent
继承自 ZJPerson
,没有实现 personInstanceMethod
,运行下面的这段代码会出现什么问题?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| // ***ZJPerson类**** @interface ZJPerson : NSObject - (void)personInstanceMethod; @end @implementation ZJPerson - (void)personInstanceMethod{ NSLog(@"person对象方法:%s",__func__); } @end
// ******ZJStudent类****** @interface ZJStudent : ZJPerson - (void)helloword; + (void)sayHello; @end @implementation ZJStudent - (void)helloword{ NSLog(@"student对象方法:%s",__func__); } + (void)sayHello{ NSLog(@"student类方法:%s",__func__); } @end
//*****VC**** @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 黑魔法坑点二:子类没有实现 - 父类实现 ZJStudent * student = [[ZJStudent alloc] init]; [student personInstanceMethod]; ZJPerson * person = [[ZJPerson alloc] init]; [person personInstanceMethod]; } @end
|
其中,方法交换代码如下,是通过 ZJStudent
的分类 ZJ
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| // ****ZJStudent+ZJ分类***** @interface ZJStudent (ZJ) @end @implementation ZJStudent (ZJ) + (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [ZJRuntimeTool zj_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(zj_studentInstanceMethod)]; }); }
// personInstanceMethod 我需要父类的这个方法的一些东西 // 给你加一个personInstanceMethod 方法 // imp - (void)zj_studentInstanceMethod{ // 是否会产生递归? 不会产生递归,原因是zj_studentInstanceMethod 会走 oriIMP,即personInstanceMethod的实现中去 [self zj_studentInstanceMethod]; NSLog(@"ZJStudent分类添加的zj对象方法:%s",__func__); } @end
|
下面是封装好的 method-swizzling
方法
1 2 3 4 5 6 7 8 9 10 11 12 13
| @interface ZJRuntimeTool : NSObject + (void)zj_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL; @end @implementation ZJRuntimeTool + (void)zj_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); } @end
|
通过实际代码的调试,发现会在p调用 personInstanceMethod
方法时崩溃,下面来进行详细说明
[student personInstanceMethod];
中不报错是因为 student
中的 imp
交换成了 zj_studentInstanceMethod
,而 ZJStudent
中有这个方法(在ZJ分类中),所以不会报错
崩溃的点在于 [person personInstanceMethod];
,其本质原因:ZJStudent
的分类ZJ
中进行了方法交换,将 person
中的 IMP
交换成了 ZJStudent
中的 zj_studentInstanceMethod
,但是 ZJPerson
中没有 zj_studentInstanceMethod
方法,即 相关的IMP找不到
,所以就崩溃了
【优化:避免imp找不到】
通过 class_addMethod
尝试添加你要交换的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| + (void)zj_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{ if (!cls) { NSLog(@"传入的交换类不能为空"); } Method oriMethod = class_getInstanceMethod(cls, oriSEL); Method swiMethod = class_getInstanceMethod(cls, swizzledSEL); // 一般交换方法:交换自己有的方法 // 交换自己没有实现的方法: // 首先:会先尝试给自己添加要交换的方法 - personInstanceMethod(SEL) -> swiMethod(IMP) // 然后再将父类的IMP给swizzle personInstanceMethod(IMP) -> swizzledSEL BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod)); if (success) { // 自己没有 - 交换 - 没有父类进行处理(重写一个) class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); }else { // 自己有 method_exchangeImplementations(oriMethod, swiMethod); } }
|
下面是 class_replaceMethod、class_addMethod、method_exchangeImplementations
的源码实现
其中 class_replaceMethod
和 class_addMethod
中都调用了 addMethod
方法,区别在于 bool
值的判断,下面是 addMethod
的源码实现。
坑点3:子类没有实现,父类也没有实现,下面的调用有什么问题?
在调用 personInstanceMethod
方法时,父类 ZJPerson
中只有声明,没有实现,子类 ZJStudent
中即没有声明,也没有实现
1 2 3 4 5 6
| // 将父类 ZJPerson 实现注释 @implementation ZJPerson //- (void)personInstanceMethod{ // NSLog(@"person对象方法:%s",__func__); //} @end
|
经过调试,发现运行代码会崩溃,报错结果如下所示:
原因是:栈溢出,递归死循环
,那么为什么会发生递归呢?
主要是因为 personInstanceMethod
没有实现,然后在方法交换后,始终找不到 oriMethod
,然后交换了个寂寞,即交换失败,当我们调用 personInstanceMethod(oriMethod)
时,也就是 oriMethod
会进入 ZJ
中 zj_studentInstanceMethod
方法,然后这个方法中又调用了 zj_studentInstanceMethod
,此时的 zj_studentInstanceMethod
并没有指向 oriMethod
,然后导致了 自己调自己
,即递归死循环
【优化:避免递归死循环】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| + (void)zj_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{ if (!cls) { NSLog(@"传入的交换类不能为空"); } Method oriMethod = class_getInstanceMethod(cls, oriSEL); Method swiMethod = class_getInstanceMethod(cls, swizzledSEL); // 如果没有 oriMethod if (!oriMethod) { // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现 class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self,SEL _cmd){})); } // 一般交换方法:交换自己有的方法 // 交换自己没有实现的方法: // 首先:会先尝试给自己添加要交换的方法 - personInstanceMethod(SEL) -> swiMethod(IMP) // 然后再将父类的IMP给swizzle personInstanceMethod(IMP) -> swizzledSEL BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); if (success) { // 自己没有 - 交换 - 没有父类进行处理(重写一个) class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); }else { // 自己有 method_exchangeImplementations(oriMethod, swiMethod); } }
|
method-swizzling 类方法
类方法和实例方法的method-swizzling的原理是类似的,唯一的区别就是类方法存在 元类 中,所以可以做如下操作
ZJStudent
中只有类方法 sayHello
的声明,没有实现
1 2 3 4 5
| @interface ZJStudent : ZJPerson + (void)sayHello; @end @implementation ZJStudent @end
|
- 在ZJStudent的分类的load方法中实现类方法的交换方法
1 2 3 4 5 6 7 8 9 10 11
| //*****ZJStudent(ZJ) + (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [ZJRuntimeTool zj_bestClassMethodSwizzlingWithClass:self oriSEL:@selector(sayHello) swizzledSEL:@selector(zj_studentClassMethod)]; }); } + (void)zj_studentClassMethod{ NSLog(@"ZJStudent分类添加的lg类方法:%s",__func__); [[self class] zj_studentClassMethod]; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| + (void)zj_bestClassMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{ if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getClassMethod([cls class], oriSEL); Method swiMethod = class_getClassMethod([cls class], swizzledSEL); if (!oriMethod) { // 避免动作没有意义 // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下: class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ NSLog(@"来了一个空的 imp"); })); } // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败 // 交换自己没有实现的方法: // 首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP) // 然后再将父类的IMP给swizzle personInstanceMethod(imp) -> swizzledSEL //oriSEL:personInstanceMethod
BOOL didAddMethod = class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); if (didAddMethod) { class_replaceMethod(object_getClass(cls), swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); }else{ method_exchangeImplementations(oriMethod, swiMethod); } }
|
1 2 3 4 5
| - (void)viewDidLoad { [super viewDidLoad]; [ZJStudent sayHello]; }
|
- 运行结果如下,由于符合
方法没有实现
,所以或走到 空的imp
中
1 2
| 2022-03-14 15:48:59.225558+0800 Method-Swizzling[5512:3942308] ZJStudent分类添加的lg类方法:+[ZJStudent(ZJ) zj_studentClassMethod] 2022-03-14 15:48:59.225602+0800 Method-Swizzling[5512:3942308] 来了一个空的 imp
|
method-swizzling 应用
method-swizzling
最常用到的是 防止崩溃、字典、数组等越界
。
在iOS中,NSNumber、NSArray、NSDictionary 等这些都是类族,一个 NSArray 的实现可能由 多个类组成,所以如果想对 NSArray进行swizzling,必须获取到其“真身”进行swizzling,直接对NSArray进行操作是无效的
下面列举了 NSArray 和 NSDictionary 本类的类名,可以通过 Runtime 函数来取出本类
- NSArray:
__NSArrayI
- NSMutableArray:
__NSArrayM
- NSDictionary:
__NSDictionaryI
- NSMutableDicionary:
__NSDictionaryM
【以NSArray为例】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @implementation NSArray (ZJ) // Swizzling核心代码 // 须要注意的是,好多同窗反馈下面代码不起做用,形成这个问题的缘由大多都是其调用了super load方法。在下面的load方法中,不该该调用父类的load方法。 + (void)load { Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:)); Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cm_objectAtIndex:)); method_exchangeImplementations(fromMethod, toMethod); }
// 为了不和系统的方法冲突,我通常都会在swizzling方法前面加前缀 - (id)cm_objectAtIndex:(NSUInteger)index { // 判断下标是否越界,若是越界就进入异常拦截 if (self.count-1 < index) { @try { return [self cm_objectAtIndex:index]; } @catch (NSException *exception) { // 在崩溃后会打印崩溃信息。若是是线上,能够在这里将崩溃信息发送到服务器 NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__); NSLog(@"%@", [exception callStackSymbols]); return nil; } @finally {} } // 若是没有问题,则正常进行方法调用 else { return [self cm_objectAtIndex:index]; } }
|