method-swizzling是什么?
-
method-swizzling
的含义是方法交换
,其主要作用是在运行时将一个方法的实现替换成另一个方法的实现
,这就是我们常说的iOS黑魔法
- 在OC中就是
利用method-swizzling实现AOP
,其中AOP
(Aspect Oriented Programming,面向切面编程
)是一种编程的思想,区别于OOP(面向对象编程)
-
OOP
和AOP
都是一种编程的思想 -
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
method-swizzling使用过程中的问题分析
坑点1:method-swizzling使用过程中的一次性问题
所谓的一次性就是:mehod-swizzling
写在load
方法中,而load
方法会主动调用多次
,这样会导致方法的重复交换
,使方法sel的指向又恢复成原来的imp的问题
解决方案
可以通过单例设计
原则,使方法交换只执行一次
,在OC中可以通过dispatch_once
实现单例
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[HTRuntimeTool ht_bestMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(ht_studentInstanceMethod)];
});
}
坑点2:子类没有实现,父类实现了
- 创建
HTPerson
类,并且实现personInstanceMethod
方法
@interface HTPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation HTPerson
- (void)personInstanceMethod {
NSLog(@"person对象方法:%s", __func__);
}
@end
- 创建
HTTeacher
类,继承子HTPerson
,没有实现personInstanceMethod
,代码如下
@interface HTTeacher : HTPerson
@end
/** HTTeacher.m 文件*/
#import "HTTeacher.h"
#import "HTRuntimeTool.h"
@implementation HTTeacher
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[HTRuntimeTool ht_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(teacherInstanceMethod)];
});
}
- (void)teacherInstanceMethod {
// 是否会产生递归?--不会产生递归,原因是teacherInstanceMethod 会走 oriIMP,即personInstanceMethod的实现中去
[self teacherInstanceMethod];
NSLog(@"teacher对象方法:%s", __func__);
}
@end
-
HTRuntimeTool.m
中的方法如下
+ (void)ht_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.m
中调用如下
#import "ViewController.h"
#import "HTTeacher.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
HTTeacher *t = [HTTeacher alloc];
[t personInstanceMethod];
HTPerson *p = [HTPerson alloc];
[p personInstanceMethod];
}
@end
- 通过实际代码的调试,发现会在
p
调用personInstanceMethod
方法时崩溃,下面来进行详细说明
-
[t personInstanceMethod];
中不报错是因为HTTeacher
类在+ (void)load
方法中将personInstanceMethod
交换成了teacherInstanceMethod
,对于HTTeacher
的对象来说,这两个方法都是可以找到的,所以不会报错 - 崩溃的点在于
[p personInstanceMethod];
,其本质原因:-
HTTeacher
类在+ (void)load
方法中将personInstanceMethod
交换成了teacherInstanceMethod
, -
[p personInstanceMethod];
相当于调用的是HTTeacher
的teacherInstanceMethod
方法,然后需要取HTPerson
类中查找teacherInstanceMethod
方法,但是HTPerson
中没有方法实现,即相关的imp找不到
,所以就崩溃了
-
优化:避免imp找不到
通过class_addMethod
尝试添加你要交换的方法
- 如果
添加成功
,即类中没有这个方法,则通过class_replaceMethod
进行替换
,其内部会调用class_addMethod
进行添加 - 如果
添加不成功
,即类中有这个方法,则通过method_exchangeImplementations
进行交换
+ (void)ht_betterMethodSwizzlingWithClass:(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
的源码实现
/**********************************************************************
* 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(false);
} else {
// 替换方法:返回替换之前的方法实现
result = _method_setImplementation(cls, m, imp);
}
} else { // 不存在
// fixme optimize
// 新建一个method_list,添加方法
method_list_t *newlist;
newlist = (method_list_t *)calloc(method_list_t::byteSize(method_t::bigSize, 1), 1);
newlist->entsizeAndFlags =
(uint32_t)sizeof(struct method_t::big) | fixed_up_method_list;
newlist->count = 1;
auto &first = newlist->begin()->big();
first.name = name;
first.types = strdupIfMutable(types);
first.imp = imp;
// 对rwe进行处理,将增的newlist加进去
addMethods_finish(cls, newlist);
result = nil;
}
return result;
}
坑点3:子类没有实现,父类也没有实现,下面的调用有什么问题?
在调用personInstanceMethod
方法时,父类HTPerson
中只有声明,没有实现,子类HTTeacher
中既没有声明,也没有实现
- 运行代码,报错结果如下所示
原因是 栈溢出,递归死循环了
,那么为什么会发生递归呢?----主要是因为
-
personInstanceMethod
没有实现,即oriMethod
空,调用class_addMethod
方法后,oriSEL
和swizzledSEL
的方法实现都是swiMethod
,然后在方法替class_replaceMethod
时,替换失败 - 当我们调用
personInstanceMethod(oriSEL)
时,也就是swiMethod
,会进入HTTeacher
中的teacherInstanceMethod
方法,然后这个方法中又调用了teacherInstanceMethod
,此时的teacherInstanceMethod
并没有指向oriMethod
,然后导致了自己调自己
,即递归死循环
优化:避免递归死循环
如果oriMethod为空
,为了避免方法交换没有意义,而被废弃,需要做一些事情
- 通过
class_addMethod
给oriSEL
添加swiMethod
方法 - 通过
method_setImplementation
将swiMethod
的IMP
指向不做任何事的空实现
+ (void)ht_bestMethodSwizzlingWithClass:(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为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
//oriSEL:personInstanceMethod
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);
}
}
method-swizzling的应用
method-swizzling
最常用的应用是防止数组、字典等越界崩溃
在iOS中NSNumber
、NSArray
、NSDictionary
等这些类都是类簇
,一个NSArray
的实现可能由多个类组成
。所以如果想对NSArray进行Swizzling,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的
。
- 下面列举了
NSArray
和NSDictionary
本类的类名,可以通过Runtime函数取出本类。
类名 | 真身 |
---|---|
NSArray | __NSArrayI |
NSMutableArray | __NSArrayM |
NSDictionary | __NSDictionaryI |
NSMutableDictionary | __NSDictionaryM |
NSArray防崩溃处理
- 创建
NSArray
的一个分类HT
#import "NSArray+HT.h"
#import <objc/runtime.h>
@implementation NSArray (HT)
//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(ht_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
});
}
- (id)ht_objectAtIndex:(NSUInteger)index{
//判断下标是否越界,如果越界就进入异常拦截
if (self.count-1 < index) {
// 这里做一下异常处理,不然都不知道出错了。
#ifdef DEBUG // 调试阶段
return [self ht_objectAtIndex:index];
#else // 发布阶段
@try {
return [self ht_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 ht_objectAtIndex:index];
}
}
@end
- 测试代码如下
NSArray *array = @[@"1", @"2", @"3"];
[array objectAtIndex:3];
- 打印结果如下,会输出崩溃的日志,但是实际并不会崩溃