详讲Runtime方法交换(class_addMethod ,class_replaceMethod和method_exchangeImplementations)

前言

最近在整理博客,发现自己之前写的关于Runtime拦截替换方法的一篇文章《12- Runtime基础使用场景-拦截替换方法(class_addMethod ,class_replaceMethod和method_exchangeImplementations)》,大家还是很关注的,文章大家看完依然疑问,但是由于当时生产力不足和后续的种种原因,并没有补发文章。最近工作不忙,正好结合自己工作中遇到的Runtime使用场景,补上自己这个坑。

之前留给大家的疑惑主要有两点,如下:

  • 第一,下边这三个方法的具体作用:

    • class_addMethod
    • class_replaceMethod
    • method_exchangeImplementations
  • 第二,为什么方法交换需要用到class_addMethodclass_replaceMethod这两个方法?

要回答这两个问题,我们有必要了解一下OC中的类对象结构和消息机制,往下看。

类结构

OC中的类对象结构和消息机制包含的内容其实很多,避免篇幅过长,这里我只简单的说一下和本文相关的部分。

首先,在OC中有实例对象、类对象、元类对象,如下:

[Student class]; // 是类对象
Student *stu = [Student new]; // p是实例对象
object_getClass([Student class]) // 元类

类对象(即我们日常叫称为的),它是基于实例对象的一种抽象定义,比如说喵咪小花都属于猫,那么猫就是一种抽象的概念,定义了猫的外形、活动特定等等属性。我们用代码的方法可以这么定义:

猫 *小花 = [猫 new];

所以OC中,类对象也是一个定义了一个实例对象包含了哪些方法、属性、父类是谁等信息的抽象对象。OC的底层是c/c++实现,所以OC中的类结构是采用c++中的结构体来表示的。下边是我写的一个伪结构体,从伪结构体中我们可以知道,类对象中有方法列表父类这个信息。

struct objc_class {
    Class super_class        // 当前类的父类
    struct objc_method_list * * methodLists         // 方法列表 
    //......  其它信息这里忽略
}

(类对象的真实结构并不是这样,这里我们也可以忽略它的真实结构。即便你日后了解了类的真实结构,也不会影响到下边的结论。)
super_class 就是当前类的父类。
methodLists实际上是一个数组,保存着类有哪些方法。这里可以提到元类了,实例对象是一种结构,而类对象和元类对象是另外一种结构。关于类对象和元类对象的区别,对于本文只要记住一个:对象方法是保存在类对象的methodLists中,而类方法保存在元类的methodLists中

methodLists数组中保存着结构体method_t,这个结构体包含了我们平时写的方法的信息。伪结构体如下:

struct method_t {
    SEL method_name 
    IMP method_imp  
    char * types  
}  

SEL method_name sel就是方法的名字
IMP method_imp imp保存着一个指针,这个指针指向函数的具体实现地址。 所以,方法真正的实现是单独保存在一个地方的,它的实现地址交给imp保存。当我们执行方法的时候,实际上是从method_t结构体中找到imp,然后调用。
这里有一点很重要,Runtime替换方法实际上就是替换imp。所以产生了替换了方法之后,明明你调用的是methodA,但是执行的是methodB的效果。因为methodA对应的method_t结构体中的imp实际保存的是methodB方法的实现地址了。
types 可以简单的认为到能代表这个方法的特定字符,用法我们暂时忽略。 有一点需要注意,如果你修改了imp为新的imp外,同时修改types改成新方法的types,这样才是真正把method_t结构体改成了新的方法。

消息机制

一般在OC中调用方法,底层会转成一个objc_msgSend的c++函数。比如说:[stu instanceMthod],底层实际上是

objc_msgSend([Student class], @Seletor(instanceMthod))

表示我们从Person这个类对象结构体中查找instanceMthod这个方法,找到它并且调用。查找调用这个方法的过程,我们可以简单认为就是消息机制。 下边我们简单说一下消息机制的流程,还是用[stu instanceMthod]来作为例子:

假如这个方法在Student的父类Person中

  • 首先,实例对象p在对应的类对象Student的方法列表中methodLists查找instanceMthod方法,没有找到。(如果能找到,那么就直接调用对应method_t结构中的imp执行方法,结束查找)
  • 通过类对象Student的superClass找到父类对象Person,在父类的的方法列表中methodLists查找instanceMthod方法,找到了,调用方法,结束查找。(如果在父类对象Person、以及Person的父类对象NSObject没有找到该方法,那么会进入消息转发的另外两个阶段,如果这两个阶段还是没有找到要调用的方法,那么就会报经典错误unrecognized selector sent to instance

当然这个消息机制的过程是非常简陋的,实际上在进入methodLists查找之前,会先进入方法缓存cache中查找,有兴趣你可以自己多了解一下。

三个方法的作用

class_addMethod

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 

作用:就是动态在类对象cls中添加一个方法。参数SELIMPtypes我们上边都提到过。
注意:如果cls中已经有了要添加的方法声明和方法实现,那么添加失败,返回NO。如果没有声明和实现方法,或者只声明没有方法实现,都可以添加成功,返回YES。

class_replaceMethod

IMP class_replaceMethod(Class cls, SEL name, IMP newImp, const char *newTypes) 

作用:把类对象中的cls的方法的imp替换成newImp,同时还需要替换newTypes。

method_exchangeImplementations

void method_exchangeImplementations(Method m1, Method m2)

作用:Method这里可以认为就是上边说到的method_t结构体,交换m1和m2,实际上就是交换两个method_t结构体中的IMP和types。
注意:如果imp为nil,交换操作将失败。

为什么会用到dispatch_once

dispatch_once我们日常最长用的就是单例。保证在程序运行过程中,其代码块内的代码只执行一次。Runtime交换方法之所以会用到dispatch_once,是为了防止load被手动调用。 load方法的调用时机是在main函数被调用之前,且只被系统调用一次。正常情况下,我们无需再手动调用load方法,但是为了防止意外,所以加了dispatch_once,保证替换方
法的Runtime代码只能执行一次,从而避免方法有替换回去。

method_exchangeImplementations

一般我们交换方法实现的场景比较明确,比如替换苹果API中的类的某个方法或者第三方框架中的类的某个方法。
例子如下:

@interface LLPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LLPerson
- (void)personInstanceMethod{
    NSLog(@"person对象方法, 调用者:%@ 方法:%s", self,__FUNCTION__);
}
@end


@interface LLPerson (huan)
@end
@implementation LLPerson (huan)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"------// 替换开始 //------");
        NSLog(@"%@", self);
        Method oriMethod = class_getInstanceMethod(self, @selector(personInstanceMethod));
        Method swiMethod = class_getInstanceMethod(self, @selector(new_personInstanceMethod));
        method_exchangeImplementations ( oriMethod, swiMethod) ;
        NSLog(@"------// 替换完毕 //------");
    });
}

- (void)new_personInstanceMethod {
    NSLog(@"Person中的新方法, 调用者:%@, 方法:%s",self,__FUNCTION__);
    [self new_personInstanceMethod];
}
@end

调用代码如下:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"------------");
    LLStudent *stu = [LLStudent new];
    [stu personInstanceMethod];
}
@end

结果如下:

2020-01-07 22:19:27.096634+0800 RunTime1[1972:190371] ------// 替换开始 //------
2020-01-07 22:19:27.097367+0800 RunTime1[1972:190371] LLPerson
2020-01-07 22:19:27.097739+0800 RunTime1[1972:190371] ------// 替换完毕 //------
2020-01-07 22:19:27.203340+0800 RunTime1[1972:190371] ------------
2020-01-07 22:19:27.203516+0800 RunTime1[1972:190371] Person中的新方法, 调用者:<LLStudent: 0x600003520330>, 方法:-[LLPerson(huan) new_personInstanceMethod]
2020-01-07 22:19:27.203679+0800 RunTime1[1972:190371] person对象方法, 调用者:<LLStudent: 0x600003520330> 方法:-[LLPerson personInstanceMethod]

这种情况非常简单,我们明确的知道了LLPerson中的方法声明和方法实现,只需要在分类中直接交换就可以了。不需要其它的额外代码。 下边介绍一种特殊的情况,请往下看。

为什么会用到class_addMethod、class_replaceMethod

下边要讲的这种情况是在我开发过程中遇到的,如果只是用method_exchangeImplementations进行方法交换之后,运行会出现crash。
场景:Person声明了某个方法并且实现了方法,然后Student继承Person,没有重写父类的这个方法,依然不影响直接调用和使用Person中的这个方法。 如果此时我们在Student的分类中交换父类的这个方法,会发生了什么?
代码如下:

@interface LLPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LLPerson
- (void)personInstanceMethod{
    NSLog(@"person对象方法, 调用者:%@ 方法:%s", self,__FUNCTION__);
}
@end


@interface LLStudent : LLPerson
@end
@implementation LLStudent
@end

@interface LLStudent (huan)
@end
@implementation LLStudent (huan)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    NSLog(@"------// 替换开始 //------");
    NSLog(@"%@", self);
        Method oriMethod = class_getInstanceMethod(self, @selector(personInstanceMethod));
        Method swiMethod = class_getInstanceMethod(self, @selector(studentInstanceMethod));
        method_exchangeImplementations ( oriMethod, swiMethod) ;
        NSLog(@"------// 替换完毕 //------");
    });
}

- (void)studentInstanceMethod {
      NSLog(@"Student中的新方法, 调用者:%@, 方法:%s",self,__FUNCTION__);
      [self studentInstanceMethod];
}
@end

调用代码:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"------------");
    LLStudent *stu = [LLStudent new];
    [stu personInstanceMethod];
    
    NSLog(@"------------");
    LLPerson *person = [LLPerson new];
    [person personInstanceMethod]; 
}
@end

运行结果:

2020-01-07 22:41:10.744351+0800 RunTime1[2421:244815] ------// 替换开始 //------
2020-01-07 22:41:10.745134+0800 RunTime1[2421:244815] LLStudent
2020-01-07 22:41:10.745427+0800 RunTime1[2421:244815] ------// 替换完毕 //------
2020-01-07 22:41:10.852287+0800 RunTime1[2421:244815] ------------
2020-01-07 22:41:10.852488+0800 RunTime1[2421:244815] Student中的新方法, 调用者:<LLStudent: 0x600003964160>, 方法:-[LLStudent(huan) studentInstanceMethod]
2020-01-07 22:41:10.852652+0800 RunTime1[2421:244815] person对象方法, 调用者:<LLStudent: 0x600003964160> 方法:-[LLPerson personInstanceMethod]
2020-01-07 22:41:10.852797+0800 RunTime1[2421:244815] ------------
2020-01-07 22:41:10.852948+0800 RunTime1[2421:244815] Student中的新方法, 调用者:<LLPerson: 0x600003958050>, 方法:-[LLStudent(huan) studentInstanceMethod]
2020-01-07 22:41:10.853127+0800 RunTime1[2421:244815] -[LLPerson studentInstanceMethod]: unrecognized selector sent to instance 0x600003958050
// 崩溃

具体的崩溃位置是LLStudent+huan分类中的[self studentInstanceMethod];这一行。看到这里,可能大家会有点蒙,会有下边这几个问题:

  • 不是已经交换了父类的方法吗,为什么执行[person personInstanceMethod]会crash呢?
  • 为什么[stu personInstanceMethod]执行之后没有crash?
  • 为什么在父类的分类中交换方法就没有问题呢?
  • 怎么处理这个问题呢?

分析如下:

问题一:为什么执行[person personInstanceMethod]会crash呢?

注意,上边的代码是在子类Student的分类中把父类的方法和子类的方法进行了交换。交换之后如下:


图一

当执行[person personInstanceMethod]时,实际上是执行子类中的studentInstanceMethod方法,首先调用NSLog,输出结果。然后调用studentInstanceMethod方法中的 [self studentInstanceMethod]。这里要特别注意,NSLog打印出的当前self是<LLPerson: 0x600003958050>,所以 [self studentInstanceMethod]实际上就是[person studentInstanceMethod],底层实现代码为

objc_msgSend([LLPerson class], @Seletor(studentInstanceMethod))

用我们上边提到的消息机制来还原查找方法studentInstanceMethod的过程,类对象LLPerson中的方法列表methodLists中只有一个方法personInstanceMethod,且这个方法的IMP指向了studentInstanceMethod的实现地址。但是methodLists中根本没有studentInstanceMethod这个方法,所以经过消息机制的三个阶段也找不到该方法,最终报错-[LLPerson studentInstanceMethod]: unrecognized selector sent to instance 0x600003958050

问题二:为什么[stu personInstanceMethod]执行之后没有crash?

[stu personInstanceMethod]底层实现代码为
底层代码即

objc_msgSend([Student class], @Seletor(personInstanceMethod))

用消息机制来还原查找方法personInstanceMethod的过程,首先在

  • 首先,在实例对象stu对应的类对象Student的方法列表methodLists中查找personInstanceMethod方法,没有找到。
  • 然后通过类对象Student的superClass找到父类对象Person,在父类的的方法列表methodLists中查找personInstanceMethod方法,找到了,调用方法的IMP,此时是studentInstanceMethod。首先执行NSLog,打印结果。注意打印结果中的self是<LLStudent: 0x600003964160>,所以接下来调用[self studentInstanceMethod]实际上就是[stu studentInstanceMethod],所以底层实现代码是
objc_msgSend([Student class], @Seletor(studentInstanceMethod))

按照消息机制的查找过程,我们在类对象Student的方法列表methodLists中找到studentInstanceMethod,然后调用该方法的IMP,此时是personInstanceMethod。
所以我们可以看到,[stu personInstanceMethod]这行代码的运行结果是:

2020-01-07 22:41:10.852488+0800 RunTime1[2421:244815] Student中的新方法, 调用者:<LLStudent: 0x600003964160>, 方法:-[LLStudent(huan) studentInstanceMethod]
2020-01-07 22:41:10.852652+0800 RunTime1[2421:244815] person对象方法, 调用者:<LLStudent: 0x600003964160> 方法:-[LLPerson personInstanceMethod]
问题三:为什么在父类的分类中交换方法就没有问题呢?

简而言之,就是类对象Person在进行方法交换之前,它的方法列表methodLists中已经包含了交换前的方法和交换后的方法,不会存在交换之后,方法找不到的问题。

问题四:怎么处理这个问题呢?

首先再次明确我们的目的是为了在Student中将使用的父类方法进行方法交换。成功的标志和直接在父类中的分类中进行方法交换的结果一样,如果stu执行studentInstanceMethod和personInstanceMethod能够调用到对方的实现,就达到了目的。 一定要明确这一点。

通过上边的分析,我们知道直接使用method_exchangeImplementations的方法实现不了我们想要的目的。解决的方式,如问题三中提到的那样,先让stu拥有交换前和交换后的方法,然后再进行交换。

好了,先看下代码和运行结果,我们再做具体的分析。

@implementation LLStudent (huan)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"------// 替换开始 //------");
        NSLog(@"%@", self);
        Method oriMethod = class_getInstanceMethod(self, @selector(personInstanceMethod));
        Method swiMethod = class_getInstanceMethod(self, @selector(studentInstanceMethod));
        BOOL didAddMethod = class_addMethod(self,
                                            @selector(personInstanceMethod),
                                            method_getImplementation(swiMethod),
                                            method_getTypeEncoding(swiMethod)
                                            );
        if (didAddMethod) {
            class_replaceMethod(self,
                                @selector(studentInstanceMethod),
                                method_getImplementation(oriMethod),
                                method_getTypeEncoding(oriMethod));
        }else{
            method_exchangeImplementations (oriMethod, swiMethod);
        }
        NSLog(@"------// 替换完毕 //------");
    });
}

- (void)studentInstanceMethod {
      NSLog(@"Student中的新方法, 调用者:%@, 方法:%s",self,__FUNCTION__);
      [self studentInstanceMethod];
}
@end
2020-01-08 00:14:35.333082+0800 RunTime1[3674:509474] ------// 替换开始 //------
2020-01-08 00:14:35.333878+0800 RunTime1[3674:509474] LLStudent
2020-01-08 00:14:35.334058+0800 RunTime1[3674:509474] ------// 替换完毕 //------
2020-01-08 00:14:35.441442+0800 RunTime1[3674:509474] ------------
2020-01-08 00:14:35.441627+0800 RunTime1[3674:509474] Student中的新方法, 调用者:<LLStudent: 0x6000036185e0>, 方法:-[LLStudent(huan) studentInstanceMethod]
2020-01-08 00:14:35.441779+0800 RunTime1[3674:509474] person对象方法, 调用者:<LLStudent: 0x6000036185e0> 方法:-[LLPerson personInstanceMethod]
2020-01-08 00:14:35.441893+0800 RunTime1[3674:509474] ------------
2020-01-08 00:14:35.442032+0800 RunTime1[3674:509474] person对象方法, 调用者:<LLPerson: 0x600003614160> 方法:-[LLPerson personInstanceMethod]

通过打印结果,可以看到已经实现了我们的目的,并且父类依然可以调用到,没有崩溃。我们具体分析下:
首先,执行class_addMethod方法

    BOOL didAddMethod = class_addMethod(self,
                                            @selector(personInstanceMethod),
                                            method_getImplementation(swiMethod),
                                            method_getTypeEncoding(swiMethod)
                                            );

结果didAddMethod = YES, 我们动态给类对象Student新添加了personInstanceMethod方法,并且这个方法的IMP是studentInstanceMethod。 此时类对象Student方法列表中就包含了交换前和交换后的方法,而类对象Person的方法列表我们并没有进行操作,所以不变,看图二。


图二

然后进入if判断中,执行下边代码:

class_replaceMethod(self,
                                @selector(studentInstanceMethod),
                                method_getImplementation(oriMethod),
                                method_getTypeEncoding(oriMethod));

类对象Student中的方法studentInstanceMethod的imp和types替换为oriMethod(即personInstanceMethod)的。此时,类对象Student中的两个方法及它们的imp实际如下:

图三

是不是很熟悉?没错,和图一中交换后的类对象Person的方法列表中一样。这个时候执行[stu personInstanceMethod]就不会crash且实现方法交换的效果了。
小结一下:如果想要实现方法交换,那么交换前后的方法必须都在当前类对象中有实现才可以。 所以,AFNetworking和其它一些第三方框架要用到class_addMethod、class_replaceMethod两个方法,是为了兼顾上边这种特殊的情况,造成crash。

结尾

终于把之前的坑补上了。其实在Runtime交换方法的使用过程中还有其它的情况存在,比如说组内多个人都对同一个方法进行了交换操作等等,所以我们最好是把这种操作交给一个人或者一个组来统一维护,避免这种情况。

交流

希望能和大家交流技术

我的博客地址: http://www.lilongcnc.cc/


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

推荐阅读更多精彩内容