016*:方法交换Method-Swizzling?(1:一次性问题:load或者initialize 2: 子类不和父类方法。)
问题
1:一次性问题:load或者initialize
2: 子类不和父类方法。
目录
1:method-swizzling 是什么?
2:注意:
3:method-swizzling - 类方法
4:method-swizzling的应用
预备
HTRuntimeTool类:负责方法交换的具体操作HTPerson类: 继承自NSObject的类,拥有personFunc方法HTStudent类: 继承自HTPerson的类,拥有studentFun#import <objc/runtime.h//MARK: - HTRuntimeTool@interface HTRuntimeTool : NSObject+ (void)ht_methodSwizzilingWithClass: (Class)cls oriSEL: (SEL)oriSel swizzledSEL: (SEL)swizzledSel; @end @implementation HTRuntimeTool + (void)ht_methodSwizzilingWithClass: (Class)cls oriSEL: (SEL)oriSel swizzledSEL: (SEL)swizzledSel { NSAssert(cls != nil, @"传入的交换类不能为空!"); // 【这是错误实例,下面坑点3讲解】 Method oriMethod = class_getInstanceMethod(cls, oriSel); Method swiMethod = class_getInstanceMethod(cls, swizzledSel); method_exchangeImplementations(oriMethod, swiMethod); } @end //MARK: - HTPerson @interface HTPerson : NSObject - (void)personFunc; @end @implementation HTPerson - (void)personFunc { NSLog(@"HTPerson实例方法: %s", __func__); } @end //MARK: - HTStudent @interface HTStudent : HTPerson - (void)studentFunc; @end @implementation HTStudent //+ (void)load { // static dispatch_once_t onceToken; // dispatch_once(&onceToken, ^{ // [HTRuntimeTool ht_methodSwizzilingWithClass:self oriSEL:@selector(personFunc) swizzledSEL:@selector(studentFunc)]; // }); //} // 避免影响启动时长,方法交换放在initialize中实现 + (void)initialize { if (self == [HTStudent class]) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [HTRuntimeTool ht_methodSwizzilingWithClass:self oriSEL:@selector(personFunc) swizzledSEL:@selector(studentFunc)]; }); } } - (void)studentFunc { [self studentFunc]; NSLog(@"HTStudent实例方法: %s", __func__); } @end //MARK: -ViewController @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; HTStudent * student = [HTStudent new]; [student studentFunc];
} @end
正文
1:method-swizzling 是什么?
-
method-swizzling的含义是方法交换,其主要作用是在运行时将一个方法的实现替换成另一个方法的实现,这就是我们常说的iOS黑魔法, -
在OC中就是
利用method-swizzling实现AOP,其中AOP(Aspect Oriented Programming,面向切面编程)是一种编程的思想,区别于OOP(面向对象编程)- OOP和AOP都是一种编程的思想ios_lowLevel
OOP编程思想更加倾向于对业务模块的封装,划分出更加清晰的逻辑单元;- 而
AOP是面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性。
-
每个类都维护着一个
方法列表,即methodList,methodList中有不同的方法即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
2:注意:
坑点1:method-swizzling使用过程中的一次性问题
1.1:可以通过单例设计原则,使方法交换只执行一次,在OC中可以通过dispatch_once实现单例
+ (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [LGRuntimeTool lg_bestMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lg_studentInstanceMethod)]; }); }
// 避免影响启动时长,方法交换放在initialize中实现 + (void)initialize { if (self == [HTStudent class]) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [HTRuntimeTool ht_methodSwizzilingWithClass:self oriSEL:@selector(personFunc) swizzledSEL:@selector(studentFunc)]; }); } }
坑点2:必须提前准备
交换操作必须提前完成,不然会产生调用混乱,执行错误会造成crash或其他业务bug。
我们可以在+load方法或+initialize方法内完成交换操作,保障交换操作的提前准备。
-
+load方法:将懒加载类变成非懒加载类,在程序启动前就完成相应操作。会影响程序启动时长。不建议使用。 -
+initialize方法:系统动态加入到NSObject的方法,所有继承自NSObject的类,都拥有该方法。
在类首次被调用时,首先会执行+initialize方法。所以既做到了懒加载,不提前占用资源。也满足了提前准备的要求。
坑点3:不可交换父类方法
上面案例,粗看没啥问题,但是当我们创建HTPerson对象,调用personFunc函数时,crash了:

崩溃信息告诉我们:HTPerson类没有studentFunc方法,导致崩溃。
-
我们进行方法交换时,
HTStudent中没有找到personFunc方法,所以会沿着继承链往上找,在父类HTPerson中找到了personFunc方法。所以我们将HTStudent的studentFunc方法与HTPerson的personFunc方法进行了交换。 -
当使用子类
HTStudent实例对象进行调用时,一切都正常。但是当使用父类HTPerson进行调用时,就会找不到交换后的studentFunc方法,导致崩溃。
解决方法:
将影响范围,限制在当前类中,可借助父类的IMP实现,但不可让交换主体变成父类。
具体操作:
方法交换前,先尝试给自己添加待交换的方法,再将父类的IMP指给swizzle。
保障交换cls是当前对象,不会找到父类或继承链上层。
#import <objc/runtime.h> //MARK: - HTRuntimeTool @interface HTRuntimeTool : NSObject + (void)ht_methodSwizzilingWithClass: (Class)cls oriSEL: (SEL)oriSel swizzledSEL: (SEL)swizzledSel; @end @implementation HTRuntimeTool + (void)ht_methodSwizzilingWithClass: (Class)cls oriSEL: (SEL)oriSel swizzledSEL: (SEL)swizzledSel { NSAssert(cls, @"传入的交换类不能为空"); // 1. 分别读取`oriMethod`和`swiMethod`. (此时的oriMethod实现可能来自于`继承链`上的`某个类`。) Method oriMethod = class_getInstanceMethod(cls, oriSel); Method swiMethod = class_getInstanceMethod(cls, swizzledSel); // 2. 被交换的函数必须实现 (想要交换的函数都没实现,就完全没有意义了) NSString * str = [NSString stringWithFormat:@"被交换的函数:[%@]没有实现",NSStringFromSelector(swizzledSel)]; NSAssert(swiMethod, str); // 3. 检查oriMethod是否存在。(不存在表示整个继承链都没有`oriSel`的实现) if (!oriMethod) { // 不存在时,为了避免crash,我们手动添加一个空的Block IMP class_addMethod(cls, oriSel, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); method_setImplementation(swiMethod, imp_implementationWithBlock(^{ NSLog(@"[%@]创建新对象%@",NSStringFromClass(cls) ,NSStringFromSelector(oriSel)); })); } // 4. 尝试给cls添加`oriSel`方法。 BOOL addMethod = class_addMethod(cls, oriSel, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); // 4.1 添加成功, 表示之前cls没有`oriSel`方法。 if (addMethod) { // `addMethod`时,我们已将`oriSel`的imp实现,并指向了swiMethod, // 所以此时,只需要将`swizzledSel`的imp实现,指向oriMethod即可。 // class_replaceMethod 是替换,覆盖的意思。等于重写绑定了`swizzledSel`的sel和imp关系 class_replaceMethod(cls, swizzledSel, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); } // 4.2 添加失败,表示之前cls已经存在`oriSel`方法。 else { // 需要将`oriSel`和`swizzledSel`的Imp进行交换 // method_exchangeImplementations 是交换的意思,等于将`oriMethod`和`swiMethod`的sel和imp的绑定关系进行交叉互换。 //(oriSel -> swiMethod, swizzledSel -> oriMethod) method_exchangeImplementations(oriMethod, swiMethod); } } @end //MARK: - HTPerson @interface HTPerson : NSObject - (void)personFunc; @end @implementation HTPerson - (void)personFunc { NSLog(@"HTPerson实例方法: %s", __func__); } @end //MARK: - HTStudent @interface HTStudent : HTPerson - (void)studentFunc; @end @implementation HTStudent //+ (void)load { // static dispatch_once_t onceToken; // dispatch_once(&onceToken, ^{ // [HTRuntimeTool ht_methodSwizzilingWithClass:self oriSEL:@selector(personFunc) swizzledSEL:@selector(studentFunc)]; // }); //} // 避免影响启动时长,方法交换放在initialize中实现 + (void)initialize { if (self == [HTStudent class]) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [HTRuntimeTool ht_methodSwizzilingWithClass:self oriSEL:@selector(personFunc) swizzledSEL:@selector(studentFunc)]; }); } } - (void)studentFunc { // 当我们将studentFunc的实现与personFunc的实现互换后,此处就不是递归调用自己了。 [self studentFunc]; NSLog(@"HTStudent实例方法: %s", __func__); } @end //MARK: -ViewController @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; HTStudent * student = [HTStudent new]; [student studentFunc]; [student personFunc]; HTPerson * person = [HTPerson new]; [person personFunc]; } @end
ht_methodSwizzilingWithClass分析:
1:分别读取oriMethod和swiMethod.
(此时的oriMethod实现可能来自于继承链上的某个类。)
2:被交换的函数(swiMethod)必须实现
(比如: 你想将HTStudent对象的studentFunc与HTPerson的personFunc进行交换,你至少得实现studentFunc方法啊)
3:检查oriMethod是否存在。
(不存在表示 整个继承链都没有oriSel的实现)
4:如果不存在,为了避免crash,我们手动添加一个IMP(内容是个空的Block)
尝试给cls添加oriSel方法。
4.1 添加成功:
表示之前cls没有oriSel方法。addMethod时,我们已将oriSel的IMP实现,并指向了swiMethod,此时只需将swizzledSel的IMP实现,指向oriMethod即可。
(class_replaceMethod:是替换,覆盖的意思。等于重绑定了swizzledSel的sel和imp关系)
4.2 添加失败:
表示之前cls已存在oriSel方法。
需要将oriSel和swizzledSel的SEL和IMP进行交换
(method_exchangeImplementations:是交换的意思,等于将oriMethod和swiMethod的SEL和IMP的绑定关系进行交叉互换。
(oriSel -> swiMethod,swizzledSel -> oriMethod)
3:method-swizzling - 类方法
类方法和实例方法的method-swizzling的原理是类似的,唯一的区别是类方法存在元类中,所以可以做如下操作
LGStudent中只有类方法sayHello的声明,没有实现
@interface LGStudent : LGPerson - (void)helloword; + (void)sayHello; @end @implementation LGStudent @end
在LGStudent的分类的load方法中实现类方法的方法交换
+ (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [LGRuntimeTool lg_bestClassMethodSwizzlingWithClass:self oriSEL:@selector(sayHello) swizzledSEL:@selector(lg_studentClassMethod)]; }); } + (void)lg_studentClassMethod{ NSLog(@"LGStudent分类添加的lg类方法:%s",__func__); [[self class] lg_studentClassMethod]; }
类方法的方法交换如下
-
需要通过
class_getClassMethod方法获取类方法 -
在调用
class_addMethod和class_replaceMethod方法添加和替换时,需要传入的类是元类,元类可以通过object_getClass方法获取类的元类
//封装的method-swizzling方法 + (void)lg_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); } }
调用如下
- (void)viewDidLoad { [super viewDidLoad]; [LGStudent sayHello]; }
运行结果如下,由于符合方法没有实现,所以会走到空的imp中

method-swizzling最常用的应用是防止数组、字典等越界崩溃
在iOS中NSNumber、NSArray、NSDictionary等这些类都是类簇,一个NSArray的实现可能由多个类组成。所以如果想对NSArray进行Swizzling,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的。
下面列举了NSArray和NSDictionary本类的类名,可以通过Runtime函数取出本类。
下面列举了NSArray和NSDictionary本类的类名,可以通过Runtime函数取出本类。
以 NSArray 为例
| 类名 | 真身 |
|---|---|
| NSArray | __NSArrayI |
| NSMutableArray | __NSArrayM |
| NSDictionary | __NSDictionaryI |
| NSMutableDictionary | __NSDictionaryM |
@implementation NSArray (CJLArray) //如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效 + (void)load{ Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:)); Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cjl_objectAtIndex:)); method_exchangeImplementations(fromMethod, toMethod); } //如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效 - (id)cjl_objectAtIndex:(NSUInteger)index{ //判断下标是否越界,如果越界就进入异常拦截 if (self.count-1 < index) { // 这里做一下异常处理,不然都不知道出错了。 #ifdef DEBUG // 调试阶段 return [self cjl_objectAtIndex:index]; #else // 发布阶段 @try { return [self cjl_objectAtIndex:index]; } @catch (NSException *exception) { // 在崩溃后会打印崩溃信息,方便我们调试。 NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__); NSLog(@"%@", [exception callStackSymbols]); return nil; } @finally { } #endif }else{ // 如果没有问题,则正常进行方法调用 return [self cjl_objectAtIndex:index]; } } @end
引用
1:OC底层原理二十一:内存平移 & Mothod Swizzling的应用
2:iOS-底层原理 21:Method-Swizzling 方法交换
浙公网安备 33010602011771号