美文网首页
第十九节—Method_Swizzling

第十九节—Method_Swizzling

作者: L_Ares | 来源:发表于2020-11-07 18:49 被阅读0次

    本文为L_Ares个人写作,以任何形式转载请表明原文出处。

    Method_SwizzlingiOS开发者常见的一种方法,那么关于Method_Swizzling到底是什么,有一些什么坑在里面,本节将会通过自己的视角来阐述。

    首先,虽然很熟悉Method_Swizzling了,但是还是要系统的介绍一下。

    一、Method_Swizzling是什么

    Method_Swizzling是一种在运行时,将方法编号(sel)对应的方法实现(imp)进行交换的手段。

    通俗的说,方法在类中是以method_list_t的形式存储着的,也就是之前在说类的结构中的class_ro_t* ro中的baseMethodList对象中存储。而method_list_t中存储的方法是以method_t结构体结构存储的方法,而method_t拥有着方法编号(sel) --- 方法实现(imp)属性,Method_Swizzling就是要将方法编号(sel)对应的方法实现(imp)进行交换。

    如下图 :

    method_swizzling.png

    二、Method_Swizzling要用到的API

    即然是方法交换,那么必然就需要一些和类、方法、实现相关的API

    1. 通过sel获取Method

    • 获取实例方法 : class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)

      • cls : 要获取哪个类的实例方法
      • name : 方法编号的名称
    • 获取类方法 : class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)

      • cls : 要获取哪个类的类方法
      • name : 方法编号的名称

    2. 方法的实现Imp

    • 获取一个方法的实现 : method_getImplementation(Method _Nonnull m)
      • m : 哪个方法的IMP实现
    • 设置一个方法的实现 : method_setImplementation(Method _Nonnull m, IMP _Nonnull imp)
      • m : 要给哪个方法设置实现。
      • imp : 实现IMP

    3. 编码类型获取

    • 获取一个方法的编码类型 : method_getTypeEncoding(Method _Nonnull m)
      • m : 要获取编码类型的方法。

    4. 方法相关

    • 添加一个方法 : class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)

      • cls : 给哪个类添加方法
      • name : 指定要添加的方法名称的选择器
      • imp : 一个新方法的实现函数。该函数必须使用至少两个参数—self和_cmd。
      • types : 描述方法参数类型的字符数组。
    • 替换给定类的方法实现 : class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)

      • cls : 你想要修改的类。
      • name : 你想要替换方法实现的方法的方法编号。
      • imp : 你想要给上面的name修改成为的方法实现。
      • types : 描述方法参数类型的字符数组。
    • 交换两个方法的实现 : method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)

      • m1m2 : 你想要交换哪两个方法的方法实现。

    三、Method_Swizzling中可能存在的一些问题

    准备 : 随意创建一个iOSProject--->App,然后定义两个类。继承于NSObjectJDPerson类,和继承与JDPersonJDStudent类。类名自拟。创建NSArray的分类NSArray+JD

    1. 数组越界和类蔟

    什么意思呢,就是说在进行Method_Swizzling的时候,我们是可以进行数组越界的一个处理的,防止进入越界的crash中。但是数组是类蔟,所以有一些特殊。

    举个例子 :

    我们会利用NSArray- (ObjectType)objectAtIndex:(NSUInteger)index;方法获取数组中index对应的元素。但是一旦index的数字大于array.count - 1,就会造成越界,程序就会进入crash流程。所以我们可以对它进行相应的处理,让越界不进入crash流程。

    先在NSArray的分类NSArray+JD.m中实现如下交换代码(错误示范) :

    #import "NSArray+JD.h"
    #import <objc/runtime.h>
    
    @implementation NSArray (JD)
    
    + (void)load
    {
        
        //获取NSArray的 objectAtIndex: 的Method
        Method originalMethod = class_getInstanceMethod([self class], @selector(objectAtIndex:));
        //获取下面自定义的,用来替换objectAtIndex实现的Method
        Method swizzlingMethod = class_getInstanceMethod([self class], @selector(jd_objectAtIndex:));
        //交换两个方法的实现
        method_exchangeImplementations(originalMethod, swizzlingMethod);
        
    }
    
    - (id)jd_objectAtIndex:(NSUInteger)index
    {
        //判断如果index比数组拥有的元素数量还多
        if (self.count - 1 < index) {
            //你可以进行其他的处理,这里作为举例,只返回nil,只进行NSLog,方便查看控制台
            NSLog(@"数组越界了");
            return nil;
        }
        
        //这里再调用jd_objectAtIndex就已经在+(void)load中就被换成了objectAtIndex的实现
        return [self jd_objectAtIndex:index];
    }
    
    @end
    
    

    然后,在ViewController里面随意定义一个数组属性,进行数组初始化,然后打印超过数组元素数量的index的值。代码如下 :

    #import "ViewController.h"
    
    @interface ViewController ()
    
    @property (nonatomic, strong) NSArray *tempArray;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        
        [super viewDidLoad];
        
        
        self.tempArray = @[@"name",@"sex",@"age",@"work"];
        
        NSLog(@"%@",[self.tempArray objectAtIndex:4]);
        
        // Do any additional setup after loading the view.
    }
    
    
    @end
    

    但是这么运行起来,依然是会报错数组越界的。如下图 :

    图3.1.0.png

    原因 : 数组是一个类蔟,获取NSArrayobjectAtIndex方法的类应该是__NSArrayI

    类蔟 :

    类蔟是一种设计模式,类蔟中的类利用相同的接口却可以有不同的实现。

    具体的介绍大家可以直接进入这里。直接把一些常见的类蔟写一下。

    类名 实际名称
    NSArray __NSArrayI
    NSMutableArray __NSArrayM
    NSDictionary __NSDictionaryI
    NSMutableDictionary __NSDictionaryM

    正确的Method_Swizzling :

    #import "NSArray+JD.h"
    #import <objc/runtime.h>
    
    @implementation NSArray (JD)
    
    + (void)load
    {
        
        //获取NSArray的 objectAtIndex: 的Method
    //    Method originalMethod = class_getInstanceMethod([self class], @selector(objectAtIndex:));
        Method originalMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
        //获取下面自定义的,用来替换objectAtIndex实现的Method
    //    Method swizzlingMethod = class_getInstanceMethod([self class], @selector(jd_objectAtIndex:));
        Method swizzlingMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(jd_objectAtIndex:));
        //交换两个方法的实现
        method_exchangeImplementations(originalMethod, swizzlingMethod);
        
    }
    
    - (id)jd_objectAtIndex:(NSUInteger)index
    {
        //判断如果index比数组拥有的元素数量还多
        if (index > (self.count - 1)) {
            //你可以进行其他的处理,这里作为举例,只返回nil,只进行NSLog,方便查看控制台
            NSLog(@"数组越界了");
            return nil;
        }
        
        //这里再调用jd_objectAtIndex就已经在+(void)load中就被换成了objectAtIndex的实现
        return [self jd_objectAtIndex:index];
    }
    
    @end
    
    

    执行结果 :

    图3.1.2.png

    当然,最常用的数组取值还是以self.tempArray[4]这种居多,所以可以再在load中添加objectAtIndexedSubscriptMethod_Swizzling

    + (void)load
    {
        
        //获取NSArray的 objectAtIndex: 的Method
    //    Method originalMethod = class_getInstanceMethod([self class], @selector(objectAtIndex:));
        Method originalMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
        //获取下面自定义的,用来替换objectAtIndex实现的Method
    //    Method swizzlingMethod = class_getInstanceMethod([self class], @selector(jd_objectAtIndex:));
        Method swizzlingMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(jd_objectAtIndex:));
        //交换两个方法的实现
        method_exchangeImplementations(originalMethod, swizzlingMethod);
        
        //针对self.tempArray[4]取值进行防止越界crash
        Method oriM = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
        Method swiM = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(jd_objectAtIndexedSubscript:));
        method_exchangeImplementations(oriM, swiM);
        
    }
    
    - (id)jd_objectAtIndex:(NSUInteger)index
    {
        //判断如果index比数组拥有的元素数量还多
        if (index > (self.count - 1)) {
            //你可以进行其他的处理,这里作为举例,只返回nil,只进行NSLog,方便查看控制台
            NSLog(@"数组越界了");
            return nil;
        }
        
        //这里再调用jd_objectAtIndex就已经在+(void)load中就被换成了objectAtIndex的实现
        return [self jd_objectAtIndex:index];
    }
    
    - (id)jd_objectAtIndexedSubscript:(NSUInteger)idx
    {
    
        if ((unsigned long)index > (self.count - 1)) {
            NSLog(@"数组越界了");
            return nil;
        }
        
        return [self jd_objectAtIndexedSubscript:(unsigned long)index];
    }
    
    

    2. 多次执行交换问题

    上面的代码的确解决了数组越界造成的crash,但是如果在有人不知情的情况下,在代码中又调用了[NSArray load],就会出现又一个问题,sel对应的imp被多次交换,可能继续造成数组越界的crash。所以还可以优化一下。
    利用单例的设计,只让Method_Swizzling出现一次。

    + (void)load
    {
        
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Method originalMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
            Method swizzlingMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(jd_objectAtIndex:));
            method_exchangeImplementations(originalMethod, swizzlingMethod);
            
            Method oriM = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
            Method swiM = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(jd_objectAtIndexedSubscript:));
            method_exchangeImplementations(oriM, swiM);
        });
    
    }
    

    3. 子类交换父类的实现

    • 再创建一个JDStudent(子类)的分类JDStudent+JD,进行Method_Swizzling

    • 准备中的JDPerson类中创建一个实例方法- (void)personInstanceMethod;,并且实现。子类则没有方法。

    • Method_Swizzling封装成一个工具类RuntimeTools,方便调用,也方便修改。

    /**
      RuntimeTools.h
     */
    #import <Foundation/Foundation.h>
    
    @interface RuntimeTools : NSObject
    
    /**
     交换方法
     @param cls 交换对象
     @param oriSEL 原始方法编号
     @param swizzledSEL 交换的方法编号
     */
    + (void)jd_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swiSEL;
    
    
    @end
    
    /**
      RuntimeTools.m
     */
    #import "RuntimeTools.h"
    #import <objc/runtime.h>
    
    @implementation RuntimeTools
    
    + (void)jd_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swiSEL
    {
        if (!cls) NSLog(@"传入的交换类不能为空");
    
        Method oriMethod = class_getInstanceMethod(cls, oriSEL);
        Method swiMethod = class_getInstanceMethod(cls, swiSEL);
        method_exchangeImplementations(oriMethod, swiMethod);
    }
    
    @end
    

    因为子类会继承父类的实例方法,有的时候直接会用子类调用父类的实例方法,然后进行了imp的交换,比如JDStudent+JD.m中会有 :

    #import "JDStudent+JD.h"
    #import "RuntimeTools.h"
    #import <objc/runtime.h>
    
    @implementation JDStudent (JD)
    
    + (void)load
    {
        static dispatch_once_t jdOnceToken;
        dispatch_once(&jdOnceToken, ^{
            [RuntimeTools jd_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(jd_studentInstanceMethod)];
        });
    }
    
    - (void)jd_studentInstanceMethod
    {
        NSLog(@"JDStudent(子类)的方法 : %s",__func__);
        [self jd_studentInstanceMethod];
    }
    
    @end
    

    然后在ViewController- (void)viewDidLoad :

    #import "ViewController.h"
    #import "JDPerson.h"
    #import "JDStudent.h"
    #import "JDStudent+JD.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        
        [super viewDidLoad];
        
        JDStudent *student = [[JDStudent alloc] init];
        [student personInstanceMethod];
    }
    @end
    

    执行结果是没有问题的,因为子类把父类本身的实现交换成了自己分类JDStudent+JD.m中的jd_studentInstanceMethod。如图 :

    图3.3.0.png

    但是,如果这个时候,父类JDPerson再调用自己的personInstanceMethod就会出现问题。在ViewController- (void)viewDidLoad中添加代码 :

        JDPerson *person = [[JDPerson alloc] init];
        [person personInstanceMethod];
    

    再次执行,报错。因为它的impJDStudent交换了,可是它是父类,是找不到子类的实现的。就会出现如下图错误 :

    图3.3.1.png

    这也会引发错误,所以还可以对RunTimeTools中的Method_Swizzling方法进行改进。

    改进的思路是给子类进行方法的添加,然后让子类交换添加后的,自己的personInstanceMethod,这就不会影响父类自己的实现。

    改进后RunTimeTools代码 :

    #import "RuntimeTools.h"
    #import <objc/runtime.h>
    
    @implementation RuntimeTools
    
    + (void)jd_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swiSEL
    {
        if (!cls) NSLog(@"传入的交换类不能为空");
    
        //还是先拿好这两个方法
        //因为如果子类本身就有`personInstanceMethod`方法,就不需要再进行添加
        //直接交换也无所谓,因为交换的是子类自己的`personInstanceMethod`方法
        Method oriMethod = class_getInstanceMethod(cls, oriSEL);
        Method swiMethod = class_getInstanceMethod(cls, swiSEL);
        
        //用来判断是否可以给子类添加`personInstanceMethod`方法
        //可以添加则证明子类没有这个方法
        //不能添加则证明子类有这个方法,不需要再添加,直接交换即可,不会影响父类
        BOOL canAdd = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        
        if (canAdd)
        {
            //子类没有这个方法的实现,现在刚刚加进去
            //但是子类的personInstanceMethod--->jd_studentInstanceMethod的实现
            //而jd_studentInstanceMethod又调用了自己,如果不把jd_studentInstanceMethod修改
            //成父类的实现,那么就会一直递归jd_studentInstanceMethod
            //所以把子类的jd_studentInstanceMethod的实现,替换成父类的实现
            class_replaceMethod(cls, swiSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }
        else
        {
            //子类本身就有这个方法的实现,那么直接交换
            method_exchangeImplementations(oriMethod, swiMethod);
        }
        
    }
    
    @end
    
    

    再执行,就不会发生上面的错误了。执行结果 :

    图3.3.2.png

    4. 父类也没有实现,子类交换父类的方法

    就是说如果父类的方法也没实现,子类也没有一个方法的实现,但是子类还是交换了父类的方法,就会出现一个不停递归的问题。

    就上面的代码,把personInstanceMethod的实现从JDPerson.m里面去掉。再执行。就会出现如下图的问题 :

    图3.4.0.png

    原因 :

    图3.4.1.png

    没有实现就没办法替换imp,所以jd_studentInstanceMethod还是自己的实现,就造成了死循环。

    解决 :

    • 先给子类添加上它调用的方法,也就是和上面一样,利用class_addMethod

    • 子类有了方法后,只要给sel : jd_studentInstanceMethod替换一个存在的IMP就行。

    • 所以要给swiMethod也添加实现。

    #import "RuntimeTools.h"
    #import <objc/runtime.h>
    
    @implementation RuntimeTools
    
    + (void)jd_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swiSEL
    {
        if (!cls) NSLog(@"传入的交换类不能为空");
    
        //还是先拿好这两个方法
        //因为如果子类本身就有`personInstanceMethod`方法,就不需要再进行添加
        //直接交换也无所谓,因为交换的是子类自己的`personInstanceMethod`方法
        Method oriMethod = class_getInstanceMethod(cls, oriSEL);
        Method swiMethod = class_getInstanceMethod(cls, swiSEL);
        
        
        //如果父类也没有方法实现
        if (!oriMethod) {
            class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
            method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
        }
        
        //用来判断是否可以给子类添加`personInstanceMethod`方法
        //可以添加则证明子类没有这个方法
        //不能添加则证明子类有这个方法,不需要再添加,直接交换即可,不会影响父类
        BOOL canAdd = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        
        if (canAdd)
        {
            //子类没有这个方法的实现,现在刚刚加进去
            //但是子类的personInstanceMethod--->jd_studentInstanceMethod的实现
            //而jd_studentInstanceMethod又调用了自己,如果不把jd_studentInstanceMethod修改
            //成父类的实现,那么就会一直递归jd_studentInstanceMethod
            //所以把子类的jd_studentInstanceMethod的实现,替换成父类的实现
            class_replaceMethod(cls, swiSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }
        else
        {
            //子类本身就有这个方法的实现,那么直接交换
            method_exchangeImplementations(oriMethod, swiMethod);
        }
        
    }
    
    @end
    
    

    ViewController.m- (void)viewDidLoad中让子类JDStudent调用方法personInstanceMethod就不会出现死循环。

    效果图 :

    图3.4.2.png

    相关文章

      网友评论

          本文标题:第十九节—Method_Swizzling

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