简单使用
简介
Method Swizzle的本质是在运行时交换方法实现(IMP),一般是在原有的方法中,插入自己的业务需求。
原理
Objective-C的消息机制:在 Objective-C 中调用一个方法, 实际上是在底层通过 objc_msgSend()发送一个消息。 而查找消息的唯一依据是selector的方法名。
[obj doSomething]; /// => objc_msgSend(obj,@selector(doSomething))
每一个OC实例对象都保存有isa指针和实例变量,其中isa指针所属类,类维护一个运行时可接收的方法列表(MethodLists); 方法列表(MethodLists)中保存selector & IMP的映射关系。在运行时,通过selecter找到匹配的IMP,从而找到的具体的实现函数。
开发中可以利用Objective-C的动态特性,在运行时替换selector对应的方法实现(IMP),达到给hook的目的。下图是利用 Method Swizzle 来替换selector对应IMP后的方法列表示意图。
例子
在description() 之前打印“description 被 Swizzle 了”这样的日志。
@implementation NSObject (Swizzle)
+ (void)load{
//调换IMP
Method originalMethod = class_getInstanceMethod([NSObject class], @selector(description));
Method newMethod = class_getInstanceMethod([NSObject class], @selector(replace_description));
method_exchangeImplementations(originalMethod, newMethod);
}
- (void)replace_description{
NSLog(@"description 被 Swizzle 了");
[self replace_description];
}
@end
使用swizzle时,我们应该注意哪些问题呢?
问题一:继承问题
如果 originalMethod 是其父类实现的,那么直接 method_exchangeImplementations 是把父类中的 originalMethod 给替换了,导致该父类以及其他子类调用的 originalMethod 也会被替换
解决: 通过 class_addMethod 判断 method 是不是属于本类自己实现的?
- class_addMethod 返回 YES -> addMethod 成功,class中不存在 method,也就是存在父类中。addMethod之后,当前class也就存在method 了(覆盖了父类的方法)
- class_addMethod 返回 NO -> addMethod 失败,class中存在 method,说明当前方法属于当前class
- 判断之后,再执行 exchange
代码:
@implementation Model (Swizzle)
+ (void)load {
Class class = [self class];
SEL originalSelector = @selector(hhh);
SEL swizzledSelector = @selector(new_hhh);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 添加 originalSelector->swizzle method 到 class
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) { // 说明originalSelector在父类中
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else { // 说明originalSelector在当前类中
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@end
问题二:方法的参数会被改变
如果 originalMethod 中使用了 _cmd参数,可能造成bug
@interface IncorrectSwizzleClass : NSObject
- (void) swizzleExample;
- (void) originalMethod;
@end
@implementation IncorrectSwizzleClass
- (void)swizzleExample {
Method m1 = class_getInstanceMethod([self class], @selector(originalMethod));
Method m2 = class_getInstanceMethod([self class], @selector(replaceImp));
method_exchangeImplementations(m1, m2);
}
- (void)originalMethod {
NSLog(@"方法名为 originalMethod,其 _cmd 的值为:%@",[NSString stringWithFormat:@"*** -%@", NSStringFromSelector(_cmd)]);
}
- (void)replaceImp {
/*
* 添加自己的逻辑:比如添加log
*/
[self replaceImp];
}
@end
- (void)incorrect {
NSLog(@"#################### incorrect #######################");
IncorrectSwizzleClass* example2 = [[IncorrectSwizzleClass alloc] init];
NSLog(@"## swizzle 之前,调用 originalMethod 的打印信息:");
[example2 originalMethod];
[example2 swizzleExample];
NSLog(@"## swizzle 之后,调用 originalMethod 的打印信息:");
[example2 originalMethod];
}
打印结果:
分析: 执行 OC方法时,默认会传递两个参数(self & _cmd) [self replaceImp]; /// 会被编译器变成 objc_msgSend(self, @selector(replaceImp)),方法的第二个参数是 @“replaceImp”,故 originalMethod 中打印的是 replaceImp。
解决:C方法+ method_setImplementation 的方式
@interface CorrectSwizzleClass : NSObject
- (void) swizzleExample;
- (void) originalMethod;
@end
static IMP __original_Method_Imp;
void replaceImp(id self, SEL _cmd) {
/*
* 添加自己的逻辑:比如添加 log
*/
((int(*)(id,SEL))__original_Method_Imp)(self, _cmd);
}
@implementation CorrectSwizzleClass
- (void)swizzleExample {
Method m = class_getInstanceMethod([self class],@selector(originalMethod));
/// method_setImplementation:return The previous implementation of the method
__original_Method_Imp = method_setImplementation(m,(IMP)replaceImp);
}
- (void)originalMethod {
NSLog(@"方法名为 originalMethod,其 _cmd 的值为:%@",[NSString stringWithFormat:@"*** -%@", NSStringFromSelector(_cmd)]);
}
@end
- (void)correct {
NSLog(@"#################### correct #######################");
CorrectSwizzleClass* example = [[CorrectSwizzleClass alloc] init];
NSLog(@"## swizzle 之前,调用 originalMethod 的打印信息:");
[example originalMethod];
[example swizzleExample];
NSLog(@"## swizzle 之后,调用 originalMethod 的打印信息:");
[example originalMethod];
}
打印结果:
问题三:如何做到对象级别的 swizzle?
只对某个对象进行 swizzle,不影响其他对象
方案:
- 类本身支持。可以标记一下,在执行方法时,判断是否存在标记来判断是否执行swizzle 之后的方法。可以参考:第三方库 DZNEmptyDataSet(统一空白页)
- 动态生成一个当前对象所属类的子类,并将当前对象与子类关联。这样的话,swizzle的都是其子类的方法,不会影响父类。可以参考:第三方库 Aspects
聊一下Aspects
Aspects属于AOP编程的库,源码总数不超过1000行,对外就暴露了两个方法。 使用方式:可以hook 类方法、对象实例方法,还有三种执行位置:before、insert、after
@interface NSObject (Aspects)
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
@end
例子:
/**
* 事件拦截
* 拦截UIViewController的viewDidLoad方法
*/
[UIViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo)
{
/**
* 添加我们要执行的代码,由于withOptions 是 AspectPositionAfter。
* 所以每个控制器的 viewDidLoad 触发都会执行下面的方法
*/
[self doSomethings];
} error:NULL];
- (void)doSomethings {
//TODO: 比如日志输出、统计代码
NSLog(@"------");
}
简单原理:
- 把待 hook 的 originalSelector 生成 aliasSelector
- 把待 hook 的 originalSelector 添加前缀aspects_ -> aliasSelector -> 用 block & aliasSelector生成 aspectContainer
- 通过 associated 把 aspectContainer 绑定到 self( 对象或class),key为 aliasSelector
- 把 originalSelector 的 IMP 设置为 _objc_msgForward(会触发消息转发,不会查询方法列表了)
- swizzle forwardInvocation
- 在自定义的 forwardInvocation 中通过 associated & selector -> aliasSelector 获取 aspectContainer
- 根据 before/insert/after 的规则执行 originalSelector & block
_objc_msgForward
_objc_msgForward
是一个函数指针(和 IMP 的类型一样),是用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward
会直接走消息转发。看一个不存在方法的例子:
❤️hook对象
动态生成一个当前对象的子类,并将当前对象与子类关联,然后替换子类的 forwardInvocation 方法(具体参考源码)。那么就可以将当前对象变成一个子类的实例,同时对于外部使用者而言,仍可以把它继续当成原对象使用,而且所有的 swizzle 操作都发生在子类,这样做的好处是你不需要去更改对象本身的类
Aspects 优点
- 不会影响其他对象
- 当你在 remove aspects 的时候,如果发现当前对象的 aspect 都被移除了,那么,你可以将 isa 指针重新指回对象本身的类,从而消除了该对象的 swizzle
Aspects 缺点
- 没有解决上面提到的问题二:originalMethod 中使用 _cmd 的问题,需要我们注意一下
参考: