MethodSwizzling

MethodSwizzling的實(shí)現(xiàn)

首先oc中的方法,是一個(gè) objc_method 結(jié)構(gòu)體,由三部分組成

typedef struct objc_method *Method;
struct objc_method {
    SEL _Nonnull method_name        // 方法sel                         
    char * _Nullable method_types     // 方法參數(shù)
    IMP _Nonnull method_imp            // 方法實(shí)現(xiàn)
}  

IMP 指向了方法的實(shí)現(xiàn)

我們常使用objc/runtime.h中提供的api:method_exchangeImplementations(Method m1, Method m2)來(lái)交換IMP,重新綁定sel和 IMP對(duì)應(yīng)關(guān)系

Method oriMethod = class_getInstanceMethod(self, @selector(method1));
Method swiMethod = class_getInstanceMethod(self, @selector(method2));
method_exchangeImplementations(oriMethod, swiMethod);

class_getInstanceMethod函數(shù),在當(dāng)前類(lèi)沒(méi)有實(shí)現(xiàn)方法時(shí),會(huì)返回父類(lèi)的方法

method_exchangeImplementations函數(shù)僅僅交換了兩個(gè) Method 結(jié)構(gòu)體的 imp,上面的代碼,當(dāng)子類(lèi)沒(méi)有實(shí)現(xiàn)oriMethod時(shí)會(huì)將父類(lèi)和子類(lèi)方法的實(shí)現(xiàn)互換.

因此可以使用下面的方式

    Method oriMethod = class_getInstanceMethod(self, @selector(method1));
    Method swiMethod = class_getInstanceMethod(self, @selector(method2));
    method_exchangeImplementations(oriMethod, swiMethod);
    
    // 添加方法
    if(class_addMethod(self, @selector(method1), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod))){
        // 如果添加成功了,當(dāng)前類(lèi)沒(méi)有該方法,需要把swiMethod的實(shí)現(xiàn)替換成oriMethod的.
        class_replaceMethod(self, @selector(method2), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }
    else{
        // 如果添加失敗了,當(dāng)前類(lèi)有這個(gè)方法
        method_exchangeImplementations(oriMethod, swiMethod);
    }

或者使用下面這種方式

//  確保當(dāng)前類(lèi),兩個(gè)方法都實(shí)現(xiàn)了,再去交換
    Method oriMethod = class_getInstanceMethod(self, @selector(method1));
       
    Method swiMethod = class_getInstanceMethod(self, @selector(method2));
       
    class_addMethod(self,
                    @selector(method1),
                       class_getMethodImplementation(self, @selector(method1)),
                       method_getTypeEncoding(oriMethod));

    class_addMethod(self,
                    @selector(method2),
                       class_getMethodImplementation(self, @selector(method2)),
                       method_getTypeEncoding(swiMethod));
   
    method_exchangeImplementations(class_getInstanceMethod(self, @selector(method1)), class_getInstanceMethod(self, @selector(method1)));

隱患

  • 只在 +load 中執(zhí)行 swizzling 才是安全的败徊。

  • 被 hook 的方法必須是當(dāng)前類(lèi)自身的方法趾徽,如果把繼承來(lái)的 IMP copy 到自身上面會(huì)存在問(wèn)題。父類(lèi)的方法應(yīng)該在調(diào)用的時(shí)候使用奶段,而不是 swizzling 的時(shí)候 copy 到子類(lèi)库糠。

  • 被 Swizzled 的方法如果依賴(lài) cmd 伙狐,hook 之后 cmd 發(fā)送了變化,就會(huì)有問(wèn)題(一般你 hook 的是系統(tǒng)類(lèi),也不知道系統(tǒng)用沒(méi)用 cmd 這個(gè)參數(shù))鳞骤。

  • 命名如果沖突導(dǎo)致之前 hook 的失效 或者是循環(huán)調(diào)用窒百。

第一條和第四條說(shuō)的是通常 MethodSwizzling 是在分類(lèi)里面實(shí)現(xiàn)的, 而分類(lèi)的 Method 是被Runtime 加載的時(shí)候追加到類(lèi)的 MethodList ,如果不在 +load 中執(zhí)行, Swizzling 一旦出現(xiàn)重名豫尽,那么 SEL 和 IMP 不匹配致 hook 的結(jié)果是循環(huán)調(diào)用篙梢。

第三條是一個(gè)不容易被發(fā)現(xiàn)的問(wèn)題。
我們都知道 Objective-C Method 都會(huì)有兩個(gè)隱含的參數(shù) self,cmd美旧,有的時(shí)候開(kāi)發(fā)者在使用關(guān)聯(lián)屬性的適合可能懶得聲明 (void *) 的 key渤滞,直接使用 cmd 變量 objc_setAssociatedObject(self, _cmd, xx, 0); 這會(huì)導(dǎo)致對(duì)當(dāng)前IMP對(duì) cmd 的依賴(lài)。

一旦此方法被 Swizzling榴嗅,那么方法的 cmd 勢(shì)必會(huì)發(fā)生變化妄呕,出現(xiàn)了 bug 之后想必一定找不到,

Copy父類(lèi)的方法帶來(lái)的隱患

上面的兩種 Swizzling 方式,存在copy父類(lèi)方法帶來(lái)的隱患

當(dāng)父類(lèi)中存在一個(gè) 方法.

子類(lèi)的分類(lèi) hook 父類(lèi)方法時(shí),會(huì)把父類(lèi)實(shí)現(xiàn) copy到子類(lèi).

這時(shí)父類(lèi)的分類(lèi)再去 hook 父類(lèi)的 方法,會(huì)導(dǎo)致子類(lèi) Hook 不到父類(lèi)的分類(lèi)的實(shí)現(xiàn)

這時(shí)可以使用RSSwizzle

RSSwizzleInstanceMethod([Student class],
                            @selector(sayHello),
                            RSSWReturnType(void),
                            RSSWArguments(),
                            RSSWReplacement(
                                            {
                                                // 調(diào)用之前的實(shí)現(xiàn)
                                                RSSWCallOriginal();
                                                // 自身的實(shí)現(xiàn)
                                                NSLog(@"Student + swizzle say hello sencod time");
                                            }), 0, NULL);

    RSSwizzleInstanceMethod([Person class],
                            @selector(sayHello),
                            RSSWReturnType(void),
                            RSSWArguments(),
                            RSSWReplacement(
                                            {
                                                // 調(diào)用之前的實(shí)現(xiàn)
                                                RSSWCallOriginal();
                                                // 自身的實(shí)現(xiàn)
                                                NSLog(@"Person + swizzle say hello");
                                            }), 0, NULL);

將宏展開(kāi)

// 創(chuàng)建block
RSSwizzleImpFactoryBlock newImp = ^id(RSSwizzleInfo *swizzleInfo) {
        void (*originalImplementation_)(__unsafe_unretained id,SEL));
        SEL selector_ = @selector(sayHello);
        return ^void (__unsafe_unretained id self) {
            //  調(diào)用父類(lèi)的實(shí)現(xiàn)
            ((__typeof(originalImplementation_))[swizzleInfo getOriginalImplementation])(self, selector_);
            //只有這一行是我們的核心邏輯
            NSLog(@"Student + swizzle say hello");
        };
    };
    [RSSwizzle swizzleInstanceMethod:@selector(sayHello)
                             inClass:[[Student class] class]
                       newImpFactory:newImp
                                mode:0 key:((void*)0)];;

其中RSSwizzleInfo的聲明和 swizzleInstanceMethod:inClass:newImpFactory:newImp:mode:key:方法的實(shí)現(xiàn)

/*
其中
typedef id (^RSSwizzleImpFactoryBlock)(RSSwizzleInfo *swizzleInfo);
*/

@interface RSSwizzleInfo : NSObject
/**
返回swizzled方法的原始實(shí)現(xiàn)嗽测。
如果swizzled類(lèi)實(shí)現(xiàn)了方法本身绪励,那么它實(shí)際上是一個(gè)原始實(shí)現(xiàn);如果沒(méi)有實(shí)現(xiàn)方法,則是從父類(lèi)獲取的實(shí)現(xiàn)唠粥。
@注意:調(diào)用時(shí)疏魏,必須始終將返回的實(shí)現(xiàn)強(qiáng)制轉(zhuǎn)換為相應(yīng)的函數(shù)指針。
@返回指向swizzled方法的原始實(shí)現(xiàn)的函數(shù)指針晤愧。
 */
-(RSSwizzleOriginalIMP)getOriginalImplementation;

/// The selector of the swizzled method.
@property (nonatomic, readonly) SEL selector;
@end
*/

typedef NS_ENUM(NSUInteger, RSSwizzleMode) {
    ///RSSwizzle總是做swizzing大莫。
    RSSwizzleModeAlways = 0,
    /// 如果以前用同一個(gè)鍵 RSSwizzle 過(guò)同一個(gè)類(lèi),就不會(huì)RSSwizzle官份。
    RSSwizzleModeOncePerClass = 1,
    /// 如果同一個(gè)類(lèi)或父類(lèi)之前已經(jīng)用同一個(gè)鍵進(jìn)行了swizzle只厘,就不會(huì)RSSwizzle。
 
    RSSwizzleModeOncePerClassAndSuperclasses = 2
};
+(BOOL)swizzleInstanceMethod:(SEL)selector
                     inClass:(Class)classToSwizzle
               newImpFactory:(RSSwizzleImpFactoryBlock)factoryBlock
                        mode:(RSSwizzleMode)mode
                         key:(const void *)key
{
        if (key){
            // 獲得key對(duì)應(yīng)的set
            NSSet *swizzledClasses = swizzledClassesForKey(key);
            if (mode == RSSwizzleModeOncePerClass) {
                // 是否用同一個(gè)key  RSSwizzle 過(guò)同一個(gè)類(lèi)
                if ([swizzledClasses containsObject:classToSwizzle]){
                    return NO;
                }
            }else if (mode == RSSwizzleModeOncePerClassAndSuperclasses){
                for (Class currentClass = classToSwizzle;
                     nil != currentClass;
                     currentClass = class_getSuperclass(currentClass))
                {
                    if ([swizzledClasses containsObject:currentClass]) {
                        return NO;
                    }
                }
            }
        }
        
        swizzle(classToSwizzle, selector, factoryBlock);
        
        if (key){
            //  保存key
            [swizzledClassesForKey(key) addObject:classToSwizzle];
        }
    }
    
    return YES;
}

RSSwizzle核心代碼其實(shí)只有一個(gè)函數(shù),

// 刪除無(wú)關(guān)的加鎖舅巷,防御邏輯羔味,簡(jiǎn)化理解。
static void swizzle(Class classToSwizzle,
                    SEL selector,
                    RSSwizzleImpFactoryBlock factoryBlock)
{
    Method method = class_getInstanceMethod(classToSwizzle, selector);
    
    __block IMP originalIMP = NULL;

    RSSWizzleImpProvider originalImpProvider = ^IMP{
        // 保存 class_replaceMethod 獲得之前的實(shí)現(xiàn),如果之前沒(méi)有實(shí)現(xiàn),
        IMP imp = originalIMP;
        // 如果imp不存在,則獲取父類(lèi)的方法的imp.
        if (NULL == imp){
            imp = method_getImplementation(class_getInstanceMethod(superclass,selector));
        }
        return imp;
    };
    
    RSSwizzleInfo *swizzleInfo = [RSSwizzleInfo new];
    swizzleInfo.selector = selector;
    swizzleInfo.impProviderBlock = originalImpProvider;
    
    // 執(zhí)行傳進(jìn)來(lái)的block, 獲得 替換后的實(shí)現(xiàn)
    id newIMPBlock = factoryBlock(swizzleInfo);
    // 獲得返回值類(lèi)型
    const char *methodType = method_getTypeEncoding(method);
      
    // 創(chuàng)建實(shí)現(xiàn)  
    IMP newIMP = imp_implementationWithBlock(newIMPBlock);
    
    // 替換方法,并獲得之前的實(shí)現(xiàn)
    originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType);
}

通過(guò)閱讀源碼可知, RSSwizzle 并沒(méi)有去交換方法的imp,而是,保存舊的imp, 然后新的方法替換了舊的方法.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末钠右,一起剝皮案震驚了整個(gè)濱河市赋元,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌爬舰,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寒瓦,死亡現(xiàn)場(chǎng)離奇詭異情屹,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)杂腰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)垃你,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事惜颇〗源蹋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵凌摄,是天一觀的道長(zhǎng)羡蛾。 經(jīng)常有香客問(wèn)我,道長(zhǎng)锨亏,這世上最難降的妖魔是什么痴怨? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮器予,結(jié)果婚禮上浪藻,老公的妹妹穿的比我還像新娘。我一直安慰自己乾翔,他們只是感情好爱葵,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著反浓,像睡著了一般萌丈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上勾习,一...
    開(kāi)封第一講書(shū)人閱讀 49,185評(píng)論 1 284
  • 那天浓瞪,我揣著相機(jī)與錄音,去河邊找鬼巧婶。 笑死乾颁,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的艺栈。 我是一名探鬼主播英岭,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼湿右!你這毒婦竟也來(lái)了诅妹?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤毅人,失蹤者是張志新(化名)和其女友劉穎吭狡,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體丈莺,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡划煮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了缔俄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弛秋。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡器躏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蟹略,到底是詐尸還是另有隱情登失,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布挖炬,位于F島的核電站揽浙,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏茅茂。R本人自食惡果不足惜捏萍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望空闲。 院中可真熱鬧令杈,春花似錦、人聲如沸碴倾。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)跌榔。三九已至异雁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間僧须,已是汗流浹背纲刀。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留担平,地道東北人示绊。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像暂论,于是被迫代替她去往敵國(guó)和親面褐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

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