iOS开发- runtime方法交换的坑

class_replaceMethod与method_exchangeImplementations区别

方法交换在开发中还是挺常见的,比如hook调viewDidLoad方法,想在每个viewDidLoad里面打印出当前类名,可以写个jm_ viewDidLoad方法,在用runtime交换俩方法的实现(也叫IMP)。


viewDidLoad方法交换示意图->网上找的

看不少开源库都用到方法交换,基本有俩种实现方式:
第一种实现:

void swizzleInstanceMethod(Class cls, SEL origSelector, SEL newSelector){
  Method originalMethod = class_getInstanceMethod(cls, origSelector);
  Method swizzledMethod = class_getInstanceMethod(cls, newSelector);

  IMP previousIMP = class_replaceMethod(cls, origSelector, method_getImplementation(swizzledMethod),
                                                 method_getTypeEncoding(swizzledMethod));
  class_replaceMethod(cls, newSelector, previousIMP,method_getTypeEncoding(originalMethod));
}

第二种实现:

void swizzleInstanceMethod(Class cls, SEL origSelector, SEL newSelector){
  Method originalMethod = class_getInstanceMethod(cls, origSelector);
  Method swizzledMethod = class_getInstanceMethod(cls, newSelector);
  method_exchangeImplementations(originalMethod, swizzledMethod);
}

在某些情况下,这俩种确实都能达到交换IMP的效果,但是其中又有一丝区别。

看看class_replaceMethod的文档解释:

方法:IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char * types)

  • Replaces the implementation of a method for a given class.
  • @param cls The class you want to modify.
  • @param name A selector that identifies the method whose implementation you want to replace.
  • @param imp The new implementation for the method identified by name for the class identified by cls.
  • @param types An array of characters that describe the types of the arguments to the method.
  • Since the function must take at least two arguments-self and _cmd, the second and third characters
  • must be “@:” (the first character is the return type).
  • @return The previous implementation of the method identified by name for the class identified by \e cls.
  • @note This function behaves in two different ways:
  • If the method identified by name does not yet exist, it is added as if class_addMethod were called.
  • If the method identified by name does exist, its IMP is replaced as if method_setImplementation were called.
    重点是最后note的描述:当name描述的方法不存在的时候,将调用class_addMethod方法;当这个方法存在,将调用method_setImplementation方法。

再看看method_exchangeImplementations的文档解释:

方法:void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)

  • @param m1 Method to exchange with second method.
  • @param m2 Method to exchange with first method.
  • @note This is an atomic version of the following:
  • IMP imp1 = method_getImplementation(m1);
  • IMP imp2 = method_getImplementation(m2);
  • method_setImplementation(m1, imp2);
  • method_setImplementation(m2, imp1);
    可以看出这个方法完全是个二愣子,直接交换IMP,啥都不管。
俩中方法都用到了class_getInstanceMethod(cls, selector)方法,这个方法有个特点:如果这个类中没有实现selector这个方法,它返回的是它某父类的 Method 对象(沿着继承链找到为止)。

重点来了:
如果这个类没实现这个方法,但是它父类实现了,直接拿这方法来交换,真没问题么??
先说结果:第二种实现有问题,第一种实现没问题。

看代码上代码:

@interface NSObject(test)
@end

@implementation NSObject(test)

- (void)oneSwizzleInstanceMethod:(SEL)origSelector withMethod:(SEL)newSelector
{    Class cls = [self class];
    Method originalMethod = class_getInstanceMethod(cls, origSelector);
    Method swizzledMethod = class_getInstanceMethod(cls, newSelector);
    
    IMP previousIMP = class_replaceMethod(cls,
                                          origSelector,
                                          method_getImplementation(swizzledMethod),
                                          method_getTypeEncoding(swizzledMethod));
    
    class_replaceMethod(cls,
                        newSelector,
                        previousIMP,
                        method_getTypeEncoding(originalMethod));
}
- (void)twoSwizzleInstanceMethod:(SEL)origSelector withMethod:(SEL)newSelector
{
    Class cls = [self class];
    Method originalMethod = class_getInstanceMethod(cls, origSelector);
    Method swizzledMethod = class_getInstanceMethod(cls, newSelector);
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

@end
@interface Base : NSObject
@end

@implementation Base
- (void)print:(NSString*)msg
{
    NSLog(@"print-->obj %@ print say:%@", NSStringFromClass(self.class), msg);
}
- (void)hookPrint:(NSString*)msg {
    NSLog(@"hookPrin-->obj %@ print say:%@", NSStringFromClass(self.class), msg);
}
@end

//A只实现了print方法,没有实现hookPrint方法。
@interface A : Base
@end
@implementation A
- (void)print:(NSString*)msg {
    NSLog(@"A obj print say:%@", msg);
}
@end

//B啥都没实现。
@interface B : Base
@end
@implementation B
@end

现在我们测试第一种实现:分别交换A、B的print和hookPrint的方法实现。

[A oneSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)];
[B oneSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)];
//测试代码是这样的:
 A* a = [A new]; [a print:@"hello1"];
 B* b = [B new]; [b print:@"hello2"];
//结果:
2019-07-02 14:55:19.294580+0800 xctest[3533:667117] hookPrin-->obj A print say:hello1
2019-07-02 14:55:19.294871+0800 xctest[3533:667117] hookPrin-->obj B print say:hello2

说明A、B确实交换了方法实现。

现在我们测试第二种实现:分别交换A、B的print和hookPrint的方法实现。

 [A twoSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)];
 [B twoSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)];
//测试代码是这样的:
 A* a = [A new]; [a print:@"hello1"];
 B* b = [B new]; [b print:@"hello2"];
//结果:
2019-07-02 14:58:48.692403+0800 xctest[3585:681651] hookPrin-->obj A print say:hello1
2019-07-02 14:58:48.692697+0800 xctest[3585:681651] A obj print say:hello2

嗯?有没不对劲?B的交换方法失败了。不是期望中hookPrin-->obj B print say:hello2。
why?分析下:
第一种方法,用class_replaceMethod()实现的。由于A中不存在hookPrint方法,class_replaceMethod会调用class_addMethod方法,而class_addMethod会把Base的hookPrint实现添加到当前类,print的实现最终会和hookPrint的实现交换。B中俩方法都不存在,也会添加俩方法,最终交换俩方法的实现。最终打印的时候,会调用Base的hookPrint方法。
如果调用[a hookPrint:@"hello_k"];那么最终实现的应该是A类中print的实现。结果是:A obj print say:hello_k。
第二种方法,用method_exchangeImplementations实现。由于A没有实现hookPrint方法,在调用
[A twoSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)]的时候,将A的print实现与Base的hookPrint实现交换了。
接着调用 [B twoSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)]的时候,由于B类啥都没实现,它只能将Base的print实现与base的hookPrint交换了。最终,调用[b print:@"hello2"]的时候,调用的是代码中A类的print方法的实现。

巨丑的示意图

这里加不加class_addMethod的判断,结果都一样。可以自己试试。

测试代码地址:github链接
注意:整篇这是没考虑交换方法不存在的情况下考量的。

结论:当我们写的类没有继承的关系的时候,俩种方法都没什么问题。当有继承关系又不确定方法实现没实现,最好用class_replaceMethod方法。当啥都不确定的时候,老老实实地用class_replaceMethod 吧,安全无痛苦。

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