第十九节—Method_Swizzling

本文为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 

@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 

@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 

@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 

@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 

@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 

@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 

@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)