美文网首页iOS开发技巧
OC底层原理15-Method Swizzling

OC底层原理15-Method Swizzling

作者: 夏天的枫_ | 来源:发表于2020-10-29 19:36 被阅读0次

    iOS--OC底层原理文章汇总

    Method Swizzling

    方法交换,这是发生在Runtime中的一种处理两个方法交换的手段,它处理的是什么?怎么处理的?今天就来探究一下。
    method swizzling在runtime将一个方法的实现替换为另一个方法的实现。在前面的篇章中,我们知道,一个方法的实现是需要sel、imp对应的,通过sel就能找到impmethod swizzling正是通过改变sel、imp指向来实现方法的交换。这也是Runtime的一种应用。
    下面是sel、imp交换前后示意图

    交换前

    经过method swizzling,也可以理解为imp swizzling,将二者的sel、imp,通过更改实现

    交换后

    我们是在什么时间去处理这个调用转换呢?我们可以选择在+load调用时(也有用在initialize)。由前面的篇章我们了解到,如果类实现+load,使得该类成为非懒加载类,这个方法系统调用的时间会很早,所以在runtime过程中,放在此处是很合适的。
    先来看看处理方法交换的相关API,苹果文档显示右如下的方法是处理方法可能用到的:

    method_invoke 调用指定方法的实现。
    method_invoke_stret 调用返回数据结构的指定方法的实现。
    method_getName 返回方法的名称。
    method_getTypeEncoding 返回描述方法参数和返回类型的字符串。
    method_copyReturnType 返回描述方法返回类型的字符串。
    method_copyArgumentType 返回描述方法的单个参数类型的字符串。
    method_getReturnType 通过引用返回描述方法返回类型的字符串。
    method_getNumberOfArguments 返回方法接受的参数数量。
    method_getArgumentType 通过引用返回描述方法的单个参数类型的字符串。
    method_getDescription 返回指定方法的方法描述结构。
    method_setImplementation 设置方法的实现。
    method_exchangeImplementations 交换两种方法的实现。
    method_getImplementation 返回方法的实现。

    实操

    由于选择在+load中处理方法交换,+load可能被多次调用,那就得保证方法交换仅仅执行一次,所以采用单例模式处理:

    + (void)load
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{  
            /** 保证执行一次交换
             *  定义一个Runtime工具类,在其中实现交换
             * animalInstanceMethod:原始待替换方法
             * smallCatInstanceMethod:替换之后的方法
             */
            [TLRuntimeTool runtimeMethodSwizzlingWithClass:self originSEL:@selector(animalInstanceMethod) swizzlingSEL:@selector(smallCatInstanceMethod)];
        });
    }
    

    以下是TLRuntimeTool的实现

    //--------TLRuntimeTool.h----------
    @interface TLRuntimeTool : NSObject
    
    +(void)runtimeMethodSwizzlingWithClass:(Class)targetCls originSEL:(SEL)oriSel swizzlingSEL:(SEL)swizzlingSel;
    
    @end
    //--------TLRuntimeTool.m ----------
    #import "TLRuntimeTool.h"
    #import <objc/runtime.h>
    
    @implementation TLRuntimeTool
    
    +(void)runtimeMethodSwizzlingWithClass:(Class)targetCls originSEL:(SEL)oriSel swizzlingSEL:(SEL)swizzlingSel
    {
        if (!targetCls) {
            NSLog(@"传入的类不能为空");
            return;
        }
        Method originMethod = class_getInstanceMethod(targetCls, oriSel);
        Method swizzlingMethod = class_getInstanceMethod(targetCls, swizzlingSel);
        method_exchangeImplementations(originMethod, swizzlingMethod);   
    }
    

    现在对方法替换做一个测试,定义一个Animal类、Cat类、Cat+small分类,并在VC页面对其调用

    测试工程结构
    以下是各类实现
    • Animal
    #import <Foundation/Foundation.h>
    NS_ASSUME_NONNULL_BEGIN
    @interface Animal : NSObject
    -(void)animalInstanceMethod;
    
    +(void)animalClassMethod;
    
    @end
    
    NS_ASSUME_NONNULL_END
    // -----------------------
    #import "Animal.h"
    @implementation Animal
    - (void)animalInstanceMethod{
        NSLog(@"animal instance method:%s",__func__);
    }
    + (void)animalClassMethod{
        NSLog(@"animal class metohd: %s",__func__);
    }
    @end
    
    • Cat
    #import "Animal.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Cat : Animal
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    #import "Cat.h"
    
    @implementation Cat
    - (void)animalInstanceMethod{
        NSLog(@"cat instance method:%s",__func__);
    }
    @end
    
    • Cat+small
    #import "Cat.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Cat (small)
    
    @end
    
    NS_ASSUME_NONNULL_END
    //----------------
    #import "Cat+small.h"
    #import "TLRuntimeTool.h"
    @implementation Cat (small)
    
    + (void)load
    {
            /** 保证执行一次交换
             *  定义一个Runtime工具类,在其中实现交换
             * animalInstanceMethod:原始待替换方法
             * smallCatInstanceMethod:替换之后的方法
             */
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [TLRuntimeTool runtimeMethodSwizzlingWithClass:self originSEL:@selector(animalInstanceMethod) swizzlingSEL:@selector(smallCatInstanceMethod)];
        });
    }
    - (void)smallCatInstanceMethod
    {
        NSLog(@"给 small cat 分类添加了实例方法:%s",__func__);
      [self smallCatInstanceMethod]; 
    }
    @end
    

    正常情况下,这样就能处理运行时对方法的替换,不会有什么问题。但会有一些坑点,这样是不完善的,下面来看一看

    坑点1:[self smallCatInstanceMethod]是否会产生递归

    这里可能会有一个面试点,这样是否会产生递归呢?答案是不会,因为经过方法替换,自己调用自己的 smallCatInstanceMethod,其实该originSel指向的是swizzlingIMP,而swizzlingIMP指向的又会是originIMP,不会产生递归。

    循环调用是否递归?
    方法替换结果

    坑点2:父类的方法,父类实现了,子类未实现

    Animal类实现了一个方法animalInstanceMethod(),继承它的一个子类Cat未实现该方法animalInstanceMethod(),然后需要替换的就是该子类Cat的animalInstanceMethod(),而子类就会因为未实现而奔溃。

    子类未实现父类方法奔溃
    奔溃提示-[Animal smallCatInstanceMethod]找不到,就是因为在TLRuntimTool在方法替换过程中,分类替换方法中去执行[self smallCatInstanceMethod],Animal 的animalInstanceMethod的实现已经是指向Cat类的smallCatInstanceMethod,而分类中未实现animalInstanceMethod,当Animal去查找对应IMP时,是查找不到smallCatInstanceMethod,所以就崩溃了。为了解决该情况,我们就需要在TLRuntimTool中对特殊情况做处理:
    //--------TLRuntimeTool.h----------
    @interface TLRuntimeTool : NSObject
    
    +(void)runtimeMethodSwizzlingWithClass:(Class)targetCls originSEL:(SEL)oriSel swizzlingSEL:(SEL)swizzlingSel;
    
    @end
    //--------TLRuntimeTool.m ----------
    #import "TLRuntimeTool.h"
    #import <objc/runtime.h>
    
    @implementation TLRuntimeTool
    
    +(void)runtimeMethodSwizzlingWithClass:(Class)targetCls originSEL:(SEL)oriSel swizzlingSEL:(SEL)swizzlingSel
    {
        if (!targetCls) {
            NSLog(@"传入的类不能为空");
            return;
        }
        
        Method originMethod = class_getInstanceMethod(targetCls, oriSel);
        Method swizzlingMethod = class_getInstanceMethod(targetCls, swizzlingSel);
        
        BOOL swizzlingResult = class_addMethod(targetCls,
                                               oriSel,
                                               method_getImplementation(swizzlingMethod),
                                               method_getTypeEncoding(originMethod));
        /**
         * 判断是否能添加成功;YES->表明对象类没有方法,重写一个实现方法
         */
        if (swizzlingResult) {
            class_replaceMethod(targetCls,
                                swizzlingSel,
                                method_getImplementation(originMethod),
                                method_getTypeEncoding(originMethod));
        }else{
            // 原有类有实现方法
            method_exchangeImplementations(originMethod, swizzlingMethod);
        }
    }
    

    先获取到原有方法和将替换方法,对二者做一个操作,向具有给定名称和实现的目标类中添加新方法.
    1.如果返回YES,表明目标类没有方法,重写一个实现方法,替换给定类的方法的实现。即originSEL_A -> originIMP_B -> swizzlingSEL - > originIMP_B.
    2.如果返回NO,则该类有给定方法的实现,则替换两个方法的实现;即originSEL_B -> oringIMP_A.
    这样就处理之后优化了

    坑点3:父类子类都不曾实现需要替换的方法

    假设需要替换Cat类的一个animalInstanceMethod,父类声明了方法animalInstanceMethod,子类父类都未实现,按坑点2优化后的结果依然是有问题的。
    按照

    - (void)smallCatInstanceMethod
    {
        NSLog(@"给 small cat 分类添加了实例方法:%s",__func__);
      [self smallCatInstanceMethod]; 
    }
    

    这个时候就会出现递归

    递归
    原因是,子类父类都未实现animalInstanceMethod,在Cat交换smallCatInstanceMethod时再次调用自身就会产生递归。为了避免这个情况,那就还需要优化:
    
    @implementation TLRuntimeTool
    
    +(void)runtimeMethodSwizzlingWithClass:(Class)targetCls originSEL:(SEL)oriSel swizzlingSEL:(SEL)swizzlingSel
    {
        if (!targetCls) {
            NSLog(@"传入的类不能为空");
            return;
        }
        
        Method originMethod = class_getInstanceMethod(targetCls, oriSel);
        Method swizzlingMethod = class_getInstanceMethod(targetCls, swizzlingSel);
        
        if (!originMethod) {
            // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现
            class_addMethod(targetCls, oriSel, method_getImplementation(swizzlingMethod), method_getTypeEncoding(swizzlingMethod));
            method_setImplementation(swizzlingMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
        }
        
        BOOL swizzlingResult = class_addMethod(targetCls,
                                               oriSel,
                                               method_getImplementation(swizzlingMethod),
                                               method_getTypeEncoding(originMethod));
        /**
         * 判断是否能添加成功;如果成功,表明对象类没有方法,重写一个实现方法
         */
        if (swizzlingResult) {
            class_replaceMethod(targetCls,
                                swizzlingSel,
                                method_getImplementation(originMethod),
                                method_getTypeEncoding(originMethod));
        }else{
            // 原有类有实现方法
            method_exchangeImplementations(originMethod, swizzlingMethod);
        }
    }
    

    添加了一个对原有方法的判断,避免对没有实现的方法进行替换而出错,在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现。

    这样就非常棒的解决了几个特殊情况下导致的错误。

    方法替换的应用

    method-swizzling最常用的应用是防止数组越界奔溃、字典取值崩溃等情况。
    在iOS中NSNumber、NSArray、NSDictionary等这些类都是类簇,NSArray的实现时可能会由多个类组成。所以如果想对NSArray进行替换,必须获取到其本类(__NSArrayM)进行替换,直接对NSArray进行操作是无效的。类簇详情请参看Apple文档class cluster的内容.

    // 开发过程中断点时留意下就知道其底层的类,如 mutA __NSArrayM * @"2 elements" 0x00006000036fd770
    类簇                              “真身”
    NSArray               --->    __NSArrayI
    NSMutableArray        --->    __NSArrayM
    NSDictionary          --->    __NSDictionaryI
    NSMutableDictionary   --->    __NSDictionaryM
    NSNumber              --->    __NSCFNumber
    

    NSArray为例,新建一个NSArray分类,在里面添加以下方法,区分开发模式发布模式

    
    #import "NSArray+AvoidCrash.h"
    #import "TLRuntimeTool.h"
    #import <objc/runtime.h>
    @implementation NSArray (AvoidCrash)
    + (void)load
    {
            /** 保证执行一次交换
             *  定义一个Runtime工具类,在其中实现交换
             * animalInstanceMethod:原始待替换方法
             * smallCatInstanceMethod:替换之后的方法
             */
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            
            [TLRuntimeTool runtimeMethodSwizzlingWithClass:NSClassFromString(@"__NSArrayI")
                                                 originSEL:@selector(objectAtIndex:)
                                              swizzlingSEL:@selector(avoidCrashObjectsAtIndexes:)];
    
        });
    }
    
    //__NSArrayI  objectAtIndex:
    - (id)avoidCrashObjectsAtIndexes:(NSUInteger)index {
    
    #ifdef DEBUG  // 开发模式
          return  [self avoidCrashObjectsAtIndexes:index];
    #else // 发布模式
        id object = nil;
        @try {
           object = [self avoidCrashObjectsAtIndexes:index];
       }
       @catch (NSException *exception) {
           // 捕捉到的错误
           
           NSLog(@"** Exception class :%s ** Exception Method: %s \n", class_getName(self.class), __func__);
           
           NSLog(@"Uncaught exception description: %@", exception);
           
           NSLog(@"%@", [exception callStackSymbols]);
           
       }
       @finally {
           return object;
       }
    #endif
    }
    @end
    

    写一个数组,取值测试如下

        NSArray * mutA = @[@"3",@"2"];
        NSLog(@"%@",[mutA objectAtIndex:3]);
    
    • 开发模式


      该崩还是让它崩
    • 模拟发布模式


      读取崩溃错误

      打印了错误日志,但程序不会崩溃

    GitHub工程地址

    相关文章

      网友评论

        本文标题:OC底层原理15-Method Swizzling

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