iOS底层探究—–Method Swizzling

前言

这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战

基本原理

我们都知道,类的最终父类是NSObject,在程序编译后,在底层可以发现类就是一个结构体,每个类都有一个 isa 指针,能够访问到结构体里面的数据。方法查找的是时候,是在类的方法列表里面,通过SEL查找对应的IMP

大家或多或少听到过iOS黑魔法,也就是方法交换。同时苹果的运行时 runtime 也提供了一个很好的环境。利用OCRuntime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法。下面用两个图例来展示下:

  • 交换前(正常情况):

882505D3-1FCC-4CDB-AF73-61E743933956.png

  • 交换后:

A085AFD2-4E52-49C6-9EDC-7F3BD513C942.png

根据这两张图,我们能稍微明白些这个交换的是怎么一回事。Runtime提供了交换两个SELIMP对应关系的函数:

OBJC_EXPORT void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
复制代码

Runtime机制对于AOP面向切面编程提供良好的支持。在OC中,可利用Method Swizzling实现AOP,其中AOPAspect Oriented Programming)是一种编程的思想,同样面向对象编程OOP也一种编程的思想,但是AOPOOP有本质的区别:

  • OOP编程思想,他更加倾向于对业务模块的封装,同时也能够划分出更为清晰的业务逻辑单元;
  • AOP编程思想,是面向切面进行提取封装,提取各个模块中的公共部分,这样能提高模块的复用率,降低业务之间的耦合性;

API 介绍

  • 通过SEL获取方法Method

获取实例方法

OBJC_EXPORT Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)  OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
复制代码

获取类方法

OBJC_EXPORT Method _Nullable
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
复制代码
  • IMPgetter/setter方法:

获取某个方法的实现

OBJC_EXPORT IMP _Nonnull
method_getImplementation(Method _Nonnull m) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
复制代码

设置一个方法的实现

OBJC_EXPORT IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
复制代码
  • 获取方法实现的编码类型
OBJC_EXPORT const char * _Nullable
method_getTypeEncoding(Method _Nonnull m) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
复制代码
  • 添加方法实现
OBJC_EXPORT void
class_addMethods(Class _Nullable, struct objc_method_list * _Nonnull) OBJC2_UNAVAILABLE;
复制代码
  • 替换方法的IMP
OBJC_EXPORT IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
复制代码
  • 交换两个方法的IMP
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
复制代码
  • 尽量放在单利里面,这样能保证只调用一次,保证安全。

案例分析

交换方法的使用

创建一个类LGPerson,然后创建LGTeacher继承LGPerson,使用如下代码:

// LGPerson .h 
@interface LGPerson : NSObject 

- (void)person_instanceMethod; 

@end 

// LGPerson.m 
@implementation LGPerson 

- (void)person_instanceMethod {
    NSLog(@"\n 打印 person_instanceMethod: %s\n", __func__); 
} 

@end 

// LGTeacher.h 
@implementation LGTeacher

+ (void)load {
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ [LGRuntimeUtil 
    lg_methodSwizzlingWithClass:self 
                         oriSEL:@selector(person_instanceMethod) 
                    swizzledSEL:@selector(teacher_instanceMethod)]; 
    });
} 

- (void)teacher_instanceMethod {
    NSLog(@"\n 打印 teacher_instanceMethod: %s\n", __func__); 
} 
    
@end 

// 封装LGRuntimeUtil.m 
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL { 

    if (!cls) NSLog(@"传入的交换类不能为空");
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL); 
    Method swizzleMethod = class_getInstanceMethod(cls, swizzledSEL); 
    method_exchangeImplementations(oriMethod, swizzleMethod); 
}
复制代码

main 文件里面初始化这两个类,都调用 person_instanceMethod 方法:

    LGPerson  *person = [LGPerson alloc] init];
    [person person_instanceMethod];
    
    LGTeacher  *teacher = [LGTeacher alloc] init];
    [teacher person_instanceMethod];
复制代码

但是,打印出来的方法名,却都是 teacher_instanceMethod 。那么就说明替换成功了。

  • 因为person_instanceMethodSEL 找到的是teacher_instanceMethodIMP,所以找到的就是teacher_instanceMethod方法;

  • teacher_instanceMethodSEL 找到的却是person_instanceMethodIMP,但IMP对应的是person_instanceMethod方法,再继续根据person_instanceMethod方法的 SEL 找到的是交换后的IMP,所以找到了teacher_instanceMethod方法。

递归问题

就是在 teacher_instanceMethod 方法里面,再次调用 teacher_instanceMethod,代码如下:

- (void)teacher_instanceMethod { 
    [self teacher_instanceMethod]; 
    NSLog(@"\n 打印 teacher_instanceMethod: %s\n", __func__);
}
复制代码

运行之后,直接报错:

BA08173A-4A03-4AA2-8395-7372712C9B43.png

为什么会这样了?

  • 对于LGTeacher而言调用person_instanceMethod就是调用LGTeacher:teacher_instanceMethod-> LGPerson:person_instanceMethod

  • 对于LGPerson调用person_instanceMethod是调用LGTeacher:teacher_instanceMethod -> LGPerson:teacher_instanceMethod。而LGPerson没有实现teacher_instanceMethod,所以报错。

所以交换方法一定是去交换自己的方法

  • 为什么要调用自己呢?

因为有时候,在做一些处理的时候,需要保持原来的逻辑,所以需要再次调用本类。

  • 那怎样才能避免这类的情况了?

可以通过class_addMethod去尝试添加要交换的方法。

性能优化一

  • class_addMethod方法的使用,我们可以使用这个方法来添加要交换的方法:

    • 如果添加成功,说明在本类中没有这个方法,但是可以通过class_replaceMethod进行替换,其内部会调用class_addMethod进行添加的方法;

    • 如果添加不成功,就说明类里面有这个方法,则通过method_exchangeImplementations进行交换

  • 代码如下:

+ (void)lg_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); 
    } 
}
复制代码

性能优化二

根据上面的使用案例,如果子类和父类都没有实现person_instanceMethod这个方法,在子类里面调用[self teacher_instanceMethod]时,就会产生递归,如果不处理,就回报错。

怎么解决了?如果该方法不存在,可以在添加方法后,再给此方法添加一个空的实现,也就是相当于增加一个不做任何事情的IMP,代码如下:

+ (void)ssl_bestMethodSwizzlingWithClass:(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) { 
        // 在 oriMethod 为 nil 时,替换后将swizzledSEL复制一个不做任何事的空实现 
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); 
        
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ NSLog(@"来了一个空的 imp"); })); 
    } 
    
    // 尝试添加你要交换的方法 
    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); 
    } 
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享