美文网首页
iOS底层系列19 -- Method-Swizzling

iOS底层系列19 -- Method-Swizzling

作者: YanZi_33 | 来源:发表于2021-03-12 16:02 被阅读0次
Method-Swizzling
  • Method-Swizzling即方法的交换,其实现方式是在运行时将一个方法的实现替换成另一个方法的实现,这就是我们常说的iOS黑魔法。
  • 每个类都维护着一个方法类表即rwe中的method_array_t,在method_array_t中存储中许多不同的方法即Method,在iOS底层系列16 -- 类的加载机制已经探索了在运行时,这些主类的方法集合和分类的方法集合是如何加载进入主类class rwe结构中的method_array_t当中的。
  • 每个Method都包含sel和IMP两个部分,分别表示method的名称和method的实现;方法交换就是将sel和imp原本的对应断开,并将sel和新的IMP生成对应关系。
Snip20210312_114.png
Method-Swizzling常见函数
  • class_getInstanceMethod:获取实例方法
  • class_getClassMethod:获取类方法
  • method_getImplementation:获取方法的实现
  • method_setImplementation:设置方法的实现
  • method_getTypeEncoding:获取方法实现的编码类型
  • class_addMethod:给方法添加实现
  • class_replaceMethod:用一个方法的实现,替换另一个方法的实现,即IMP1指向 IMP2,但是IMP2不一定指向IMP1
  • method_exchangeImplementations:交换两个方法的实现,即IMP1指向IMP2,IMP2指向IMP1

下面通过几个案例来阐述在使用Method-Swizzling过程中需要注意的事项
准备工作:新建两个类父类YYPerson子类YYStudent,Method-Swizzling帮助类YYRuntimeHelperYYStudent分类(Add)

#import <Foundation/Foundation.h>

@interface YYPerson : NSObject

- (void)walk;

@end

#import "YYPerson.h"

@implementation YYPerson

- (void)walk{
    NSLog(@"%s",__func__);
}

@end
#import <Foundation/Foundation.h>

@interface YYStudent : NSObject

- (void)read;

- (void)write;

@end

#import "YYStudent.h"

@implementation YYStudent

- (void)read{
    NSLog(@"%s",__func__);
}

- (void)write{
    NSLog(@"%s",__func__);
}

@end
#import "YYStudent.h"

@interface YYStudent (Add)

@end

#import "YYStudent+Add.h"
#import "YYRuntimeHelper.h"

@implementation YYStudent (Add)

//在运行时,进行方法的交换
+ (void)load{
    [YYRuntimeHelper yy_instanceMethodSwizzlingithClass:self originalSEL:@selector(read) swizzledSEL:@selector(yy_read)];
}

- (void)yy_read{
    NSLog(@" 使用yy_read的方法去实现read");
}

@end
#import <Foundation/Foundation.h>

@interface YYRuntimeHelper : NSObject

+ (void)yy_instanceMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL;

@end

#import "YYRuntimeHelper.h"
#import <objc/runtime.h>

@implementation YYRuntimeHelper

+ (void)yy_instanceMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL{
    Method originalMethod = class_getInstanceMethod(cls, originalSEL);
    Method swizzledMethod = class_getInstanceMethod(cls, swizzledSEL);
    //方法交换
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

@end
案例一:外界主动调用目标类的load方法,导致Method-Swizzling失效
Snip20210312_116.png

上面的截图是正常情况;但如果在AppDelegate.m文件中,主动调用[YYStudent load],那么当代码执行到控制器时,Method-Swizzling就失效了,因为前后交换了两次还原了。

Snip20210312_118.png

解决方案:保证YYStudent(Add)中load方法中的Method-Swizzling只执行一次,加入dispatch_once
YYStudent(Add)修改后的代码如下:

#import "YYStudent+Add.h"
#import "YYRuntimeHelper.h"

@implementation YYStudent (Add)

//在运行时,进行方法的交换
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [YYRuntimeHelper yy_instanceMethodSwizzlingithClass:self originalSEL:@selector(read) swizzledSEL:@selector(yy_read)];
    });
}

- (void)yy_read{
    NSLog(@" 使用yy_read的方法去实现read");
}

@end
案例二:子类没有实现目标方法,但是父类实现了目标方法,在子类的分类中交换目标方法的实现时,最终交换的是父类的实现。

例如上面的YYPerson类有walk的实现,但在子类YYStudent中没有walk的实现;YYStudent分类中的交换实现如下:

#import "YYStudent+Add.h"
#import "YYRuntimeHelper.h"

@implementation YYStudent (Add)

//在运行时,进行方法的交换
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [YYRuntimeHelper yy_instanceMethodSwizzlingithClass:self originalSEL:@selector(walk) swizzledSEL:@selector(yy_walk)];
    });
}

- (void)yy_walk{
   //这里不会出现循环调用,因为已经实现了方法的交换
   [self yy_walk];
    NSLog(@" 使用yy_walk的方法去实现walk");
}

@end

外界调用及LLDB调试如下:

Snip20210315_122.png Snip20210315_121.png

出现了崩溃,首先因为子类YYStudent没有walk方法实现,所以Method-Swizzling交换的是父类的walk,见下图所示:

Snip20210315_123.png
  • [student walk]由于student没有walk方法实现,会调用父类YYPerson的walk方法,而父类的walk在运行时进行了Method-Swizzling,所以会调用YYStudent(Add)中的yy_walk方法实现,其内部首先调用[self yy_walk],此时的self是student,调用的则是父类YYPerson的walk方法实现;
  • [person walk]则调用了YYStudent(Add)中的yy_walk方法实现,其内部首先调用[self yy_walk]此时的self是person,而YYPerson类中并没有yy_walk方法,所以会出现崩溃。
  • 本意是交换子类的walk实现,不想将父类的walk实现也交换了,且会发生崩溃;为了Method-Swizzling不影响父类该怎么做?

解决方案:在进行方法交换时,首先判断当前类是否有目标方法,如果有直接进行交换;如果没有调用class_addMethod函数,给当前类动态添加一个方法(目标方法),但其实现是交换方法的实现;添加成功之后再调用class_replaceMethod将交换方法的实现替换成目标方法的实现;这样才完整的实现了Method-Swizzling

YYRuntimeHelper修改后的代码如下:

#import "YYRuntimeHelper.h"
#import <objc/runtime.h>

@implementation YYRuntimeHelper

+ (void)yy_instanceMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) {
        return;
    }
    
    Method original_method = class_getInstanceMethod(cls, originalSEL);
    Method swizzled_method = class_getInstanceMethod(cls, swizzledSEL);
    
    IMP original_imp = method_getImplementation(original_method);
    IMP swizzled_imp = method_getImplementation(swizzled_method);
    
    //主要用来检测当前类是否有original_method,避免替换了父类的实现
    //originalSEL --> swizzled_imp
    BOOL success = class_addMethod(cls, originalSEL, swizzled_imp, method_getTypeEncoding(original_method));
    //添加成功,表示当前类没有original_method
    if (success) {
        //swizzledSEL --> original_imp
        //因为当前类没有original_method 这里的original_imp是当前类父类的实现
        class_replaceMethod(cls, swizzledSEL, original_imp, method_getTypeEncoding(original_method));
    }else{//添加失败,表示当前类有original_method
        //方法交换
        method_exchangeImplementations(original_method,swizzled_method);
    }
}

@end
  • 注意这里的original_imp是父类的实现,因为当前类没有original_method

LLDB调试结果如下:

Snip20210312_120.png
案例三:子类没有实现目标方法,且父类也没有实现目标方法。

外界调用及LLDB调试结果如下:

Snip20210315_124.png Snip20210315_125.png

Method-Swizzling的代码实现:

Snip20210315_126.png
  • 首先运行时进行Method-Swizzling,由于当前类YYStudent没有walk方法,所以会调用class_addMethod函数,给YYStudent添加walk方法其实现为yy_walk;添加成功后会执行class_replaceMethod函数,但由于original_imp不存在,所以会交换失败;那么Method-Swizzling只实现了单方的交换,如下图所示:
Snip20210315_128.png
  • [student walk]时,会来到yy_walk方法,当执行[self yy_walk]时,由于yy_walk并没有交换成功,所以调用的实现还是自己本身,则出现循环调用,最后导致内存溢出应用崩溃。

解决方案:在进行Method-Swizzling之前,对原始方法进行检测,若为空调用class_addMethod函数,添加原始方法walk,方法实现为交换方法实现yy_walk;然后将交换方法yy_walk的实现置为{},即为空方法什么也不做,接着再调用class_addMethod函数,此时上面已经添加过了,所以再次添加会不成功,最后走method_exchangeImplementations函数,但由于original_method是空的,所以不会发生交换,最终的效果如下所示:

Snip20210315_130.png

YYRuntimeHelper修过之后的代码如下:

+ (void)yy_instanceMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) {
        return;
    }
    
    Method original_method = class_getInstanceMethod(cls,originalSEL);
    Method swizzled_method = class_getInstanceMethod(cls,swizzledSEL);
    
    IMP original_imp = method_getImplementation(original_method);
    IMP swizzled_imp = method_getImplementation(swizzled_method);
    
    if (!original_method) {
        //在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现
        class_addMethod(cls,originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
        
        method_setImplementation(swizzled_method, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }

    BOOL didAddMethod = class_addMethod(cls,originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
    if (didAddMethod) {
        class_replaceMethod(cls,swizzledSEL,original_imp, method_getTypeEncoding(original_method));
    }else{
        method_exchangeImplementations(original_method,swizzled_method);
    }
}

method_exchangeImplementations源码实现如下:

Snip20210315_131.png
Method-Swizzling之类方法

上面介绍的是实例方法的交换,类方法的交换原理是类似的,只不过类方法存储在元类,具体实现代码如下:

+ (void)yy_classMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL{
    if (!cls) {
        return;
    }
    
    class_getClassMethod(cls,originalSEL);
    Method original_method = class_getClassMethod([cls class],originalSEL);
    Method swizzled_method = class_getClassMethod([cls class],swizzledSEL);
    
    IMP original_imp = method_getImplementation(original_method);
    IMP swizzled_imp = method_getImplementation(swizzled_method);
    
    if (!original_method) {
        //在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现
        //object_getClass(cls) 获取元类
        class_addMethod(object_getClass(cls),originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
        method_setImplementation(swizzled_method,
                                 imp_implementationWithBlock(^(id self,SEL _cmd){
            NSLog(@"来了一个空的 imp");
        }));
    }

    BOOL didAddMethod = class_addMethod(object_getClass(cls),originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
    if (didAddMethod) {
        class_replaceMethod(object_getClass(cls),swizzledSEL,original_imp, method_getTypeEncoding(original_method));
    }else{
        method_exchangeImplementations(original_method,swizzled_method);
    }
}

相关文章

网友评论

      本文标题:iOS底层系列19 -- Method-Swizzling

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