美文网首页
iOS底层系列19 -- Method-Swizzling

iOS底层系列19 -- Method-Swizzling

作者: YanZi_33 | 来源:发表于2021-03-12 16:02 被阅读0次
    Method-Swizzling
    • Method-Swizzling即方法的交换,其实现方式是在运行时将一个方法的实现替换成另一个方法的实现,这就是我们常说的iOS黑魔法。
    • 每个类都维护着一个方法类表即rwe中的method_array_t,在method_array_t中存储中许多不同的方法即Method,在iOS底层系列16 -- 类的加载机制已经探索了在运行时,这些主类的方法集合和分类的方法集合是如何加载进入主类class rwe结构中的method_array_t当中的。
    • 每个Method都包含sel和IMP两个部分,分别表示method的名称和method的实现;方法交换就是将sel和imp原本的对应断开,并将sel和新的IMP生成对应关系。
    Snip20210312_114.png
    Method-Swizzling常见函数
    • class_getInstanceMethod:获取实例方法
    • class_getClassMethod:获取类方法
    • method_getImplementation:获取方法的实现
    • method_setImplementation:设置方法的实现
    • method_getTypeEncoding:获取方法实现的编码类型
    • class_addMethod:给方法添加实现
    • class_replaceMethod:用一个方法的实现,替换另一个方法的实现,即IMP1指向 IMP2,但是IMP2不一定指向IMP1
    • method_exchangeImplementations:交换两个方法的实现,即IMP1指向IMP2,IMP2指向IMP1

    下面通过几个案例来阐述在使用Method-Swizzling过程中需要注意的事项
    准备工作:新建两个类父类YYPerson子类YYStudent,Method-Swizzling帮助类YYRuntimeHelperYYStudent分类(Add)

    #import <Foundation/Foundation.h>
    
    @interface YYPerson : NSObject
    
    - (void)walk;
    
    @end
    
    #import "YYPerson.h"
    
    @implementation YYPerson
    
    - (void)walk{
        NSLog(@"%s",__func__);
    }
    
    @end
    
    #import <Foundation/Foundation.h>
    
    @interface YYStudent : NSObject
    
    - (void)read;
    
    - (void)write;
    
    @end
    
    #import "YYStudent.h"
    
    @implementation YYStudent
    
    - (void)read{
        NSLog(@"%s",__func__);
    }
    
    - (void)write{
        NSLog(@"%s",__func__);
    }
    
    @end
    
    #import "YYStudent.h"
    
    @interface YYStudent (Add)
    
    @end
    
    #import "YYStudent+Add.h"
    #import "YYRuntimeHelper.h"
    
    @implementation YYStudent (Add)
    
    //在运行时,进行方法的交换
    + (void)load{
        [YYRuntimeHelper yy_instanceMethodSwizzlingithClass:self originalSEL:@selector(read) swizzledSEL:@selector(yy_read)];
    }
    
    - (void)yy_read{
        NSLog(@" 使用yy_read的方法去实现read");
    }
    
    @end
    
    #import <Foundation/Foundation.h>
    
    @interface YYRuntimeHelper : NSObject
    
    + (void)yy_instanceMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL;
    
    @end
    
    #import "YYRuntimeHelper.h"
    #import <objc/runtime.h>
    
    @implementation YYRuntimeHelper
    
    + (void)yy_instanceMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL{
        Method originalMethod = class_getInstanceMethod(cls, originalSEL);
        Method swizzledMethod = class_getInstanceMethod(cls, swizzledSEL);
        //方法交换
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    
    @end
    
    案例一:外界主动调用目标类的load方法,导致Method-Swizzling失效
    Snip20210312_116.png

    上面的截图是正常情况;但如果在AppDelegate.m文件中,主动调用[YYStudent load],那么当代码执行到控制器时,Method-Swizzling就失效了,因为前后交换了两次还原了。

    Snip20210312_118.png

    解决方案:保证YYStudent(Add)中load方法中的Method-Swizzling只执行一次,加入dispatch_once
    YYStudent(Add)修改后的代码如下:

    #import "YYStudent+Add.h"
    #import "YYRuntimeHelper.h"
    
    @implementation YYStudent (Add)
    
    //在运行时,进行方法的交换
    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [YYRuntimeHelper yy_instanceMethodSwizzlingithClass:self originalSEL:@selector(read) swizzledSEL:@selector(yy_read)];
        });
    }
    
    - (void)yy_read{
        NSLog(@" 使用yy_read的方法去实现read");
    }
    
    @end
    
    案例二:子类没有实现目标方法,但是父类实现了目标方法,在子类的分类中交换目标方法的实现时,最终交换的是父类的实现。

    例如上面的YYPerson类有walk的实现,但在子类YYStudent中没有walk的实现;YYStudent分类中的交换实现如下:

    #import "YYStudent+Add.h"
    #import "YYRuntimeHelper.h"
    
    @implementation YYStudent (Add)
    
    //在运行时,进行方法的交换
    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [YYRuntimeHelper yy_instanceMethodSwizzlingithClass:self originalSEL:@selector(walk) swizzledSEL:@selector(yy_walk)];
        });
    }
    
    - (void)yy_walk{
       //这里不会出现循环调用,因为已经实现了方法的交换
       [self yy_walk];
        NSLog(@" 使用yy_walk的方法去实现walk");
    }
    
    @end
    

    外界调用及LLDB调试如下:

    Snip20210315_122.png Snip20210315_121.png

    出现了崩溃,首先因为子类YYStudent没有walk方法实现,所以Method-Swizzling交换的是父类的walk,见下图所示:

    Snip20210315_123.png
    • [student walk]由于student没有walk方法实现,会调用父类YYPerson的walk方法,而父类的walk在运行时进行了Method-Swizzling,所以会调用YYStudent(Add)中的yy_walk方法实现,其内部首先调用[self yy_walk],此时的self是student,调用的则是父类YYPerson的walk方法实现;
    • [person walk]则调用了YYStudent(Add)中的yy_walk方法实现,其内部首先调用[self yy_walk]此时的self是person,而YYPerson类中并没有yy_walk方法,所以会出现崩溃。
    • 本意是交换子类的walk实现,不想将父类的walk实现也交换了,且会发生崩溃;为了Method-Swizzling不影响父类该怎么做?

    解决方案:在进行方法交换时,首先判断当前类是否有目标方法,如果有直接进行交换;如果没有调用class_addMethod函数,给当前类动态添加一个方法(目标方法),但其实现是交换方法的实现;添加成功之后再调用class_replaceMethod将交换方法的实现替换成目标方法的实现;这样才完整的实现了Method-Swizzling

    YYRuntimeHelper修改后的代码如下:

    #import "YYRuntimeHelper.h"
    #import <objc/runtime.h>
    
    @implementation YYRuntimeHelper
    
    + (void)yy_instanceMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL{
        
        if (!cls) {
            return;
        }
        
        Method original_method = class_getInstanceMethod(cls, originalSEL);
        Method swizzled_method = class_getInstanceMethod(cls, swizzledSEL);
        
        IMP original_imp = method_getImplementation(original_method);
        IMP swizzled_imp = method_getImplementation(swizzled_method);
        
        //主要用来检测当前类是否有original_method,避免替换了父类的实现
        //originalSEL --> swizzled_imp
        BOOL success = class_addMethod(cls, originalSEL, swizzled_imp, method_getTypeEncoding(original_method));
        //添加成功,表示当前类没有original_method
        if (success) {
            //swizzledSEL --> original_imp
            //因为当前类没有original_method 这里的original_imp是当前类父类的实现
            class_replaceMethod(cls, swizzledSEL, original_imp, method_getTypeEncoding(original_method));
        }else{//添加失败,表示当前类有original_method
            //方法交换
            method_exchangeImplementations(original_method,swizzled_method);
        }
    }
    
    @end
    
    • 注意这里的original_imp是父类的实现,因为当前类没有original_method

    LLDB调试结果如下:

    Snip20210312_120.png
    案例三:子类没有实现目标方法,且父类也没有实现目标方法。

    外界调用及LLDB调试结果如下:

    Snip20210315_124.png Snip20210315_125.png

    Method-Swizzling的代码实现:

    Snip20210315_126.png
    • 首先运行时进行Method-Swizzling,由于当前类YYStudent没有walk方法,所以会调用class_addMethod函数,给YYStudent添加walk方法其实现为yy_walk;添加成功后会执行class_replaceMethod函数,但由于original_imp不存在,所以会交换失败;那么Method-Swizzling只实现了单方的交换,如下图所示:
    Snip20210315_128.png
    • [student walk]时,会来到yy_walk方法,当执行[self yy_walk]时,由于yy_walk并没有交换成功,所以调用的实现还是自己本身,则出现循环调用,最后导致内存溢出应用崩溃。

    解决方案:在进行Method-Swizzling之前,对原始方法进行检测,若为空调用class_addMethod函数,添加原始方法walk,方法实现为交换方法实现yy_walk;然后将交换方法yy_walk的实现置为{},即为空方法什么也不做,接着再调用class_addMethod函数,此时上面已经添加过了,所以再次添加会不成功,最后走method_exchangeImplementations函数,但由于original_method是空的,所以不会发生交换,最终的效果如下所示:

    Snip20210315_130.png

    YYRuntimeHelper修过之后的代码如下:

    + (void)yy_instanceMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL{
        
        if (!cls) {
            return;
        }
        
        Method original_method = class_getInstanceMethod(cls,originalSEL);
        Method swizzled_method = class_getInstanceMethod(cls,swizzledSEL);
        
        IMP original_imp = method_getImplementation(original_method);
        IMP swizzled_imp = method_getImplementation(swizzled_method);
        
        if (!original_method) {
            //在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现
            class_addMethod(cls,originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
            
            method_setImplementation(swizzled_method, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
        }
    
        BOOL didAddMethod = class_addMethod(cls,originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
        if (didAddMethod) {
            class_replaceMethod(cls,swizzledSEL,original_imp, method_getTypeEncoding(original_method));
        }else{
            method_exchangeImplementations(original_method,swizzled_method);
        }
    }
    

    method_exchangeImplementations源码实现如下:

    Snip20210315_131.png
    Method-Swizzling之类方法

    上面介绍的是实例方法的交换,类方法的交换原理是类似的,只不过类方法存储在元类,具体实现代码如下:

    + (void)yy_classMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL{
        if (!cls) {
            return;
        }
        
        class_getClassMethod(cls,originalSEL);
        Method original_method = class_getClassMethod([cls class],originalSEL);
        Method swizzled_method = class_getClassMethod([cls class],swizzledSEL);
        
        IMP original_imp = method_getImplementation(original_method);
        IMP swizzled_imp = method_getImplementation(swizzled_method);
        
        if (!original_method) {
            //在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现
            //object_getClass(cls) 获取元类
            class_addMethod(object_getClass(cls),originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
            method_setImplementation(swizzled_method,
                                     imp_implementationWithBlock(^(id self,SEL _cmd){
                NSLog(@"来了一个空的 imp");
            }));
        }
    
        BOOL didAddMethod = class_addMethod(object_getClass(cls),originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
        if (didAddMethod) {
            class_replaceMethod(object_getClass(cls),swizzledSEL,original_imp, method_getTypeEncoding(original_method));
        }else{
            method_exchangeImplementations(original_method,swizzled_method);
        }
    }
    

    相关文章

      网友评论

          本文标题:iOS底层系列19 -- Method-Swizzling

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