ios 安全的Method Swizzing

Method Swizzing方法交换,在Objective-C中使用还是比较常见的,要搞清它的本质,要首先理解方法的本质。

一、方法的本质

Objective-C中,方法是由SELIMP组成的,前者叫做方法编号,后者叫方法实现。Objective-C中调用方法,其实就是通过SEL查找IMP的过程。
SEL:表示选择器,通常可以把它理解为一个字符串。运行时维护着一张全局的SEL表,将相同字符串的方法名映射到唯一一个SEL。 通过sel_registerName(char *name)方法,可以查找到这张表中方法名对应的SEL。苹果提供了一个语法糖@selector用来方便地调用该函数。
IMP:是一个函数指针。objc中的方法最终会被转换成纯C的函数,IMP就是为了表示这些函数的地址。

二、Method Swizzing原理

方法混淆就是利用runtime特性以及方法的组成本质实现的。比如有方法sel1对应imp1sel2对应imp2,经过方法混淆,使得runtime在方法查找时将sel1的查找结果变为imp2,如下图:

Method Swizzing.png

三、安全实现

新建一个项目,添加两个类:AnimalDog(父类为Animal),添加一个方法-eat

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

添加一个Dog的Method Swizzing分类,

+ (void)load {
    Method oriMethod = class_getInstanceMethod(self, @selector(eat));
    Method swiMethod = class_getInstanceMethod(self, @selector(swi_eat));
    
    method_exchangeImplementations(oriMethod, swiMethod);
}
- (void)swi_eat{
    NSLog(@"%s", __func__);
    
    [self swi_eat];
}

下面具体分析一下可能遇到的坑点,以及如何避免。

3.1 多次交互

最好写成单例,避免方法多次交换。例如手动调用+ load方法。

viewController 执行下面的代码:

    [Dog load];
    
    Dog *dog = Dog.new;
    [dog eat];

打印结果为:

-[Dog eat]

这是因为执行了两次方法交换,方法被换回去了,为了避免这种意外,最好写成单例。

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

  • 父类Animal实现了-eat方法,子类Dog没有重写
  • Dog分类进行方法交换

AnimalDog分别调用-eat方法,子类正常打印,但是父类确崩溃了,为什么呢?
因为Dog交换方法时,先在本类查找-eat方法,再往父类查找。在父类 Animal找到方法实现,就执行了方法交换。但是新方法-swi_eat在子类DogAnimal找不到就报异常:reason: '-[Animal swi_eat]: unrecognized selector sent to instance 0x600000267590'

所以安全的实现,应该只交换子类的方法,不动父类方法

+ (void)load {
    //1、单例,避免多次交互
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        Method oriMethod = class_getInstanceMethod(self, @selector(eat));
        Method swiMethod = class_getInstanceMethod(self, @selector(swi_eat));
        
        // 3、取得新方法swiMethod的实现和方法类型签名
        //把新方法实现放到旧方法名中,此刻调用-eat就是调用-swi_eat
        BOOL didAddMethod = class_addMethod(self, @selector(eat), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        if (didAddMethod) {
            // 3.1、添加成功,当前类未实现,父类实现了-eat
            // 此时不必做方法交换,直接将swiMethod的IMP替换为父类oriMethod的IMP即可
            class_replaceMethod(self, @selector(swi_eat), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }else{
            // 3.2、正常情况,直接交换
            method_exchangeImplementations(oriMethod, swiMethod);
        }
        
    });
}
  1. class_addMethod方法:取得新方法swiMethod的实现和方法类型签名,把新方法(swiMethod)实现放到旧方法(oriMethod)名中,此刻调用-eat就是调用-swi_eat
  2. didAddMethod
    2.1 添加成功,说明之前本类没有实现-eat方法,此时不必做方法交换,直接将swiMethod的IMP替换为父类(oriMethod)的IMP即可。 那么此时调用-swi_eat,实则是调用父类的-eat
    2.2 添加失败,说明本类之前已经实现,直接交换即可。

class_addMethod不会覆盖本类中已存在的实现,只会覆盖本类从父类继承的方法实现

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

为什么呢:

  1. 因为本类没有-eat的实现,所以执行class_addMethod成功,此时调用-eat就是调用-swi_eat
  2. 此时didAddMethodYES,添加方法成功,进行方法交换-class_replaceMethod。那么此时调用-swi_eat,实则是调用本类的-eat。最终造成死循环。

改变代码逻辑如下:

+ (void)load {
    //1、单例,避免多次交互
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        Method oriMethod = class_getInstanceMethod(self, @selector(eat));
        Method swiMethod = class_getInstanceMethod(self, @selector(swi_eat));
        
        // 2、当原方法未实现,添加方法,并设置IMP为空实现
        if (!oriMethod) {
            class_addMethod(self, @selector(eat), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
            method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
                NSLog(@"%@ 方法未实现", self);
            }));
        }
        
        // 3、取得新方法swiMethod的实现和方法类型签名
        //把新方法实现放到旧方法名中,调用-eat就是调用-swi_eat
        BOOL didAddMethod = class_addMethod(self, @selector(eat), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        if (didAddMethod) {
            // 3.1、添加成功,当前类未实现,父类实现了oriMethod
            // 此时不必做方法交换,直接将swiMethod的IMP替换为父类的IMP即可
            class_replaceMethod(self, @selector(swi_eat), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }else{
            // 3.2、正常情况,直接交换
            method_exchangeImplementations(oriMethod, swiMethod);
        }
        
    });
}
  1. 未实现方法时添加方法,此时调用-eat就是调用-swi_eat
  2. 如果经过正常的方法交换,-swi_eat方法内部还是调用自己-eat
  3. 所以未实现方法时,用block修改-swi_eat的实现,就可以断开死循环了。

四、总结

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

目前有两类常用的 Method Swizzling 实现方案,诸如 RSSwizzlejrswizzle 这种较为复杂且周全的一些实现方案可供参考。

block hook
关键在于理解,将Block_layout的invoke指针,强行指向_objc_msgForward指针,从而启动消息转发机制,在消息转发最后一步,将副本和hook block取出包装成NSInvocation进行调用。
可以参考:Block hook 正确姿势yulingtianxia/BlockHookBlock Hook+libffi
拓展:抖音技术团队Objective-C & Swift 最轻量级 Hook 方案

五、补充,类方法的hook

先上hook代码的简单实现

static void SwizzleMethod(Class cls, SEL ori, SEL rep) {
    Method oriMethod = class_getInstanceMethod(cls, ori);
    Method repMethod = class_getInstanceMethod(cls, rep);
    
    BOOL flag = class_addMethod(cls, ori, method_getImplementation(repMethod), method_getTypeEncoding(repMethod));
    
    if (flag) {
        class_replaceMethod(cls, rep, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    } else {
        method_exchangeImplementations(oriMethod, repMethod);
    }
}

@interface Foo : NSObject

+ (void)bar;

@end

@implementation Foo

+ (void)bar {
    NSLog(@"[Foo bar] called!!");
}

@end


@implementation Foo (Swizzle)

+ (void)swz_bar {
    NSLog(@"Before calling ----");
    [self swz_bar];
    NSLog(@"After calling ----");
}

@end

实例方法的hook实现:

SwizzleMethod([Foo class], @selector(bar), @selector(swz_bar));

类方法的hook实现,就是把 cls 参数变成这个类的 Meta-Class 即可:

SwizzleMethod(object_getClass([Foo class]), @selector(bar), @selector(swz_bar));

        [Foo bar];

参考:如何对类方法进行 Method Swizzling
class_getInstanceMethod、class_getClassMethod、isKindOfClass

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。