转载

iOS 札记1:Method Swizzling小记

  • 本文为CocoaChina网友南华coder投稿

导语:Method Swizzling是Objective-C中运行时中讨论较多的内容,本文主要介绍使用Method Swizzling遇到的问题和项目中使用的Swizzling方案

一、Method Swizzling简介

Method Swizzling的本质是在运行时交换方法实现(IMP),如hook系统方法,在原有的方法中,插入自己的业务需求。

1、Method Swizzling原理

  • Objective-C的消息机制:在 Objective-C 中调用一个方法, 实际上是在底层通过 objc_msgSend()发送一个消息。 而查找消息的唯一依据是selector的方法名。

//调用方法  
[obj doSomething];
//[obj doSomething]本质上是给obj发doSomething消息
objc_msgSend(obj,@selector(doSomething))
  • 每一个OC实例对象都保存有isa指针和实例变量,其中isa指针所属类,类维护一个运行时可接收的方法列表(MethodLists)方法列表(MethodLists)中保存selector的方法名和方法实现(IMP,指向Method实现的指针)的映射关系。在运行时,通过selecter找到匹配的IMP,从而找到的具体的实现函数。

iOS 札记1:Method Swizzling小记

MethodLists示意图.png

  • 开发中可以利用Objective-C的动态特性,在运行时替换selector对应的方法实现(IMP),达到给hook的目的。下图是利用Method Swizzling来替换selector对应IMP后的方法列表示意图。

iOS 札记1:Method Swizzling小记

hook后的MethodLists示意图.png

2、Method Swizzling使用

Method Swizzling的本质就是偷换selector的IMP,下面就Swizzle NSObject的description方法,简单举例:

#import "NSObject+Swizzle.h"
#import @implementation NSObject (Swizzle)
+ (void)load{
   //调换IMP
    Method originalMethod = class_getInstanceMethod([NSObject class], @selector(description));
    Method myMethod = class_getInstanceMethod([NSObject class], @selector(qs_description));
    method_exchangeImplementations(originalMethod, myMethod);
}
- (void)qs_description{
    NSLog(@"description 被 Swizzle 了");
    return [self qs_description];    
}
@end

说明:调用被hook的description方法,获取内容前,会打印“description 被 Swizzle 了”这样的日志。

3、Method Swizzling存在的问题

  • 不是线程安全的(Method swizzling is not atomic)

  • 改变了代码本来的行为(Changes behavior of un-owned code)

  • 潜在的命名冲突(Possible naming conflicts)

  • 改变方法的参数(Swizzling changes the method's arguments)

  • 继承问题(The order of swizzles matters)

  • 难以理解 (Difficult to understand)

  • 难以调试(Difficult to debug)

二、RSSwizzle:Method Swizzling的优雅方案

RSSwizzle线程安全的Method Swizzling方案,能够帮我们解决Method Swizzling的使用问题。介绍如下:

1、不是线程安全的(Method swizzling is not atomic)

  • 通常在 load方法中交换方法实现,如果在其他时机交换方法实现,需要考虑线程安全的问题。

  • RSSwizzle利用了自旋锁OSSpinLock保证线程安全。可以在任意时机交换方法实现。

2、 改变了代码本来的行为(Changes behavior of un-owned code)

  • 这正是Swizzle的目标。但是在Swizzle方法中,我们保留*调用原始实现的好习惯,能避免绝大多数问题。我们利用Swizzle,一般是为了在原始实现基础上,添加某些自己的业务需求,并不想刻意去破坏原有实现。

  • RSSwizzle提供调用原来实现的宏RSSWCallOriginal,很方便。

3、潜在的命名冲突(Possible naming conflicts)#####

  • 通常在替换的方法名前加前缀,可以很大程度上避免命名冲突冲突问题。

  • RSSwizzle在自定义的swizzle的静态方法完成方法替换,完全避免了命名冲突问题。

4、改变方法的参数(Swizzling changes the method's arguments)

  • 参数 _cmd 被篡改,正常调用Swizzle 的方法有问题。

//调用方法 
[self qs_setFrame:frame];  
//发消息
objc_msgSend(self, @selector(qs_setFrame:), frame);

说明:在运行时,寻找qs_setFrame:的方法实现, _cmd参数虽然是 qs_setFrame: ,但是实际上找到的方法实现是原始的 setFrame: 实现。

  • RSSwizzle的自定义的swizzle的静态方法解决这个问题。

5、继承问题(The order of swizzles matters)

  • 多个有继承关系的类的对象Swizzle时,先从父对象开始。 这样才能保证子类方法拿到父类中的被Swizzle的实现。

  • 在load中Swizzle不用担心这种问题,因为load类方法会默认从父类开始调用。

6、难以理解 (Difficult to understand)

  • 主要表现在调用原始实现,看起来像递归,有点懵。

  • RSSwizzle提供的宏RSSWCallOriginal让调用原始实现更容易,代码阅读性更强。

7、难以调试(Difficult to debug)

  • Debug时候打印出的backtrace(回溯),其中掺杂着被swizzle的方法名,看起来比较乱,所以命名清晰很重要;

  • RSSwizzle打印出来的命名很清晰,此外Swizzle了什么,最好有文档记录。

三、RSSwizzle的基础使用

RSSwizzle中提供了两种使用方式,一种是通过调用类方法来实现函数的替换,另一种是使用RSSwizzle定义的宏来进行函数的替换。

1、 使用类方法替换实例方法实现

/**
 参数1:要被替换的函数选择器
 参数2:要被替换的函数所在的类
 参数3: block中返回替换后的方法,block参数中需要返回一个方法函数,这个函数为要替换成的函数,要和原函数类型相同。在类中的函数默认都会有一个名为self的id参数
 参数4:此次替换用到的key
 */
[RSSwizzle swizzleInstanceMethod:@selector(touchesBegan:withEvent:) inClass:[ViewController class] newImpFactory:^id(RSSwizzleInfo *swizzleInfo) {
    return ^(__unsafe_unretained id self,NSSet* touches,UIEvent* event){
        NSLog(@"touchesBegan:withEvent:被Swizzle了");
    };
} mode:RSSwizzleModeAlways key:NULL];

2、 使用宏替换实例方法实现

 /*
 参数1:要被替换的函数所在的类
 参数2: 要被替换的函数选择器
 参数3:返回值类型,
 参数4:参数列表
 参数5:要替换的代码块,
 参数6:执行模式,
 参数7:key值标识,RSSwizzleModeOncePerClass模式下使用,其他情况置为NULL
 */
RSSwizzleInstanceMethod([ViewController class], @selector(touchesEnded:withEvent:), RSSWReturnType(void), RSSWArguments(NSSet *touches,UIEvent *event),RSSWReplacement({
    
    NSLog(@"touchesEnded:withEvent被Swizzle了");
    RSSWCallOriginal(touches,event);
}), RSSwizzleModeAlways, NULL);

3、 使用类方法替换类方法实现

/*
 参数1:要替换的函数选择器
 参数2:要替换此函数的类
 参数3:block中返回替换后的方法,block参数中需要返回一个方法函数,这个函数为要替换成的函数,要和原函数类型相同。在类中的函数默认都会有一个名为self的id参数
 */
[RSSwizzle swizzleClassMethod:@selector(testClassMethod1) inClass:[ViewController class] newImpFactory:^id(RSSwizzleInfo *swizzleInfo) {
    
    return ^(__unsafe_unretained id self){
        NSLog(@"Class testClassMethod1 Swizzle");
    };
}];

4、使用宏替换类方法实现

/*
 参数1:要替换方法的类
 参数2:要替换的方法选择器
 参数3:方法的返回值类型
 参数4:方法的参数列表
 参数5:要替换的方法代码块
 */
RSSwizzleClassMethod(NSClassFromString(@"ViewController"), NSSelectorFromString(@"testClassMethod2"), RSSWReturnType(void), RSSWArguments(), RSSWReplacement({
    //先执行原始方法
    RSSWCallOriginal();
    NSLog(@"Class testClassMethod2 Swizzle");
}));

说明:RSSwizzle还提供了Swizzle模式,使用Swizzle实例方法时候需要用到。Swizzle类方法,默认RSSwizzleModeAlways,定义如下:

typedef NS_ENUM(NSUInteger, RSSwizzleMode) {
    //任何情况下 始终执行替换操作
    RSSwizzleModeAlways = 0,
    //相同key标识的替换操作只会被执行一次
    RSSwizzleModeOncePerClass = 1,
    //相同key标识的替换操作在子类父类中只会被执行一次
    RSSwizzleModeOncePerClassAndSuperclasses = 2
};

四、一个使用Swizzling典型的错误案例

网络上很多博客介绍了使用Swizzling来防止重复点击UIButton,但是大部分都会有问题。

1、错误代码

一般在load中替换sendAction:to:forEvent:方法,主要代码如下:

+ (void)load {
    Method before   = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    Method after    = class_getInstanceMethod(self, @selector(qs_sendAction:to:forEvent:));
    method_exchangeImplementations(before, after);
}
- (void)qs_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    if ([NSDate date].timeIntervalSince1970 - self.qs_acceptEventTime < self.qs_acceptEventInterval) {
        return;
    }
    if (self.qs_acceptEventInterval > 0) {
        self.qs_acceptEventTime = [NSDate date].timeIntervalSince1970;
    } 
    [self qs_sendAction:action to:target forEvent:event];
 }

错误现象:

点击UITabBar上按钮会crash, 提示类似于:[UITabBarButton qs_acceptEventTime]: unrecognized selector sent to instance ...。

错误原因:

1)UITabBarButton是UITabBarController中各个子控制器在工具条中对应的按钮,是UITabBar的私有属性,UITabBarButton的父类是UIControl,而UIButton的父类也是UIControl,sendAction:to:forEvent:是UIControl的实例方法;

2) 在UIButton类中没有sendAction:to:forEvent:这个方法实现,通过class_getInstanceMethod() 获取的是父类的 Method 对象,使用 method_exchangeImplementations() 就把父类的原始实现(IMP)跟自己的 Swizzle 实现交换了。这就导致UIControl的其他子类,如UITabBarButton在被点击后,都调用了UIButton的Swizzle 实现,发生了严重的Crash问题。

说明:虽然在UIControl的分类的load方法交换方法实现,能解决问题,我们将Swizzling的影响扩大很多倍,不是理想的做法。下面介绍解决办法。

2、解决办法

在项目直接使用method_exchangeImplementations很危险,甚至导致Crash,在项目中不建议这么做。可采用的解决办法有两种:

方法A

原理:如果类中没有实现 Original Selector 对应的方法,那就通过class_addMethod方法为Original Selector增加Swizzle 的实现,通过class_replaceMethod修改Swizzle Selector 的 实现 为 Original 的实现;如果已经有Original Selector 对应的方法(通过class_addMethod方法添加是失败的), 这时才使用method_exchangeImplementations来直接交换。

代码如下

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(sendAction:to:forEvent:);
        SEL swizzledSelector = @selector(qs_sendAction:to:forEvent:);
 
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

说明1:class_addMethod方法可以为类添加新的方法实现(IMP),添加成功返回YES.。否则返回NO。如果选择器(select)已经有对应的方法实现(IMP), 添加也是失败的,利用这点可以检查是否有源方法实现,如果没有利用class_replaceMethod来将swizzledSelector和originalMethod对应设置好。

说明2:.class_replaceMethod用来替换类中的方法实现,会调用class_addMethod和method_setImplementation方法(直接设置某个方法的IMP)

方法B

原理:RSSwizzle完美避开了在load中使用method_exchangeImplementations交换方法的尴尬,基于Swizzle模式和class_replaceMethod完美控制了替换方法实现。

代码如下

+ (void)load{
    RSSwizzleInstanceMethod([UIButton class], @selector(sendAction:to:forEvent:), RSSWReturnType(void), RSSWArguments(SEL action,id target,UIEvent *event), RSSWReplacement({
           UIButton *btn = self;
            if ([NSDate date].timeIntervalSince1970 - btn.qs_acceptEventTime < btn.qs_acceptEventInterval) {
                return;
            }      
            if (btn.qs_acceptEventInterval > 0) {
                btn.qs_acceptEventTime = [NSDate date].timeIntervalSince1970;
            }        
            RSSWCallOriginal(action,target,event);    
    }), RSSwizzleModeAlways, NULL);
}

说明:RSSwizzleInstanceMethod宏实现方法实现的替换,代码更易阅读。

End

  • Demo地址

QSSwizzleKitDemo

  • 参考资料

Objective-C的hook方案(一): Method Swizzling

Objective-C Method Swizzling

  • 我是南华coder,一名北漂的初级iOS程序猿。iOS札记是我的一点学习笔记,不足之处,望批评指正。

正文到此结束
Loading...