美文网首页
详讲Runtime方法交换(class_addMethod ,c

详讲Runtime方法交换(class_addMethod ,c

作者: 春田花花幼儿园 | 来源:发表于2020-06-20 21:27 被阅读0次

    前言

    最近在整理博客,发现自己之前写的关于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/


    相关文章

      网友评论

          本文标题:详讲Runtime方法交换(class_addMethod ,c

          本文链接:https://www.haomeiwen.com/subject/gnddactx.html