iOS探索 runtime面试题分析( 八、Method Swizzing坑点)

八、Method Swizzing坑点

method swizzing不了解的可以阅读iOS逆向 代码注入+Hook

1.黑魔法应用

在日常开发中,再好的程序员都会犯错,比如数组越界

NSArray *array = @[@"F", @"e", @"l", @"i", @"x"];
NSLog(@"%@", array[5]);
NSLog(@"%@", [array objectAtIndex:5]);

因此为了避免数组越界这种问题,大神们开始玩起了黑魔法——method swizzing

  • 新建NSArray分类
  • 导入runtime头文件——<objc/runtime.h>
  • 写下新的方法
  • +load利用黑魔法交换方法
#import "NSArray+FX.h"
#import <objc/runtime.h>

@implementation NSArray (FX)

+ (void)load {
    // 交换objectAtIndex方法
    Method oriMethod1 = class_getInstanceMethod(self, @selector(objectAtIndex:));
    Method swiMethod1 = class_getInstanceMethod(self, @selector(fx_objectAtIndex:));
    method_exchangeImplementations(oriMethod1, swiMethod1);

    // 交换取下标方法
    Method oriMethod2 = class_getInstanceMethod(self, @selector(objectAtIndexedSubscript:));
    Method swiMethod2 = class_getInstanceMethod(self, @selector(fx_objectAtIndexedSubscript:));
    method_exchangeImplementations(oriMethod2, swiMethod2);
}

- (void)fx_objectAtIndex:(NSInteger)index {
    if (index > self.count - 1) {
        NSLog(@"objectAtIndex————————数组越界");
        return;
    }
    return [self fx_objectAtIndex:index];
}

- (void)fx_objectAtIndexedSubscript:(NSInteger)index {
    if (index > self.count - 1) {
        NSLog(@"取下标————————数组越界");
        return;
    }
    return [self fx_objectAtIndexedSubscript:index];
}

@end

然而程序还是无情的崩了...

其实在iOS中NSNumber、NSArray、NSDictionary等这些类都是类簇(Class Clusters),一个NSArray的实现可能由多个类组成。所以如果想对NSArray进行方法交换,必须获取到其真身进行方法交换,直接对NSArray进行操作是无效的

以下是NSArrayNSDictionary本类的类名

这样就好办了,可以使用runtime取出本类

2.坑点一

黑魔法最好写成单例,避免多次交换

比如说添加了[NSArray load]代码,方法实现又交换回去了导致了崩溃

+load方法改写成单例(虽然不常见,但也要避免)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 交换objectAtIndex方法
        Method oriMethod1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
        Method swiMethod1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(fx_objectAtIndex:));
        method_exchangeImplementations(oriMethod1, swiMethod1);

        // 交换取下标方法
        Method oriMethod2 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
        Method swiMethod2 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(fx_objectAtIndexedSubscript:));
        method_exchangeImplementations(oriMethod2, swiMethod2);
    });
}

3.坑点二

①子类交换父类实现的方法

  • 父类FXPerson类中有-doInstance方法,子类FXSon类没有重写
  • FXSon类新建分类做了方法交换,新方法中调用旧方法
  • FXPerson类FXSon类调用-doInstance

子类打印出结果,而父类调用却崩溃了,为什么会这样呢?

因为FXSon类交换方法时取得doInstance先在本类搜索方法,再往父类里查找,在FXFather中找到了方法实现就把它跟新方法进行交换了。可是新方法是在FXSon分类中的,FXFather找不到imp就unrecognized selector sent to instance 0x600002334250

所以这种情况下应该只交换子类的方法,不动父类的方法

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method oriMethod = class_getInstanceMethod(self, @selector(doInstance));
        Method swiMethod = class_getInstanceMethod(self, @selector(fx_doInstance));

        BOOL didAddMethod = class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        if (didAddMethod) {
            class_replaceMethod(self, @selector(fx_doInstance), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        } else {
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    });
}

  • 通过class_addMethodFXSon类添加方法(class_addMethod不会取代本类中已存在的实现,只会覆盖本类中继承父类的方法实现)
    • 取得新方法swiMethod的实现和方法类型
    • 往方法名@selector(fx_doInstance)添加方法
    • class_addMethod 把新方法实现放到旧方法名中,此刻调用doInstance就是调用fx_doInstance,但是调用fx_doInstance会崩溃
  • 根据didAddMethod判断是否添加成功
    • 添加成功说明之前本类没有实现——class_replaceMethod替换方法
    • 添加失败说明之前本类已有实现——method_exchangeImplementations交换方法
    • class_replaceMethoddoInstance方法实现替换掉fx_doInstance中的方法实现

FXPerson类只写了方法声明,没有方法实现,却做了方法交换——会造成死循环

  • doInstance方法中添加了新的方法实现
  • fx_doInstance方法中想用旧的方法实现替代之前的方法实现,可是找不到doInstance实现,导致class_replaceMethod无效->在fx_doInstance中调用fx_doInstance就会死循环

因此改变代码逻辑如下

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method oriMethod = class_getInstanceMethod(self, @selector(doInstance));
        Method swiMethod = class_getInstanceMethod(self, @selector(fx_doInstance));

        if (!oriMethod) {
            class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
            method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd) {
                NSLog(@"方法未实现");
            }));
        }

        BOOL didAddMethod = class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        if (didAddMethod) {
            class_replaceMethod(self, @selector(fx_doInstance), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        } else {
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    });
}

  • 未实现方法时用新的方法实现添加方法,此时调用doInstance就是调用fx_doInstance
  • 由于此时fx_doInstance方法内部还是调用自己,用block修改fx_doInstance的实现,就可以断开死循环了
  • 由于oriMethod(0x0),method_exchangeImplementations交换失败

4.注意事项

使用Method Swizzling有以下注意事项:

  • 尽可能在+load方法中交换方法
  • 最好使用单例保证只交换一次
  • 自定义方法名不能产生冲突
  • 对于系统方法要调用原始实现,避免对系统产生影响
  • 做好注释(因为方法交换比较绕)
  • 迫不得已情况下才去使用方法交换

这是一份做好封装的Method Swizzling交换方法

+ (void)FXMethodSwizzlingWithClass:(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) {
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd) {
            NSLog(@"方法未实现");
        }));
    }

    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (didAddMethod) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    } else {
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}

2020面试刷题与技术储备专区

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容