Method Swizzling的剖析与实践

在iOS开发中,有时我们会有这样的需求:想改变某一个系统类的实现,但在项目中逐一修改工作量太过于庞大,这时Method Swizzling就显现出了ta的强大。
Method Swizzling是针对Objective-C中selector实现hook的方法,基于OC中的动态特性runtime,达到替换方法的目的。
基本原理
想理解Method Swizzling就需要了解Objective-C的基础消息机制。OC中的每一个类都有一个Dispatch Table,将每个方法的名字SEL(一个C字符串)与方法的实现IMP(指向方法实现函数开始位置的指针)分别对应起来。当我们执行方法A时,OC就会从Dispatch Table中找出方法A所对应的函数IMP,如果找到则执行函数内的代码,如下:
Method Swizzling其实就是利用OC的动态特性,利用runtime把方法A的实现与方法B的实现进行交换。
例如:我希望在每个视图控制器的-viewWillAppear执行后,在控制台输出@"yoho, swift~"。那么就可以用一个新的方法-cr_viewWillAppear来替换系统API,cr_viewWillAppear中打印类名,通过Method Swizzling替换后,当前类的Dispatch Table如下:
Swizzling后,当前我们在-viewWillAppear中调用[super viewWillAppear:animated];时,则会调用重定向后-cr_viewWillAppear方法的实现;在-cr_viewWillAppear中调用[self cr_viewWillAppear:animated];时,则会调用系统-viewWillAppear的实现。这也是为什么在Swizzling后不会出现递归的根本原因。
具体实现
- Method Swizzling是非原子性的,因此,应该放在
dispatch_once中执行,确保Swizzling代码只执行一次。同样的,包含Method Swizzling的dispatch_once代码,应该放在+(void)load方法中调用。(之所以不选择+initialize方法,是因为+load会在类初始加载时调用,而+initialize方法当前类的实例方法或类方法被调用时才执行,会增加程序中的不确定因素,可能导致并发问题),实例方法的例子:
@implementation UIViewController (CRSwizzling)
+ (void)load {
[self cr_swizzlingMethod:@selector(viewWillAppear:)
withAltSEL:@selector(cr_viewWillAppear:)];
}
+ (void)cr_swizzlingMethod:(SEL)originSEL withAltSEL:(SEL)altSEL {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = originSEL;
SEL swizzledSelector = altSEL;
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)cr_viewWillAppear:(BOOL)animated {
[self cr_viewWillAppear:animated];
NSLog(@"yoho, swift~");
}
@end
- 当有继承关系的类存在时,Swizzling会从父类开始。例如一个视图控制器SubViewController继承于ViewController,那么当SubViewController需要走Swizzling方法时,发现已经在父类中使用
dispatch_once执行过了,那么SubViewController的-viewWillAppear就会在调用[super viewWillAppear:animated];时,执行父类Swizzling时走过的步骤。
显示viewController的时候,控制台输出:
显示SubViewController的时候,控制台输出:
如上讲解的demo在->这里
注意事项 & 讨论
Swizzling是基于runtime的一种巴拉巴拉魔法,所以最好遵循一些原则,避免出现魔法四溢的现象。
- 需要调用父类原始方法。毕竟我们不知道闭源的原始方法内部实现是什么╮(╯_╰)╭
- 给重定向的方法加前缀。类似于示例demo中的cr_xx,避免出现冲突。
- 按需使用。对于runtime的巴拉巴拉魔法,用好了是利刃可以势如破竹,用不好反而容易伤到自己。
推荐
其实如上这种,通过Method Swizzling动态更改方法的方式被称为——Aspect Oriented Programming(面向切面编程),简称AOP。
关于Objective-C中的AOP应用,GitHub上已经有了很棒的库——Aspects。Aspects通过runtime封装了一套很完善的hook方案,在日常项目中如果需要使用Method Swizzling,可以直接用CocoaPods导入Aspects,使用起来也非常便捷。
以上。
参考资源:
Method Swizzling
Objective-C的hook方案(一): Method Swizzling
What are the Dangers of Method Swizzling in Objective C?