方法混写
在 Objctive-C 中,混写(Swizzling)指的是偷梁换柱,将一个东西透明的换成另外一个东西,在 Runtime 运行时替换方法实现。
这个技术有什么作用呢?我们利用混写技术可以改变那些没有源代码的对象的行为,这其中包括了一些系统对象的行为,也包括了一些第三方 SDK 。
给 NSNotificationCenter 添加日志
接下来展示一个案例,每次在给 NSNotificationCenter 添加观察者的时候就打印日志。
创建 NSObject+MethodSwizzle
创建一个 NSObject 的 Category,叫做 NSObject+MethodSwizzle 用于实现方法混写。
// NSObject+MethodSwizzle.h
#import <Foundation/Foundation.h>
// 为 NSObject 添加方法混写实现
@interface NSObject (MethodSwizzle)
+ (IMP)swizzleSelector:(SEL)origSelector withIMP:(IMP)newIMP;
@end
// NSObject+MethodSwizzle.m
#import "NSObject+MethodSwizzle.h"
#import <objc/runtime.h>
@implementation NSObject (MethodSwizzle)
// 把 origSelector 的当前实现替换为新的实现
+(IMP)swizzleSelector:(SEL)origSelector withIMP:(IMP)newIMP{
// 当前 class
Class class = [self class];
// 获取 origSelector 的 Method
Method origMethod = class_getInstanceMethod(class, origSelector);
// 获取 origSelector 的实现
IMP origIMP = method_getImplementation(origMethod);
// 若是 class_addMethod 给 class 成功添加了方法,那么返回 YES,否则返回 NO。
// class_addMethod 会覆盖父类的方法实现,若是覆写方法会返回 YES。
// class_addMethod 返回 NO,那么我们可以知道这个类是直接实现了 origSelector 对应的方法
BOOL isAdd = class_addMethod(class, origSelector, newIMP, method_getTypeEncoding(origMethod));
if (!isAdd) {
// 替换 origMethod 方法的实现
method_setImplementation(origMethod, newIMP);
}
// 返回 origSelector 对应的方法的实现
return origIMP;
}
@end
NSObject+MethodSwizzle 的 + (IMP)swizzleSelector:(SEL)origSelector withIMP:(IMP)newIMP;
方法用于把类中的 origSelector 这个 Selector 对应的方法实现替换为 newIMP 这个方法实现。
在做方法混写的时候我们需要考虑三个情况:
- 该类直接实现了这个方法
- 该类的类层次结构中的某个父类实现了这个方法
- 该类压根没有实现这个方法
我们使用了 class_addMethod 来处理以上三种情况。
创建 NSNotificationCenter+MethodSwizzle
创建 NSNotificationCenter 的 Category,叫做 NSNotificationCenter+MethodSwizzle 用于给 NSNotificationCenter 添加混写方法。
// NSNotificationCenter+MethodSwizzle.h
@interface NSNotificationCenter (MethodSwizzle)
// 给自己添加方法混写
+ (void)swizzleAddObserver;
@end
// NSNotificationCenter+MethodSwizzle.m
#import "NSNotificationCenter+MethodSwizzle.h"
#import "NSObject+MethodSwizzle.h"
@implementation NSNotificationCenter (MethodSwizzle)
typedef void (*voidIMP)(id,SEL,...);
static voidIMP sOrigAddObserver = NULL;
// 用于替换 NSNotificationCenter 的 addObserver:selector:name:object: 的默认实现
static void MyAddObserver(id self,SEL _cmd,id observer,SEL selector,NSString *name,id object){
// 打印日志
NSLog(@"添加观察者:%@",observer);
NSAssert(sOrigAddObserver, @"旧方法找不到");
if(sOrigAddObserver){
// 调用原来的方法实现
sOrigAddObserver(self,_cmd,observer,selector,name,object);
}
}
// 方法混写实现
+ (void)swizzleAddObserver{
// swizzleAddObserver 只能被调用一次,可以采用更好的方法来实现这个需求
NSAssert(!sOrigAddObserver, @"swizzleAddObserver 只能调用一次");
// 准备混写的方法
SEL sel = @selector(addObserver:selector:name:object:);
// 替换方法新实现,并拿到旧方法实现
sOrigAddObserver = (void *)[self swizzleSelector:sel withIMP:(IMP)MyAddObserver];
}
@end
NSNotificationCenter+MethodSwizzle 的swizzleAddObserver会将 addObserver:selector:name:object: 的系统默认实现替换为自己的实现 MyAddObserver,
MyAddObserver 在每次 NSNotificationCenter 添加观察者时会打印一个日志,然后再调用 addObserver:selector:name:object: 的系统默认实现。
在 ViewController 中使用
- (void)viewDidLoad {
[super viewDidLoad];
// 加载方法混写
[NSNotificationCenter swizzleAddObserver];
// 给 NSNotificationCenter 添加一个观察者
Observer *observer = [[Observer alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:observer
selector:@selector(somthingHappened:)
name:@"SomethingHappenedNotification"
object:nil];
}
控制台输出
添加观察者:<Observer: 0x60800001b660>
在执行 NSNotificationCenter 添加观察者的代码的同时,控制台输出了我们预埋的日志,
可以看出 NSNotificationCenter 的 addObserver:selector:name:object: 方法实现已经被我们替换为自己的方法实现了。
总结
方法混写是一个非常强大的技术,但是用不好也会引发一些莫名其妙的 bug 。
通常方法混写技术最好不要用于线上代码,但在开发过程中对于调试,性能调优和研究系统框架实现又是有非常大的作用。
参考
本文是《iOS 编程实战》的读书笔记,对阅读的内容进行总结。当我们看懂了之后,不一定懂;我们跟着书上代码敲了一遍之后,还是不一定懂;只有我们能够把自己理解的内容写下来或者通过其它方式表达出来的时候,这个才是真的懂了;