浅谈 iOS swizzle

7,424 阅读5分钟

简单使用

简介

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 是不是属于本类自己实现的?

  1. class_addMethod 返回 YES -> addMethod 成功,class中不存在 method,也就是存在父类中。addMethod之后,当前class也就存在method 了(覆盖了父类的方法)
  2. class_addMethod 返回 NO -> addMethod 失败,class中存在 method,说明当前方法属于当前class
  3. 判断之后,再执行 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,不影响其他对象

方案:

  1. 类本身支持。可以标记一下,在执行方法时,判断是否存在标记来判断是否执行swizzle 之后的方法。可以参考:第三方库 DZNEmptyDataSet(统一空白页)
  2. 动态生成一个当前对象所属类的子类,并将当前对象与子类关联。这样的话,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(@"------");
}

简单原理:

  1. 把待 hook 的 originalSelector 生成 aliasSelector
    1. 把待 hook 的 originalSelector 添加前缀aspects_ -> aliasSelector -> 用 block & aliasSelector生成 aspectContainer
    2. 通过 associated 把 aspectContainer 绑定到 self( 对象或class),key为 aliasSelector
  2. 把 originalSelector 的 IMP 设置为 _objc_msgForward(会触发消息转发,不会查询方法列表了)
  3. swizzle forwardInvocation
    1. 在自定义的 forwardInvocation 中通过 associated & selector -> aliasSelector 获取 aspectContainer
    2. 根据 before/insert/after 的规则执行 originalSelector & block

_objc_msgForward

_objc_msgForward 是一个函数指针(和 IMP 的类型一样),是用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward 会直接走消息转发。看一个不存在方法的例子:

❤️hook对象

动态生成一个当前对象的子类,并将当前对象与子类关联,然后替换子类的 forwardInvocation 方法(具体参考源码)。那么就可以将当前对象变成一个子类的实例,同时对于外部使用者而言,仍可以把它继续当成原对象使用,而且所有的 swizzle 操作都发生在子类,这样做的好处是你不需要去更改对象本身的类

Aspects 优点

  1. 不会影响其他对象
  2. 当你在 remove aspects 的时候,如果发现当前对象的 aspect 都被移除了,那么,你可以将 isa 指针重新指回对象本身的类,从而消除了该对象的 swizzle

Aspects 缺点

  1. 没有解决上面提到的问题二:originalMethod 中使用 _cmd 的问题,需要我们注意一下

参考:

juejin.cn/post/684490…

wereadteam.github.io/2016/06/30/…

www.cocoachina.com/ios/2017091…