美文网首页IOS开发知识点基础
ios 安全的Method Swizzing

ios 安全的Method Swizzing

作者: 正_文 | 来源:发表于2020-05-13 17:21 被阅读0次

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 方案

相关文章

  • ios 安全的Method Swizzing

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

  • runtime的消息交换

    Method Swizzing Method Swizzing有什么用处呢? 我们可以创建一个子类,但是需要创建U...

  • iOS runtime之Method Swizzing

    1. Method Swizzling(动态方法交换)简介 Method Swizzling 用于改变一个已经存在...

  • iOS runtime2

    Method Swizzing(方法交换) 常用的API class_getInstanceMethod:获取实例...

  • iOS 方法调配(method swizzling)

    Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method...

  • runtime之Method Swizzing

    首先: 非常感谢大神的文章以及网上各种各样的学习资料,让我学习到了很多的知识,并且稍微运用了一下.我的Demo动态...

  • Method Swizzling

    Method Swizzing是发生在Runtime运行时的,主要用于在运行时将两个Method进行交换,我们可以...

  • iOS 里 Method Swizzing 的一个坑

    今天项目上的一个功能频繁出现数组越界导致闪退的情况,这个功能上许多地方用了NSArray的objectAtInde...

  • swizzing message

    method swizzing(hook 的一种) 因为runtime才开始进行方法实现确定的关系,我们可以在代码...

  • Container类型的crash和NSString类型的cra

    首先参考一下自己之前写的《method swizzing》这篇,特别是对类簇的methodSwizzing。 Co...

网友评论

    本文标题:ios 安全的Method Swizzing

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