Runtime学习-Method Swizzling

Method Swizzling

Method-Swizzling实际就是更换方法所对应的实现函数(IMP),其主要作用是在运行时将一个方法的实现替换成另一个方法的实现,这就是我们常说的 iOS黑魔法。
方法交换.jpg

Method Swizzling标准用法示例


+(void)load{
    //dispatch_once一次行方法
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        //这里获取类的时候,需要注意
        //在类方法中,使用[self class]获取到当前类的信息。交换实例方法时,使用[self class]
        //在类方法中,使用获取到当前类的信息object_getClass(self),获取的是元类的信息。交换类方法时,使用object_getClass(self)

        //以下是交换实例方法的举例
        Class class = [self class];
        
        //获取到方法的SEL
        SEL originalSEL = @selector(swizzle_Orginal_Method);
        SEL swizzledSEL = @selector(swizzle_Swizzled_Method);

        //从类及其父类中查询方法method_t。(如果在类中不存在,就会遍历该类的父类)
        /*
         method_t *m = nil;
         while (cls  &&  ((m = getMethodNoSuper_nolock(cls, sel))) == nil) {
             cls = cls->getSuperclass();
         }
         return m;
         */
        Method originalMethod = class_getInstanceMethod(class, originalSEL);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);

        //使用 class_addMethod方法,先添加到类中。添加的时候SEL是原来的SEL,但是IMP是新的IMP
        //该方法,如果类中已经存在方法,就返回false;不存在该方法,把方法插入到方法列表,并返回true
        /*
         if ((m = getMethodNoSuper_nolock(cls, name))) {
             // already exists
            result = m->imp(false);
         } else {
             method_list_t *newlist;
             addMethods_finish(cls, newlist);
             result = nil;
         }
         */
        BOOL didAddMethod = class_addMethod(class,
                                            originalSEL,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            //如果方法添加成功,证明originalSEL和swizzledIMP添加到了方法列表
            //接下来只处理swizzledSEL和originalIMP即可
            //使用class_replaceMethod方法,用originalIMP 替换掉swizzledSEL的IMP
            //class_replaceMethod方法,存在I方法,就直接把新的IMP赋值给老的SEL
            
            /*
             if ((m = getMethodNoSuper_nolock(cls, name))) {
                 // already exists
                result = _method_setImplementation(cls, m, imp);
             } else {
                 // fixme optimize
                 method_list_t *newlist;
                 addMethods_finish(cls, newlist);
                 result = nil;
             }
             */
            class_replaceMethod(class,
                                swizzledSEL,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            
            //如果方法列表已经存在了该方法,及addMethod方法返回的是NO。直接交换方法
            /******************
             *m1->setImp(imp2);
             *m2->setImp(imp1);
             ******************/
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

Method Swizzling涉及的相关方法

1、类方法中的classobject_getClass

在方法交换的过程中,需要判断交换的方法类型,是实例方法还是类方法。方法交换的过程一般是在类的+(void)load;方法中进行。这样的话,怎样获取类是很重要的。
在类方法中调用+ (Class)class,其实在+ (Class)class方法内部是直接返回了self,也就是当前类。而如果在类方法中调用object_getClass,其实返回的是元类。所以在类方法中,使用这两个函数,是完全不同的。

+ (Class)class {
    return self;
}

示例代码:从打印结果和地址的输出,打印出来的类是一样但是两者地址是不同的。

- (void)instanceMethod{
    Class cls_Clazz = [self class];
    Class cls_Objc = object_getClass(self);
    NSLog(@"cls_Clazz is %@ -- cls_Objc is %@",cls_Clazz,cls_Objc);
}

//打印结果为
2022-08-02 17:36:09.385057+0800 schemeUse[6795:292600] cls_Clazz is MethodSend -- cls_Objc is MethodSend

//对cls_Clazz和cls_Objc 输出:
(lldb) p/x cls_Clazz
(Class) $0 = 0x0000000102ed5380 MethodSend
(lldb) p/x cls_Objc
(Class) $1 = 0x0000000102ed5358

所以,如果要交换的方法是类方法,在获取类的时候需要使用object_getClass(self); 要交换的方法是实例方法的话,获取类的方法是[self class]

2、获取方法Method_t

获取方法使用的是:Method class_getInstanceMethod(Class cls, SEL sel)
方法参数如下:

  • Class cls:要查询方法的所在的类
  • SEL sel:要查询方法的Selector,可认为是方法的名字

该方法会按照消息发送机制的慢查询机制(lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);),查一遍方法的实现(IMP)信息。然后从类的方法列表中查询方法实现(search_method_list_inline) 。如果在当前类中查询不到方法的实现,会遍历该类的父类,查询父类的方法列表(cls = cls->getSuperclass();)。
查询代码如下:

class_getInstanceMethod.png

3、为类cls添加方法class_addMethod

为类添加一个方法,使用到的函数是BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
方法参数如下:

  • Class cls:要添加方法的所在的类
  • SEL sel:要添加方法的Selector,可认为是方法的名字
  • IMP imp:要添加方法的实现,是个函数指针
  • const char *types:是一个常量指针,表示方法的签名信息。

该方法内部调用的是static IMP addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)

addMethod方法首先对类的方法列表进行遍历(不会遍历父类的方法列表),判断是否已经存在这个方法,如果存在的话,就取出方法的实现(IMP)返回;如果不存在这个方法,就会把该方法插入到方法列表,然后返回一个 nil

class_addMethod.png

4、替换掉原来的方法实现:class_replaceMethod

使用运行时的方法来替换掉原方法的IMP,使用class_replaceMethod
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)参数如下:

  • Class cls:要替换方法的所在的类
  • SEL sel:要替换方法的Selector,可认为是方法的名字
  • IMP imp:要替换方法的实现,是个函数指针
  • const char *types:是一个常量指针,表示方法的签名信息。

该方法会在方法列表中查询一下是否有方法的实现(不会遍历父类的方法列表)。如果没有方法的实现,就把方法插入到方法列表,返回的数据为nil;方法列表中已经存在方法,就把新的IMP替换掉原来的IMP,并返回原来的IMP。
该方法的实现如下:

class_replaceMethod.png

5、方法的交换:class_replaceMethod

在类中,把方法的IMP交换,可以使用void method_exchangeImplementations(Method m1_gen, Method m2_gen).
参数如下:

  • Method m1_gen:需要交换的一个方法
  • Method m2_gen:需要交换的另一个方法
    使用查询方法,获取到方法的结构(Method)后,可以调用这个方法,实现方法的交换。
    该方法可实现同类中,不同方法交换IMP;也可以实现不同类的方法交换。
    method_exchangeImplementations.png

同类中的方法交换:

+(void)load{
    //以下是交换实例方法的举例
    Class class = [self class];
    
    //获取到方法的SEL
    SEL originalSEL = @selector(swizzle_Orginal_Method);
    SEL swizzledSEL = @selector(swizzle_Swizzled_Method);
    //获取到方法
    Method originalMethod = class_getInstanceMethod(class, originalSEL);
    Method swizzled_Dog_Method = class_getInstanceMethod(class, swizzledSEL);
    //实现方法交换
    method_exchangeImplementations(originalMethod, swizzled_Dog_Method);
}

两个类中的方法交换:


+(void)load{
    //original Method方法所在的类
    Class class = [self class];
    
    //获取到方法的SEL
    SEL originalSEL = @selector(swizzle_Orginal_Method);
    SEL swizzled_Dog_SEL = @selector(swizzling_DogMethod);
    //获取到方法
    Method originalMethod = class_getInstanceMethod(class, originalSEL);
    Method swizzled_Dog_Method = class_getInstanceMethod([Dog class], swizzled_Dog_SEL);
    
    //实现方法交换
    method_exchangeImplementations(originalMethod, swizzled_Dog_Method);
    
}

Method Swizzling需要注意的问题

1、需要确保该流程只能执行一次

method-swizzling 多次调用会导致方法的重复交换,无法保证是否达到了最终的交换效果。


多次方法交换.jpg

如何解决这个问题呢?
+load 方法中执行,且使用 dispatch_once包裹;这样就可以保证方法交换只执行一次。
使用 dispatch_once包裹的原因在于,防止后续有手动的调用+load 方法的情况,从而导致再次交换了方法。

2、子类没有方法的实现,在父类的方法列表中查到

前面介绍获取Method的方法class_getInstanceMethod时,在其方法实现中,可以看出,在查找方法时,如果在类中没有查到。会继续在类的父类中进行查询。当我们要交换方法实现时,并没有交换当前类的方法,而是交换了当前类的父类的实现。
这样当其他地方调用这个父类的方法时,也会调用我们所替换的方法,这显然使我们不想要的。

/*======================================*/
/************--Man的类实现--*************/
/*=====================================*/
@interface Man : NSObject
- (void)stu_SuperMethod;
@end

@implementation Man
- (void)stu_SuperMethod{
    NSLog(@"我是 Student 的父类--Man");
}

/*======================================*/
/**********--Student的类实现--***********/
/*=====================================*/
@interface Student : Man
-(void)stu_instanceMethod;
@end

@implementation Student
-(void)stu_instanceMethod{
    NSLog(@"我是student类中的 stu_instanceMethod");
}

+(void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        //获取到方法的SEL
        SEL originalSEL = @selector(stu_instanceMethod);
        SEL swizzledSEL = @selector(stu_SuperMethod);
        Method originalMethod = class_getInstanceMethod(class, originalSEL);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);
        
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

//方法调用

- (instancetype)init{
    if (self = [super init]) {
        
        //父类的方法调用哦
        Man * m = [[Man alloc] init];
        [m stu_SuperMethod];
        
        //调用自己的房啊
        [self stu_instanceMethod];
        
        
        //父类的方法调用哦
        Man * m1 = [[Man alloc] init];
        [m1 stu_SuperMethod];
        
    }
    return self;
}
@end

打印结果如下:

2022-08-08 11:01:27.085961+0800 schemeUse[3098:115122] 我是student类中的 stu_instanceMethod
2022-08-08 11:01:27.086018+0800 schemeUse[3098:115122] 我是 Student 的父类--Man
2022-08-08 11:01:27.086063+0800 schemeUse[3098:115122] 我是student类中的 stu_instanceMethod

解决方案:
先通过 class_addMethod 尝试给自己添加要交换的方法,如果添加成功,即表明类中没有这个方法,则通过 class_replaceMethod 进行替换,如果添加失败则说明类中有这个方法,正常进行 method_exchangeImplementations。

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

推荐阅读更多精彩内容