苹果的“黑魔法”Method Swizzling,Method Swizzling本质上就是对IMP和SEL进行交换。
Method Swizzling 原理
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。
我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP,
我们可以利用 class_replaceMethod 来修改类,
我们可以利用 method_setImplementation 来直接设置某个方法的IMP,
归根结底,都是偷换了selector的IMP,如下图所示:
上面图一中selector2原本对应着IMP2,但是为了更方便的实现特定业务需求,我们在图二中添加了selector3和IMP3,并且让selector2指向了IMP3,而selector3则指向了IMP2,这样就实现了“方法互换”。
在OC语言的runtime特性中,调用一个对象的方法就是给这个对象发送消息。是通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL,这个SEL对应着一个IMP(一个IMP可以对应多个SEL),通过这个IMP找到对应的方法调用。
在每个类中都有一个Dispatch Table,这个Dispatch Table本质是将类中的SEL和IMP(可以理解为函数指针)进行对应。而我们的Method Swizzling就是对这个table进行了操作,让SEL对应另一个IMP。
Method Swizzling使用
在实现Method Swizzling时,核心代码主要就是一个runtime的C语言API:
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
实现思路
就拿页面统计的需求来说吧,这个需求在很多公司都很常见,我们下面的Demo就通过Method Swizzling简单的实现这个需求。
我们先给UIViewController添加一个Category,然后在Category中的 +(void)load
方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。
定义Method Swizzling中我们自定义的方法时,需要注意尽量加前缀,以防止和其他地方命名冲突,Method Swizzling的替换方法命名一定要是唯一的,至少在被替换的类中必须是唯一的。
#import "UIViewController+Extension.h"
#import <objc/runtime.h>
@implementation UIViewController (Extension)
+ (void)load {
//保证交换方法只执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originaMethod = class_getInstanceMethod(self, @selector(viewDidLoad));
Method swizzleMethod = class_getInstanceMethod(self, @selector(xf_viewDidLoad));
method_exchangeImplementations(originaMethod, swizzleMethod);
});
}
-(void)xf_viewDidLoad {
NSLog(@"%@",self.class);
[self xf_viewDidLoad];
}
@end
控制台输出内容:
2016-02-14 16:19:24.871 Zhihu[20086:4972571] PageViewController
2016-02-14 16:19:24.921 Zhihu[20086:4972571] UIInputWindowController
2016-02-14 16:19:25.430 Zhihu[20086:4972571] LanuchViewController
2016-02-14 16:19:30.073 Zhihu[20086:4972571] ContainerController
2016-02-14 16:19:30.100 Zhihu[20086:4972571] DetailViewController
看到上面的代码,肯定有人会问:你写错了吧,你在 xf_viewDidLoad
方法中又调用了 [self xf_viewDidLoad];
,这难道不会产生递归调用吗?
这其实并不会。
还记得我们上面的图一和图二吗?Method Swizzling的实现原理可以理解为”方法互换“。假设我们将A和B两个方法进行互换,向A方法发送消息时执行的却是B方法,向B方法发送消息时执行的是A方法。
例如我们上面的代码,系统调用UIViewController的 viewDidLoad
方法时,实际上执行的是我们实现的 xf_viewDidLoad
方法。而我们在xf_viewDidLoad
方法内部调用[self xf_viewDidLoad];
时,执行的是UIViewController的viewDidLoad方法。
Method Swizzling类簇
在我们项目开发过程中,经常因为NSArray数组越界或者NSDictionary的key或者value值为nil等问题导致的崩溃,对于这些问题苹果并不会报一个警告,而是直接崩溃,感觉苹果这样确实有点“太狠了”。
由此,我们可以根据上面所学,对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类进行Method Swizzling,实现方式还是按照上面的例子来做。但是….你发现Method Swizzling根本就不起作用,代码也没写错啊,到底是什么鬼?
这是因为Method Swizzling对NSArray这些的类簇是不起作用的。因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArray的objectAtIndex:
方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。
所以也就是我们对NSArray类进行操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。
下面我们实现了防止NSArray因为调用objectAtIndex:
方法,取下标时数组越界导致的崩溃:
#import "NSArray+Extension.h"
#import <objc/runtime.h>
@implementation NSArray (Extension)
+ (void)load {
[super load];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originaMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method swizzleMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(xf_objectAtIndex:));
method_exchangeImplementations(originaMethod, swizzleMethod);
});
}
- (id)xf_objectAtIndex:(NSUInteger)index {
if (self.count <= index) {
// 这里做一下异常处理,不然都不知道出错了。
@try {
return [self xf_objectAtIndex:index];
}
@catch (NSException *exception) {
// 在崩溃后会打印崩溃信息,方便我们调试。
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
}
@finally {}
} else {
return [self xf_objectAtIndex:index];
}
}
@end
大家发现了吗,__NSArrayI才是NSArray真正的类,而NSMutableArray又不一样???我们可以通过runtime函数获取真正的类:
objc_getClass("__NSArrayI")
下面我们列举一些常用的类簇的“真身”:
Method Swizzling 确实是个很好用的东东,使用的时候用注意,以免出现莫名的崩溃
Github上星最多的一个第三方库 - jrswizzle 这个库已经封装的很好了
最后更新: 2023年03月25日 22:39:55
本文链接: http://aeronxie.github.io/post/b2925bae.html
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可,转载请注明出处!