iOS 札记1:Method Swizzling小记

导语: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,从而找到的具体的实现函数。

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

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

#import "NSObject+Swizzle.h"
#import <objc/runtime.h>

@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<UITouch *> *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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容