八、Method Swizzing坑点
对
method swizzing不了解的可以阅读iOS逆向 代码注入+Hook
1.黑魔法应用
在日常开发中,再好的程序员都会犯错,比如数组越界
NSArray *array = @[@"F", @"e", @"l", @"i", @"x"];
NSLog(@"%@", array[5]);
NSLog(@"%@", [array objectAtIndex:5]);
因此为了避免数组越界这种问题,大神们开始玩起了黑魔法——method swizzing
- 新建
NSArray分类 - 导入
runtime头文件——<objc/runtime.h> - 写下新的方法
- 在
+load利用黑魔法交换方法
#import "NSArray+FX.h"
#import <objc/runtime.h>
@implementation NSArray (FX)
+ (void)load {
// 交换objectAtIndex方法
Method oriMethod1 = class_getInstanceMethod(self, @selector(objectAtIndex:));
Method swiMethod1 = class_getInstanceMethod(self, @selector(fx_objectAtIndex:));
method_exchangeImplementations(oriMethod1, swiMethod1);
// 交换取下标方法
Method oriMethod2 = class_getInstanceMethod(self, @selector(objectAtIndexedSubscript:));
Method swiMethod2 = class_getInstanceMethod(self, @selector(fx_objectAtIndexedSubscript:));
method_exchangeImplementations(oriMethod2, swiMethod2);
}
- (void)fx_objectAtIndex:(NSInteger)index {
if (index > self.count - 1) {
NSLog(@"objectAtIndex————————数组越界");
return;
}
return [self fx_objectAtIndex:index];
}
- (void)fx_objectAtIndexedSubscript:(NSInteger)index {
if (index > self.count - 1) {
NSLog(@"取下标————————数组越界");
return;
}
return [self fx_objectAtIndexedSubscript:index];
}
@end
然而程序还是无情的崩了...

其实在iOS中NSNumber、NSArray、NSDictionary等这些类都是类簇(Class Clusters),一个NSArray的实现可能由多个类组成。所以如果想对NSArray进行方法交换,必须获取到其真身进行方法交换,直接对NSArray进行操作是无效的
以下是NSArray和NSDictionary本类的类名

这样就好办了,可以使用runtime取出本类

2.坑点一
黑魔法最好写成单例,避免多次交换
比如说添加了[NSArray load]代码,方法实现又交换回去了导致了崩溃

将+load方法改写成单例(虽然不常见,但也要避免)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 交换objectAtIndex方法
Method oriMethod1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method swiMethod1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(fx_objectAtIndex:));
method_exchangeImplementations(oriMethod1, swiMethod1);
// 交换取下标方法
Method oriMethod2 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
Method swiMethod2 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(fx_objectAtIndexedSubscript:));
method_exchangeImplementations(oriMethod2, swiMethod2);
});
}
3.坑点二
①子类交换父类实现的方法
- 父类
FXPerson类中有-doInstance方法,子类FXSon类没有重写 -
FXSon类新建分类做了方法交换,新方法中调用旧方法 -
FXPerson类、FXSon类调用-doInstance

子类打印出结果,而父类调用却崩溃了,为什么会这样呢?
因为FXSon类交换方法时取得doInstance先在本类搜索方法,再往父类里查找,在FXFather中找到了方法实现就把它跟新方法进行交换了。可是新方法是在FXSon分类中的,FXFather找不到imp就unrecognized selector sent to instance 0x600002334250
所以这种情况下应该只交换子类的方法,不动父类的方法
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod(self, @selector(doInstance));
Method swiMethod = class_getInstanceMethod(self, @selector(fx_doInstance));
BOOL didAddMethod = class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(self, @selector(fx_doInstance), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
});
}
- 通过
class_addMethod给FXSon类添加方法(class_addMethod不会取代本类中已存在的实现,只会覆盖本类中继承父类的方法实现)- 取得新方法
swiMethod的实现和方法类型 - 往方法名
@selector(fx_doInstance)添加方法 -
class_addMethod把新方法实现放到旧方法名中,此刻调用doInstance就是调用fx_doInstance,但是调用fx_doInstance会崩溃
- 取得新方法
- 根据
didAddMethod判断是否添加成功- 添加成功说明之前本类没有实现——
class_replaceMethod替换方法 - 添加失败说明之前本类已有实现——
method_exchangeImplementations交换方法 -
class_replaceMethod用doInstance方法实现替换掉fx_doInstance中的方法实现
- 添加成功说明之前本类没有实现——


②FXPerson类只写了方法声明,没有方法实现,却做了方法交换——会造成死循环

-
doInstance方法中添加了新的方法实现 -
fx_doInstance方法中想用旧的方法实现替代之前的方法实现,可是找不到doInstance实现,导致class_replaceMethod无效->在fx_doInstance中调用fx_doInstance就会死循环

因此改变代码逻辑如下
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod(self, @selector(doInstance));
Method swiMethod = class_getInstanceMethod(self, @selector(fx_doInstance));
if (!oriMethod) {
class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd) {
NSLog(@"方法未实现");
}));
}
BOOL didAddMethod = class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(self, @selector(fx_doInstance), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
});
}
- 未实现方法时用新的方法实现添加方法,此时调用
doInstance就是调用fx_doInstance - 由于此时
fx_doInstance方法内部还是调用自己,用block修改fx_doInstance的实现,就可以断开死循环了 - 由于oriMethod(0x0)
,method_exchangeImplementations交换失败

4.注意事项
使用Method Swizzling有以下注意事项:
- 尽可能在
+load方法中交换方法 - 最好使用
单例保证只交换一次 - 自定义方法名不能产生冲突
- 对于系统方法要调用原始实现,避免对系统产生影响
- 做好注释(因为方法交换比较绕)
- 迫不得已情况下才去使用方法交换
这是一份做好封装的Method Swizzling交换方法
+ (void)FXMethodSwizzlingWithClass:(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) {
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd) {
NSLog(@"方法未实现");
}));
}
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
}