Method Swizzling 原理
Method Swizzling可以帮助我们在运行时将两个方法交换,以保证在业务面相对象编程方式不被改变的情况下,进行AOP(Aspect-Oriented Programming,面向切面编程)。Method Swizzling的本质就是对IMP和SEL进行交换。
AOP 是一种编程范式,也可以说是一种编程思想,使用 AOP 可以解决 OOP(Object Oriented Programming,面向对象编程)由于切面需求导致单一职责被破坏的问题。通过 AOP 可以不侵入 OOP 开发,非常方便地插入切面需求功能。
Method Swizzling 使用
新建一个分类,在分类中进行Method Swizzling方法的交换。
#import "UIViewController+Swizzling.h"
#import <objc/runtime.h>
@implementation UIViewController (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(eb_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// class_addMethod() 函数返回成功表示被交换的方法没实现,然后会通过 class_addMethod() 函数先实现;返回失败则表示被交换方法已存在,可以直接进行 IMP 指针交换
if (class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
//进行方法的交换
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
//交换IMP指针
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)eb_viewWillAppear:(BOOL)animated {
//自己的代码逻辑,埋点
[self eb_viewWillAppear:animated];
}
Method Swizzling 注意点
- 在+load方法中执行
+load方法会在类初始化加载的时候调用,发生在main函数之前,按照编译顺序加载。如果在其他时候进行方法的交换,难以保证另外一个线程中不会同时调用被交换的方法,从而导致程序不能按预期执行。
- 被交换的方法必须是当前类的方法,不能是父类的方法。
比如父类A的B方法和子类C的D方法交换,预期是执行A的B方法是调用D方法,执行C的D方法是调用B方法。实际上执行父类A的B方法还是调用B方法,直接把父类的实现拷贝到子类也不会起作用。
- 交换的方法如果依赖了 cmd,那么交换后,如果 cmd 发生了变化,就会出现各种奇怪问题,而且这些问题还很难排查。特别是交换了系统方法,你无法保证系统方法内部是否依赖了 cmd。
__cmd是指每个函数中都会存在的一个隐藏参数,_cmd在Objective-C的方法中表示当前方法的selector,正如同self表示当前方法调用的对象实例。方法交换后显然原方法的cmd不同了,假如原函数有些逻辑是对_cmd进行的,这时候就会出现奇怪的错误。
举个例子,person类的一个方法,内部依赖__cmd做一些逻辑的判断,正常执行run方法,打印 跑了3步。
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
- (void)run{
NSString *str = NSStringFromSelector(_cmd);
if (str.length == 3) {
NSLog(@"跑了3步");
}else{
NSLog(@"跑了%ld步", str.length);
}
}
@end
对Person类的run方法交换。
#import "Person+Swizzling.h"
#import <objc/runtime.h>
@implementation Person (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
SEL originalSelector = @selector(run);
SEL swizzledSelector = @selector(my_run);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// class_addMethod() 函数返回成功表示被交换的方法没实现,然后会通过 class_addMethod() 函数先实现;返回失败则表示被交换方法已存在,可以直接进行 IMP 指针交换
if (class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
//进行方法的交换
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
//交换IMP指针
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)my_run {
[self my_run];
}
交换后再去执行run方法,打印输出,跑了6步。
- 方法交换命名冲突
如果方法交换后,在其他地方有重名方法,可能会导致方法交换失败。
#import "UIView+MyViewAdditions.h"
#import <objc/runtime.h>
typedef IMP *IMPPointer;
@implementation UIView (MyViewAdditions)
static void MySetFrame(id self, SEL _cmd, CGRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, CGRect frame);
static void MySetFrame(id self, SEL _cmd, CGRect frame) {
// do custom work
SetFrameIMP(self, _cmd, frame);
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
IMP imp = NULL;
Method method = class_getInstanceMethod(class, original);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(class, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}
}
if (imp && store) { *store = imp; }
return (imp != NULL);
}
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
如上使用函数指针的方式可以有效防止命名冲突的问题。
- Swizzling应该总是在dispatch_once中执行
这样能防止多次执行交换方法,因为多次交换后可能就是没有交换。
更多关于运行时方法交换的风险,你可以查看 Stackoverflow 上的问题讨论“What are the Dangers of Method Swizzling in Objective C?”。