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对应关系的函数:markdown

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

获取实例方法post

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,使用以下代码:spa

// 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); 
    } 
}
复制代码
相关文章
相关标签/搜索