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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352