Runtime學(xué)習(xí)-Method Swizzling

Method Swizzling

Method-Swizzling實(shí)際就是更換方法所對(duì)應(yīng)的實(shí)現(xiàn)函數(shù)(IMP),其主要作用是在運(yùn)行時(shí)將一個(gè)方法的實(shí)現(xiàn)替換成另一個(gè)方法的實(shí)現(xiàn)钮蛛,這就是我們常說(shuō)的 iOS黑魔法剖膳。
方法交換.jpg

Method Swizzling標(biāo)準(zhǔn)用法示例


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

        //這里獲取類的時(shí)候,需要注意
        //在類方法中吱晒,使用[self class]獲取到當(dāng)前類的信息。交換實(shí)例方法時(shí)叹话,使用[self class]
        //在類方法中墩瞳,使用獲取到當(dāng)前類的信息object_getClass(self),獲取的是元類的信息喉酌。交換類方法時(shí),使用object_getClass(self)

        //以下是交換實(shí)例方法的舉例
        Class class = [self class];
        
        //獲取到方法的SEL
        SEL originalSEL = @selector(swizzle_Orginal_Method);
        SEL swizzledSEL = @selector(swizzle_Swizzled_Method);

        //從類及其父類中查詢方法method_t般妙。(如果在類中不存在歪架,就會(huì)遍歷該類的父類)
        /*
         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方法,先添加到類中和蚪。添加的時(shí)候SEL是原來(lái)的SEL烹棉,但是IMP是新的IMP
        //該方法怯疤,如果類中已經(jīng)存在方法,就返回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添加到了方法列表
            //接下來(lái)只處理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 {
            
            //如果方法列表已經(jīng)存在了該方法,及addMethod方法返回的是NO惭笑。直接交換方法
            /******************
             *m1->setImp(imp2);
             *m2->setImp(imp1);
             ******************/
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

Method Swizzling涉及的相關(guān)方法

1、類方法中的classobject_getClass

在方法交換的過(guò)程中铺敌,需要判斷交換的方法類型屁擅,是實(shí)例方法還是類方法。方法交換的過(guò)程一般是在類的+(void)load;方法中進(jìn)行派歌。這樣的話,怎樣獲取類是很重要的胶果。
在類方法中調(diào)用+ (Class)class,其實(shí)在+ (Class)class方法內(nèi)部是直接返回了self霎烙,也就是當(dāng)前類。而如果在類方法中調(diào)用object_getClass悬垃,其實(shí)返回的是元類。所以在類方法中烘豌,使用這兩個(gè)函數(shù),是完全不同的廊佩。

+ (Class)class {
    return self;
}

示例代碼:從打印結(jié)果和地址的輸出靖榕,打印出來(lái)的類是一樣但是兩者地址是不同的。

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

//打印結(jié)果為
2022-08-02 17:36:09.385057+0800 schemeUse[6795:292600] cls_Clazz is MethodSend -- cls_Objc is MethodSend

//對(duì)cls_Clazz和cls_Objc 輸出:
(lldb) p/x cls_Clazz
(Class) $0 = 0x0000000102ed5380 MethodSend
(lldb) p/x cls_Objc
(Class) $1 = 0x0000000102ed5358

所以鸯绿,如果要交換的方法是類方法,在獲取類的時(shí)候需要使用object_getClass(self); 要交換的方法是實(shí)例方法的話,獲取類的方法是[self class]

2毒返、獲取方法Method_t

獲取方法使用的是:Method class_getInstanceMethod(Class cls, SEL sel)租幕。
方法參數(shù)如下:

  • Class cls:要查詢方法的所在的類
  • SEL sel:要查詢方法的Selector拧簸,可認(rèn)為是方法的名字

該方法會(huì)按照消息發(fā)送機(jī)制的慢查詢機(jī)制(lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);),查一遍方法的實(shí)現(xiàn)(IMP)信息贾富。然后從類的方法列表中查詢方法實(shí)現(xiàn)(search_method_list_inline) 牺六。如果在當(dāng)前類中查詢不到方法的實(shí)現(xiàn),會(huì)遍歷該類的父類淑际,查詢父類的方法列表(cls = cls->getSuperclass();)。
查詢代碼如下:

class_getInstanceMethod.png

3盗胀、為類cls添加方法class_addMethod

為類添加一個(gè)方法锄贼,使用到的函數(shù)是BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
方法參數(shù)如下:

  • Class cls:要添加方法的所在的類
  • SEL sel:要添加方法的Selector,可認(rèn)為是方法的名字
  • IMP imp:要添加方法的實(shí)現(xiàn)浸策,是個(gè)函數(shù)指針
  • const char *types:是一個(gè)常量指針屈糊,表示方法的簽名信息。

該方法內(nèi)部調(diào)用的是static IMP addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)逻锐。

addMethod方法首先對(duì)類的方法列表進(jìn)行遍歷(不會(huì)遍歷父類的方法列表),判斷是否已經(jīng)存在這個(gè)方法昧诱,如果存在的話,就取出方法的實(shí)現(xiàn)(IMP)返回凶掰;如果不存在這個(gè)方法蜈亩,就會(huì)把該方法插入到方法列表,然后返回一個(gè) nil稚配。

class_addMethod.png

4、替換掉原來(lái)的方法實(shí)現(xiàn):class_replaceMethod

使用運(yùn)行時(shí)的方法來(lái)替換掉原方法的IMP午衰,使用class_replaceMethod冒萄。
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)參數(shù)如下:

  • Class cls:要替換方法的所在的類
  • SEL sel:要替換方法的Selector,可認(rèn)為是方法的名字
  • IMP imp:要替換方法的實(shí)現(xiàn)尊流,是個(gè)函數(shù)指針
  • const char *types:是一個(gè)常量指針,表示方法的簽名信息蜘澜。

該方法會(huì)在方法列表中查詢一下是否有方法的實(shí)現(xiàn)(不會(huì)遍歷父類的方法列表)响疚。如果沒(méi)有方法的實(shí)現(xiàn),就把方法插入到方法列表忿晕,返回的數(shù)據(jù)為nil;方法列表中已經(jīng)存在方法,就把新的IMP替換掉原來(lái)的IMP宾巍,并返回原來(lái)的IMP渔伯。
該方法的實(shí)現(xiàn)如下:

class_replaceMethod.png

5、方法的交換:class_replaceMethod

在類中锣吼,把方法的IMP交換,可以使用void method_exchangeImplementations(Method m1_gen, Method m2_gen).
參數(shù)如下:

  • Method m1_gen:需要交換的一個(gè)方法
  • Method m2_gen:需要交換的另一個(gè)方法
    使用查詢方法古徒,獲取到方法的結(jié)構(gòu)(Method)后读恃,可以調(diào)用這個(gè)方法,實(shí)現(xiàn)方法的交換寺惫。
    該方法可實(shí)現(xiàn)同類中,不同方法交換IMP西雀;也可以實(shí)現(xiàn)不同類的方法交換。
    method_exchangeImplementations.png

同類中的方法交換:

+(void)load{
    //以下是交換實(shí)例方法的舉例
    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);
    //實(shí)現(xiàn)方法交換
    method_exchangeImplementations(originalMethod, swizzled_Dog_Method);
}

兩個(gè)類中的方法交換:


+(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);
    
    //實(shí)現(xiàn)方法交換
    method_exchangeImplementations(originalMethod, swizzled_Dog_Method);
    
}

Method Swizzling需要注意的問(wèn)題

1判莉、需要確保該流程只能執(zhí)行一次

method-swizzling 多次調(diào)用會(huì)導(dǎo)致方法的重復(fù)交換豆挽,無(wú)法保證是否達(dá)到了最終的交換效果。


多次方法交換.jpg

如何解決這個(gè)問(wèn)題呢帮哈?
+load 方法中執(zhí)行锰镀,且使用 dispatch_once包裹;這樣就可以保證方法交換只執(zhí)行一次泳炉。
使用 dispatch_once包裹的原因在于,防止后續(xù)有手動(dòng)的調(diào)用+load 方法的情況氧腰,從而導(dǎo)致再次交換了方法。

2古拴、子類沒(méi)有方法的實(shí)現(xiàn),在父類的方法列表中查到

前面介紹獲取Method的方法class_getInstanceMethod時(shí)黄痪,在其方法實(shí)現(xiàn)中,可以看出是嗜,在查找方法時(shí)油额,如果在類中沒(méi)有查到叠纷。會(huì)繼續(xù)在類的父類中進(jìn)行查詢潦嘶。當(dāng)我們要交換方法實(shí)現(xiàn)時(shí),并沒(méi)有交換當(dāng)前類的方法航厚,而是交換了當(dāng)前類的父類的實(shí)現(xiàn)锰蓬。
這樣當(dāng)其他地方調(diào)用這個(gè)父類的方法時(shí)幔睬,也會(huì)調(diào)用我們所替換的方法芹扭,這顯然使我們不想要的。

/*======================================*/
/************--Man的類實(shí)現(xiàn)--*************/
/*=====================================*/
@interface Man : NSObject
- (void)stu_SuperMethod;
@end

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

/*======================================*/
/**********--Student的類實(shí)現(xiàn)--***********/
/*=====================================*/
@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);
    });
}

//方法調(diào)用

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

打印結(jié)果如下:

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

解決方案:
先通過(guò) class_addMethod 嘗試給自己添加要交換的方法辅肾,如果添加成功轮锥,即表明類中沒(méi)有這個(gè)方法,則通過(guò) class_replaceMethod 進(jìn)行替換新娜,如果添加失敗則說(shuō)明類中有這個(gè)方法,正常進(jìn)行 method_exchangeImplementations概龄。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末饲握,一起剝皮案震驚了整個(gè)濱河市吸重,隨后出現(xiàn)的幾起案子歪今,更是在濱河造成了極大的恐慌,老刑警劉巖寄猩,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異替废,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)椎镣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)兽赁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人刀崖,你說(shuō)我怎么就攤上這事」萁兀” “怎么了蜂莉?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)映穗。 經(jīng)常有香客問(wèn)我,道長(zhǎng)男公,這世上最難降的妖魔是什么合陵? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮踏拜,結(jié)果婚禮上低剔,老公的妹妹穿的比我還像新娘肮塞。我一直安慰自己,他們只是感情好枕赵,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布位隶。 她就那樣靜靜地躺著,像睡著了一般涧黄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上笋妥,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天春宣,我揣著相機(jī)與錄音酵颁,去河邊找鬼信认。 笑死,一個(gè)胖子當(dāng)著我的面吹牛嫁赏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播款熬,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼攘乒,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了则酝?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤般卑,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蝠检,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體挚瘟,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡饲梭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年焰檩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锅尘。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖浪腐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情议街,我是刑警寧澤璧榄,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站骨杂,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏搓蚪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一悴能、第九天 我趴在偏房一處隱蔽的房頂上張望雳灾。 院中可真熱鬧漠酿,春花似錦谎亩、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)寸痢。三九已至紊选,卻和暖如春啼止,著一層夾襖步出監(jiān)牢的瞬間兵罢,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工卖词, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人即横。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓裆赵,卻偏偏與公主長(zhǎng)得像东囚,于是被迫代替她去往敵國(guó)和親战授。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容