iOS底层探索-Method Swizzling

1、概述

利用 OCRuntime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到 OC方法调用流程改变 的目的,主要用于OC方法

1.1、关系类比

类比关系为书的目录页,SEL是标题IMP是页码

  • 看到标题(SEL)我们能大致知道这几页讲的是什么;根据页码(IMP)我们能快速找到内容位置
  • 它们之间是一一对应的,但我们也可以将它们的对应关系进行修改(书印错了我们改一下不犯法~)

1.2、交换方法

  • Runtime 提供了 交换两个 SEL 和 IMP 对应关系的函数:

    OBJC_EXPORT void
    method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
       OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    复制代码
    /* OBJC_AVAILABLE: shorthand for all-OS availability */
    
    #   if !defined(OBJC_AVAILABLE)
    #       define OBJC_AVAILABLE(x, i, t, w, b)                    
                __OSX_AVAILABLE(x)  __IOS_AVAILABLE(i)  __TVOS_AVAILABLE(t) \
                __WATCHOS_AVAILABLE(w)
    #   endif
    复制代码
    • OBJC_AVAILABLE(10.5, 2.0, 9.0, 2.0)表示这个这个API在哪个系统哪个版可用:
      • __OSX_AVAILABLE(x):Mac OS的版本
      • __IOS_AVAILABLE(i) :iOS系统的版本
      • __TVOS_AVAILABLE(t) :苹果电视系统的版本
      • __WATCHOS_AVAILABLE(w) :苹果手表系统的版本
  • 通过这个函数交换两个 SEL 和 IMP 对应关系的技术,我们就称之为Method Swizzle方法欺骗image.png

1.3、AOP面向切面编程

  • Runtime机制对于AOP面向切面编程提供良好的支持,在OC中,可利用 Method Swizzling 实现 AOP
  • 其中 AOPAspect Oriented Programming)是一种编程的思想,和面向对象编程OOP有本质的区别:
    • OOPAOP 都是编程的思想
    • OOP 编程思想更加倾向于 对业务模块的封装,划分出更加清晰的逻辑单元
    • AOP 是面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性

2、API

2.1、通过 SEL 获取方法Method

// 获取实例方法
OBJC_EXPORT Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);

// 获取类方法
OBJC_EXPORT Method _Nullable
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name);
复制代码

2.2、IMP 的getter/setter方法

// 获取一个方法的实现
OBJC_EXPORT IMP _Nonnull
method_getImplementation(Method _Nonnull m); 

// 设置一个方法的实现
OBJC_EXPORT IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp):
复制代码

2.3、替换方法

// 获取方法实现的编码类型
OBJC_EXPORT const char * _Nullable
method_getTypeEncoding(Method _Nonnull m);

// 添加方法实现
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types);
                
// 替换方法的 IMP,如:A替换B(B指向A,A还是指向A)
OBJC_EXPORT IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types);
                    
// 交换两个方法的 IMP,如:A交换B(B指向A,A指向B)
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);
复制代码

3、坑点

3.1、保证方法交换只执行一次

为了保证方法交换的代码可以优先执行,有时候会将其写在+load方法中,但是 +load 方法也能被主动调用,如果多次调用,交换后的方法可能被还原;所以我们要保证方法只交换一次,可以选择在单例模式下

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self lz_methodSwizzlingWithClass:self oriSEL:@selector(study) swizzledSEL:@selector(play)];
    });
}

+ (void)lz_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    method_exchangeImplementations(oriMethod, swiMethod);
}
复制代码

3.2、父类未实现子类将要交换的方法

  1. 父类 LZPerson 中,实现 study 方法

    #import <Foundation/Foundation.h>
    @interface LZPerson : NSObject
    
    - (void)study;
    @end
    
    @implementation LZPerson
    
    - (void)study{
        NSLog(@"LZPerson:%s",__func__);
    }
    @end
    复制代码
  2. 子类 LZStudent 中,实现 play 方法,在 +load 方法中,和父类的 study 方法交换

    #import "LZPerson.h"
    #import <objc/runtime.h>
    
    @interface LZStudent : LZ`Person
    @end
    
    @implementation LZStudent
    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [self lz_methodSwizzlingWithClass:self oriSEL:@selector(study) swizzledSEL:@selector(play)];
        });
    }
    
    + (void)lz_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
        if (!cls) NSLog(@"传入的交换类不能为空");
    
        Method oriMethod = class_getInstanceMethod(cls, oriSEL);
        Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
        method_exchangeImplementations(oriMethod, swiMethod);
    }
    
    - (void)play{
        [self play];
        NSLog(@"LZStudent:%s",__func__);
    }
    
    @end
    复制代码
  3. 子类正常调用,但父类找不到 play 方法

    // 子类调用
    LZPerson-[LZPerson study]
    LZStudent:-[LZStudent play]
    // 父类调用
    -[LZPerson play]: unrecognized selector sent to instance 0x28218c3f0
    复制代码

    方法交换应该只影响当前类,但子类中交换的是父类方法,导致父类受到影响,其他继承于该父类的子类也会出现问题

  4. 解决办法:保证方法交换只对当前类生效

    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [self lz_betterMethodSwizzlingWithClass:self oriSEL:@selector(study) swizzledSEL:@selector(play)];
        });
    }
    
    + (void)lz_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
        if (!cls) NSLog(@"传入的交换类不能为空");
    
        Method oriMethod = class_getInstanceMethod(cls, oriSEL);
        Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
        BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
    
        if (success) {
            class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }else{
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    }
    复制代码
    • 使用class_addMethod,对当前类添加 study 方法,关联 play方法imp
    • 返回值为 YES,证明当前类中未实现 study 方法
    • 如果方法添加成功,使用class_replaceMethod将 play方法 替换为 study 的 imp
上述方式需要注意:
  • 如果子类实现 study 方法

    • 添加失败,直接交换
    • 不会影响父类
  • 如果子类未实现 study 方法

    • 添加成功,新方法关联 play 的 imp
    • play 替换为 父类 study 的 imp
    • 调用顺序,依然保持:子类play --> 父类study
    • 只会影响子类,不会影响父类

3.3、父类和子类都未实现原始方法

父类和子类都未实现原始方法,上述方式将引发子类方法的递归调用,最终造成 堆栈溢出

  • 原因在于:

    • 子类添加的 study,关联 play 的imp
    • 父类未实现study方法,子类使用class_replaceMethod,一定会替换失败,所以 子类的play的imp未发生改变
  • 解决办法,对原始方法增加是否实现的判断条件

    + (void)lz_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
        if (!cls) NSLog(@"传入的交换类不能为空");
    
        Method oriMethod = class_getInstanceMethod(cls, oriSEL);
        Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
        if (!oriMethod) {
    
            IMP imp = imp_implementationWithBlock(^(id self, SEL _cmd){
                NSLog(@"伪装study方法,其实什么都没做");
            });
    
            class_addMethod(cls, oriSEL, imp, method_getTypeEncoding(swiMethod));
        }
    
        BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
    
        if (success) {
            class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }else{
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    }
    复制代码
    • study 方法,关联一个空方法的imp
    • 使用class_addMethod,对 当前类添加 study 方法关联play的imp
    • 由于 study 已添加,此时返回值一定为NO,添加失败
    • 使用method_exchangeImplementations,直接将两个方法进行交换

4、类方法的交换

类方法和实例方法的区别,类方法存储在元类的方法列表中,所以对类方法的添加和替换,不能直接使用Class,而是要使用当前Class所属的 MetaClass

+ (void)lz_betterClassMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    
    Class metaClass = objc_getMetaClass(NSStringFromClass(cls).UTF8String);
    
    Method oriMethod = class_getInstanceMethod(metaClass, oriSEL);
    Method swiMethod = class_getInstanceMethod(metaClass, swizzledSEL);
   
    if (!oriMethod) {

        IMP imp = imp_implementationWithBlock(^(id self, SEL _cmd){
            NSLog(@"伪装study方法,其实什么都没做");
        });
        
        class_addMethod(metaClass, oriSEL, imp, method_getTypeEncoding(swiMethod));
    }
    
    BOOL success = class_addMethod(metaClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));

    if (success) {
        class_replaceMethod(metaClass, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}
复制代码

5、数组、字典的方法交换

iOS 中,NSArrayNSDictionary 等类,都有类簇的存在,因为一个NSArray的实现,可能由多个类组成;所以对NSArray、NSDictionary进行方法交换,必须对其真身进行操作

类名 类簇
NSArray __NSArrayI
NSMutableArray __NSArrayM
NSDictionary __NSDictionaryI
NSMutableDictionary __NSDictionaryM

替换NSArray的objectAtIndex方法,避免数组越界

@implementation NSArray (LZ)

+ (void)load{
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lzl_objectAtIndex:));
    method_exchangeImplementations(fromMethod, toMethod);
}

- (id)lz_objectAtIndex:(NSUInteger)index{

    if (self.count-1 < index) {
#ifdef DEBUG
        // 调试阶段
        return [self lz_objectAtIndex:index];
#else 
        // 发布阶段
        @try {
            return [self lz_objectAtIndex:index];
        } @catch (NSException *exception) {
            NSLog(@"lz_objectAtIndex crash:%@", [exception callStackSymbols]);
            return nil;
        } @finally {
            
        }
#endif
    }else{
        return [self lz_objectAtIndex:index];
    }
}
@end
复制代码

6、Runtime方法使用汇总

猜你喜欢

转载自juejin.im/post/7119513477978259469