简介
AOP
跟面向对象编程(OOP)一样,面向切面编程(AOP)是一种编程范例。这种编程思想旨在通过横切面,提高项目的模块化程度。通过对现有代码进行切入,在切入点单独指定和实现代码,通常是非业务逻辑的核心行为,比如日志记录。这种行为不影响原有业务逻辑,不会使核心代码变得混乱。
AOP 的编程思想特别适合比如日志记录,又比如 APM 功能的开发,实现底层通用抽象能力并且对业务无侵入。
Hook方式
在 iOS 开发中,由于 Objective-C 动态语言特性和 Swift 语言的特性以及动态库的加载特性,通常有以下几种实现 Hook 的方式。
从 NSObject 集成下来的 Swift 类,或者说方法派发中动态派发的 Swift 类或者对象,看成是 Objective-C 对应分支,图中 Swift 分支是指纯 Swift 的情况。
当然,结合编译过程来说,宏或者是代码扫描中按规则完成代码替换,也算是一种变相的实现 AOP 的手段,不过从常用实践和绝对意义上暂不把这些方式并入本文的介绍中。本文主要介绍 Method Swizzle 存在的问题和实践。
Method Swizzle
原理
Method本质是objc_method结构体,method swizzle 本质是修改了 selector 跟 IMP 的映射关系。
ObjectiveC
/// An opaque type that represents a method in a class definition.typedef struct objc_method *Method; // 本质是一个结构体
struct objc_method {
SEL method_name; // 方法名称
char *method_types; // 参数和返回类型的描述字串
IMP method_imp; // 方法的具体的实现的指针
}
问题和解决
关于 Method Swizzle 有个三方库叫做 RSSwizzle ,结合该库作者提出的几个 Method Swizzle 中常见的问题,加上平时使用中应注意的问题来一一梳理。
- 改变不属于本身的代码的行为
- 难以理解(代码阅读起来感觉是递归的)
- 难以调试(函数堆栈看起来有些跳跃)
重复执行
重复执行会导致 Swizzle 过来的方法又还原回去,好在有函数可以保证只执行一次
ObjectiveC
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self hook];
});
线程安全
在进行动态函数修改的时候,有可能其他线程也在做同样的操作
Method Swizzle 的执行的确不是线程安全的,这表明在多线程并发的情况下会导致 Crash,有几种保障线程安全的方式。
Load 方法
在+ (void)load
中执行,虽然可能会对启动时长有影响,好在影响很小
ObjectiveC
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self hook];
});
}
主线程
Hook UI 相关类的方法的时候,也可以程序执行时,即在 main 函数之后,在主线程做 Hook
ObjectiveC
func startHook() {
UIViewController.startHook()
UINavigationController.startHook()
}
子类未实现被替换的方法
子类尝试 Hook 子类中的方法,结果子类没有实现被 Hook 的方法,但是父类的方法中实现了这个方法。导致子类中 swizzle 了父类的原方法,而不是子类的原方法。
当父类的原方法被另外调用的时候,会出现两种问题
- 找不到方法
- 陷入死循环
父类未实现hook后的方法
类 | 被Hook方法 | 调用的新方法 |
---|---|---|
ParentViewController | sayHello | - |
ChildViewController | - | swizzled_sayHello |
父类被Hook的方法不会被另外调用的话,不会出问题。
ObjectiveC
// 调用:
ChildViewController *vc = [[ChildViewController alloc] init];
[vc sayHello];
// 打印:
-[ChildViewController swizzled_sayHello]
-[ParentViewController sayHello]
当父类调用自身被Hook的方法时,会因为找不到对应的swizzled方法而报错。
ObjectiveC
// 调用:
ChildViewController *vc = [[ChildViewController alloc] init];
[vc sayHello];
ParentViewController *pvc = [[ParentViewController alloc] init];
[pvc sayHello];
// 打印:
-[ChildViewController swizzled_sayHello]
-[ParentViewController sayHello]
-[ChildViewController swizzled_sayHello]
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ParentViewController swizzled_sayHello]: unrecognized selector sent to instance 0x1057071c0'
父类实现了hook后的方法
类 | 被Hook方法 | 调用的新方法 |
---|---|---|
ParentViewController | sayHello | swizzled_sayHello |
ChildViewController | - | swizzled_sayHello |
父类被Hook的方法不会被另外调用的话,不会出问题。
ObjectiveC
// 调用:
ChildViewController *vc = [[ChildViewController alloc] init];
[vc sayHello];
// 打印:
-[ChildViewController swizzled_sayHello]
-[ParentViewController sayHello]
当父类调用自身被Hook的方法时,会陷入死循环。
ObjectiveC
// 调用:
ChildViewController *vc = [[ChildViewController alloc] init];
[vc sayHello];
ParentViewController *pvc = [[ParentViewController alloc] init];
[pvc sayHello];
// 打印:
-[ChildViewController swizzled_sayHello]
-[ParentViewController sayHello]
-[ChildViewController swizzled_sayHello]
// 开始死循环
-[ParentViewController swizzled_sayHello]
-[ParentViewController swizzled_sayHello]
-[ParentViewController swizzled_sayHello]
-[ParentViewController swizzled_sayHello]
-[ParentViewController swizzled_sayHello]
-[ParentViewController swizzled_sayHello]
-[ParentViewController swizzled_sayHello]
//...
父类也未实现被hook的方法
这样会导致 Hook 的 method swizzle 方法中的 originalMethod 为 nil,输出如下。
ObjectiveC
// 打印:
__func__ -[ChildViewController swizzled_sayHello] _cmd sayHello
// 开始死循环
__func__ -[ChildViewController swizzled_sayHello] _cmd swizzled_sayHello
__func__ -[ChildViewController swizzled_sayHello] _cmd swizzled_sayHello
__func__ -[ChildViewController swizzled_sayHello] _cmd swizzled_sayHello
__func__ -[ChildViewController swizzled_sayHello] _cmd swizzled_sayHello
//...
解决办法
确保被 Hook 的类中,存在被 Hook 的方法
判断被 Hook 的方法存在,即 originalMethod 不为 nil。当父类中存在被 Hook 的方法时,originalMethod 就是存在的,并不能直接确定子类是否存在被 Hook 的方法。
ObjectiveC
if (!originalMethod) {
class_addMethod([self class], originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
method_setImplementation(swizzledMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}
向子类中添加被 Hook 的方法,如果已经存在,添加失败则直接 exchangeImplementation 。如果添加成功,使用 class_replaceMethod 交换。这种方式就不需要判断父类中是否还存在对应的方法,即不需要使用上面的写法。
ObjectiveC
BOOL didAddMethod = class_addMethod([self class], originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod([self class], swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
函数 _cmd 变化
OC 的方法中,默认会传递 self 和 _cmd 两个参数,一个是接收消息的对象实例,一个是 selector。
ObjectiveC
// 转换成
((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("sayHello"));
// 相当于
objc_msgSend(self, @selector("sayHello")) ;
经过 method swizzle 之后,_cmd 参数发生变化。如果有方法继续使用原来的 _cmd 参数,可能会导致问题。
ObjectiveC
__func__ -[ChildViewController swizzled_sayHello] _cmd sayHello
__func__ -[ChildViewController sayHello] _cmd swizzled_sayHello
命名冲突
命名冲突不算是 Mehod Swizzle 独有的问题,本身 OC 在没有命名空间的情况下,所有的方法都存在命名冲突的问题。
通常我们会加类似 swizzled_ 这样的前缀来区分和避免命名冲突
ObjectiveC
@interface NSView : NSObject
- (void)setFrame:(NSRect)frame;
@end@implementation NSView (MyViewAdditions)
- (void)my_setFrame:(NSRect)frame {
// do custom work
[self my_setFrame:frame];
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}
@end
也可以使用静态函数和函数指针的方法来彻底避免
ObjectiveC
@implementation NSView (MyViewAdditions)static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);
static void MySetFrame(id self, SEL _cmd, NSRect frame) {
// do custom work
SetFrameIMP(self, _cmd, frame);
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}
@end
把 swizzling method 提取出来定义如下
ObjectiveC
typedef IMP *IMPPointer;
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);
}
@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end
未调回原方法
Method Swizzle 中需要调回原方法,但是也经常出现忘记调回原方法的情况。就像在OC中的一些生命周期忘记调用Super的生命周期方法一样,也是需要注意的问题,否则其他依赖于原方法的逻辑可能会出现bug。
Swizzle 顺序
方法交换的顺序有很大的影响。假设 setFrame:
方法只定义在 UIView 中,方法交换的顺序如下。
ObjectiveC
[UIButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[UIControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[UIView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
当先交换 UIButton 中的方法的时候,由于 UIButton 本身未实现 setFrame:
方法,需要添加一个新的 setFrame:
方法在 UIButton 类中。新的setFrame:
方法直接从 UIView 中对应的方法拷贝过来。交换 UIControl 方法的时候也是如此。
这时候在调用一个 button 中的setFrame:
方法时,首先调用 swizzle 后的方法 my_buttonSetFrame:
,然后就会直接调用自身实现的,原本在 UIView 中的 setFrame:
方法。这样 UIControl 和 UIView 中 swizzle 后的方法 my_controlSetFrame:
和my_viewSetFrame:
就都不会调用到。
如果方法交换的顺序如下。
ObjectiveC
[UIView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[UIControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[UIButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
由于 UIView 在交换的最前面,所以可以正确的触发my_viewSetFrame:
。类似的由于 UIControl 在UIButton 前面,所以可以正确的触发my_controlSetFrame:
方法。
如何才能保证这样的顺序呢,使用+(void)load
来调用方法交换,因为 load 方法保证了父类的方法先被调用。
实践
iOS method swizzle的实践,也需要看具体的使用场景,总体来说避免以上的问题即可。
通常我们 hook UIViewController的生命周期,UINavigationController的一些方法,这些方法是明确已实现,也不存在调用顺序的问题。主要需要注意调回原方法,避免耗时操作。
- 通常会对Swizzle进行封装
ObjectiveC
#import "NSObject+Swizzle.h"
#import <objc/runtime.h>
@implementation NSObject (Swizzle)
+ (BOOL)swizzle:(Class)originalClass Method:(SEL)originalSelector withMethod:(SEL)swizzledSelector
{
if (!(originalClass && originalSelector && swizzledSelector)) {
return NO;
}
Class class = [self class];
Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(originalClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
return YES;
}
@end
-
针对被 Hook 方法的不同,在 load 方法或者主线程中调用。
-
也可以更加严谨的使用 RSSwizzle 中的实现,避免命名冲突和 _cmd 改变的问题
总结
method swizzle 作为 hook 的一种方式和AOP编程思想的实现,有利有弊,既然没有银弹,我们还是需要知晓原理尽量避开一些常见的坑。
引用