iOS 札記1:Method Swizzling小記

導(dǎo)語(yǔ):Method Swizzling是Objective-C中運(yùn)行時(shí)中討論較多的內(nèi)容奖慌,本文主要介紹使用Method Swizzling遇到的問(wèn)題項(xiàng)目中使用的Swizzling方案

一、Method Swizzling簡(jiǎn)介

Method Swizzling的本質(zhì)是在運(yùn)行時(shí)交換方法實(shí)現(xiàn)(IMP),如hook系統(tǒng)方法斩狱,在原有的方法中愧怜,插入自己的業(yè)務(wù)需求。

1蝇率、Method Swizzling原理
  • Objective-C的消息機(jī)制:在 Objective-C 中調(diào)用一個(gè)方法迟杂, 實(shí)際上是在底層通過(guò) objc_msgSend()發(fā)送一個(gè)消息。 而查找消息的唯一依據(jù)是selector的方法名本慕。

    //調(diào)用方法  
    [obj doSomething];
    
    //[obj doSomething]本質(zhì)上是給obj發(fā)doSomething消息
    objc_msgSend(obj,@selector(doSomething))
    
  • 每一個(gè)OC實(shí)例對(duì)象都保存有isa指針實(shí)例變量排拷,其中isa指針?biāo)鶎兕悾惥S護(hù)一個(gè)運(yùn)行時(shí)可接收的方法列表(MethodLists)锅尘;方法列表(MethodLists)中保存selector的方法名和方法實(shí)現(xiàn)(IMP监氢,指向Method實(shí)現(xiàn)的指針)的映射關(guān)系。在運(yùn)行時(shí)藤违,通過(guò)selecter找到匹配的IMP浪腐,從而找到的具體的實(shí)現(xiàn)函數(shù)。

MethodLists示意圖.png
  • 開發(fā)中可以利用Objective-C的動(dòng)態(tài)特性顿乒,在運(yùn)行時(shí)替換selector對(duì)應(yīng)的方法實(shí)現(xiàn)(IMP)议街,達(dá)到給hook的目的。下圖是利用Method Swizzling來(lái)替換selector對(duì)應(yīng)IMP后的方法列表示意圖淆游。
hook后的MethodLists示意圖.png
2傍睹、Method Swizzling使用

Method Swizzling的本質(zhì)就是偷換selector的IMP,下面就Swizzle NSObject的description方法犹菱,簡(jiǎn)單舉例:

#import "NSObject+Swizzle.h"
#import <objc/runtime.h>

@implementation NSObject (Swizzle)

+ (void)load{
   //調(diào)換IMP
    Method originalMethod = class_getInstanceMethod([NSObject class], @selector(description));
    Method myMethod = class_getInstanceMethod([NSObject class], @selector(qs_description));
    method_exchangeImplementations(originalMethod, myMethod);
}

- (void)qs_description{
    NSLog(@"description 被 Swizzle 了");
    return [self qs_description];    
}
@end

說(shuō)明:調(diào)用被hook的description方法拾稳,獲取內(nèi)容前,會(huì)打印“description 被 Swizzle 了”這樣的日志腊脱。

3访得、Method Swizzling存在的問(wèn)題
  • 不是線程安全的(Method swizzling is not atomic)
  • 改變了代碼本來(lái)的行為(Changes behavior of un-owned code)
  • 潛在的命名沖突(Possible naming conflicts)
  • 改變方法的參數(shù)(Swizzling changes the method's arguments)
  • 繼承問(wèn)題(The order of swizzles matters)
  • 難以理解 (Difficult to understand)
  • 難以調(diào)試(Difficult to debug)

二、RSSwizzle:Method Swizzling的優(yōu)雅方案

RSSwizzle線程安全的Method Swizzling方案陕凹,能夠幫我們解決Method Swizzling的使用問(wèn)題悍抑。介紹如下:

1、不是線程安全的(Method swizzling is not atomic)
  • 通常在 load方法中交換方法實(shí)現(xiàn)杜耙,如果在其他時(shí)機(jī)交換方法實(shí)現(xiàn)搜骡,需要考慮線程安全的問(wèn)題。

  • RSSwizzle利用了自旋鎖OSSpinLock保證線程安全佑女〖敲遥可以在任意時(shí)機(jī)交換方法實(shí)現(xiàn)。

2团驱、 改變了代碼本來(lái)的行為(Changes behavior of un-owned code)
  • 這正是Swizzle的目標(biāo)摸吠。但是在Swizzle方法中,我們保留*調(diào)用原始實(shí)現(xiàn)的好習(xí)慣嚎花,能避免絕大多數(shù)問(wèn)題寸痢。我們利用Swizzle,一般是為了在原始實(shí)現(xiàn)基礎(chǔ)上紊选,添加某些自己的業(yè)務(wù)需求啼止,并不想刻意去破壞原有實(shí)現(xiàn)道逗。

  • RSSwizzle提供調(diào)用原來(lái)實(shí)現(xiàn)的宏RSSWCallOriginal,很方便献烦。

3憔辫、潛在的命名沖突(Possible naming conflicts)#####
  • 通常在替換的方法名前加前綴,可以很大程度上避免命名沖突沖突問(wèn)題仿荆。

  • RSSwizzle在自定義的swizzle的靜態(tài)方法完成方法替換,完全避免了命名沖突問(wèn)題坏平。

4拢操、改變方法的參數(shù)(Swizzling changes the method's arguments)
  • 參數(shù) _cmd 被篡改,正常調(diào)用Swizzle 的方法有問(wèn)題舶替。

    //調(diào)用方法 
    [self qs_setFrame:frame];  
    //發(fā)消息
    objc_msgSend(self, @selector(qs_setFrame:), frame);  
    

    說(shuō)明:在運(yùn)行時(shí)令境,尋找qs_setFrame:的方法實(shí)現(xiàn), _cmd參數(shù)雖然是 qs_setFrame: ,但是實(shí)際上找到的方法實(shí)現(xiàn)是原始的 setFrame: 實(shí)現(xiàn)顾瞪。

  • RSSwizzle的自定義的swizzle的靜態(tài)方法解決這個(gè)問(wèn)題舔庶。

5、繼承問(wèn)題(The order of swizzles matters)
  • 多個(gè)有繼承關(guān)系的類的對(duì)象Swizzle時(shí)陈醒,先從父對(duì)象開始惕橙。 這樣才能保證子類方法拿到父類中的被Swizzle的實(shí)現(xiàn)。

  • 在load中Swizzle不用擔(dān)心這種問(wèn)題钉跷,因?yàn)閘oad類方法會(huì)默認(rèn)從父類開始調(diào)用弥鹦。

6、難以理解 (Difficult to understand)
  • 主要表現(xiàn)在調(diào)用原始實(shí)現(xiàn)爷辙,看起來(lái)像遞歸彬坏,有點(diǎn)懵。

  • RSSwizzle提供的宏RSSWCallOriginal讓調(diào)用原始實(shí)現(xiàn)更容易膝晾,代碼閱讀性更強(qiáng)栓始。

7、難以調(diào)試(Difficult to debug)
  • Debug時(shí)候打印出的backtrace(回溯)血当,其中摻雜著被swizzle的方法名幻赚,看起來(lái)比較亂,所以命名清晰很重要歹颓;

  • RSSwizzle打印出來(lái)的命名很清晰坯屿,此外Swizzle了什么,最好有文檔記錄巍扛。

三领跛、RSSwizzle的基礎(chǔ)使用

RSSwizzle中提供了兩種使用方式,一種是通過(guò)調(diào)用類方法來(lái)實(shí)現(xiàn)函數(shù)的替換撤奸,另一種是使用RSSwizzle定義的來(lái)進(jìn)行函數(shù)的替換吠昭。

1喊括、 使用類方法替換實(shí)例方法實(shí)現(xiàn)
/**
 參數(shù)1:要被替換的函數(shù)選擇器
 參數(shù)2:要被替換的函數(shù)所在的類
 參數(shù)3: block中返回替換后的方法,block參數(shù)中需要返回一個(gè)方法函數(shù),這個(gè)函數(shù)為要替換成的函數(shù)矢棚,要和原函數(shù)類型相同郑什。在類中的函數(shù)默認(rèn)都會(huì)有一個(gè)名為self的id參數(shù)
 參數(shù)4:此次替換用到的key
 */
[RSSwizzle swizzleInstanceMethod:@selector(touchesBegan:withEvent:) inClass:[ViewController class] newImpFactory:^id(RSSwizzleInfo *swizzleInfo) {
    return ^(__unsafe_unretained id self,NSSet* touches,UIEvent* event){
        NSLog(@"touchesBegan:withEvent:被Swizzle了");
    };
} mode:RSSwizzleModeAlways key:NULL];
2、 使用宏替換實(shí)例方法實(shí)現(xiàn)
 /*
 參數(shù)1:要被替換的函數(shù)所在的類
 參數(shù)2: 要被替換的函數(shù)選擇器
 參數(shù)3:返回值類型蒲肋,
 參數(shù)4:參數(shù)列表
 參數(shù)5:要替換的代碼塊蘑拯,
 參數(shù)6:執(zhí)行模式,
 參數(shù)7:key值標(biāo)識(shí),RSSwizzleModeOncePerClass模式下使用兜粘,其他情況置為NULL
 */
RSSwizzleInstanceMethod([ViewController class], @selector(touchesEnded:withEvent:), RSSWReturnType(void), RSSWArguments(NSSet<UITouch *> *touches,UIEvent *event),RSSWReplacement({
    
    NSLog(@"touchesEnded:withEvent被Swizzle了");
    RSSWCallOriginal(touches,event);
}), RSSwizzleModeAlways, NULL);    
3申窘、 使用類方法替換類方法實(shí)現(xiàn)
/*
 參數(shù)1:要替換的函數(shù)選擇器
 參數(shù)2:要替換此函數(shù)的類
 參數(shù)3:block中返回替換后的方法,block參數(shù)中需要返回一個(gè)方法函數(shù),這個(gè)函數(shù)為要替換成的函數(shù)孔轴,要和原函數(shù)類型相同剃法。在類中的函數(shù)默認(rèn)都會(huì)有一個(gè)名為self的id參數(shù)
 */
[RSSwizzle swizzleClassMethod:@selector(testClassMethod1) inClass:[ViewController class] newImpFactory:^id(RSSwizzleInfo *swizzleInfo) {
    
    return ^(__unsafe_unretained id self){
        NSLog(@"Class testClassMethod1 Swizzle");
    };
}];
4、使用宏替換類方法實(shí)現(xiàn)
/*
 參數(shù)1:要替換方法的類
 參數(shù)2:要替換的方法選擇器
 參數(shù)3:方法的返回值類型
 參數(shù)4:方法的參數(shù)列表
 參數(shù)5:要替換的方法代碼塊
 */
RSSwizzleClassMethod(NSClassFromString(@"ViewController"), NSSelectorFromString(@"testClassMethod2"), RSSWReturnType(void), RSSWArguments(), RSSWReplacement({
    //先執(zhí)行原始方法
    RSSWCallOriginal();
    NSLog(@"Class testClassMethod2 Swizzle");
}));

說(shuō)明:RSSwizzle還提供了Swizzle模式路鹰,使用Swizzle實(shí)例方法時(shí)候需要用到贷洲。Swizzle類方法,默認(rèn)RSSwizzleModeAlways晋柱,定義如下:

typedef NS_ENUM(NSUInteger, RSSwizzleMode) {
    //任何情況下 始終執(zhí)行替換操作
    RSSwizzleModeAlways = 0,
    //相同key標(biāo)識(shí)的替換操作只會(huì)被執(zhí)行一次
    RSSwizzleModeOncePerClass = 1,
    //相同key標(biāo)識(shí)的替換操作在子類父類中只會(huì)被執(zhí)行一次
    RSSwizzleModeOncePerClassAndSuperclasses = 2
};

四优构、一個(gè)使用Swizzling典型的錯(cuò)誤案例

網(wǎng)絡(luò)上很多博客介紹了使用Swizzling來(lái)防止重復(fù)點(diǎn)擊UIButton,但是大部分都會(huì)有問(wèn)題雁竞。

1俩块、錯(cuò)誤代碼

一般在load中替換sendAction:to:forEvent:方法,主要代碼如下:

+ (void)load {
    Method before   = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    Method after    = class_getInstanceMethod(self, @selector(qs_sendAction:to:forEvent:));
    method_exchangeImplementations(before, after);
}

- (void)qs_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    if ([NSDate date].timeIntervalSince1970 - self.qs_acceptEventTime < self.qs_acceptEventInterval) {
        return;
    }

    if (self.qs_acceptEventInterval > 0) {
        self.qs_acceptEventTime = [NSDate date].timeIntervalSince1970;
    } 
    [self qs_sendAction:action to:target forEvent:event];
 }

錯(cuò)誤現(xiàn)象

點(diǎn)擊UITabBar上按鈕會(huì)crash, 提示類似于:[UITabBarButton qs_acceptEventTime]: unrecognized selector sent to instance ...浓领。

錯(cuò)誤原因

1)UITabBarButton是UITabBarController中各個(gè)子控制器在工具條中對(duì)應(yīng)的按鈕玉凯,是UITabBar的私有屬性,UITabBarButton的父類是UIControl联贩,而UIButton的父類也是UIControl漫仆,sendAction:to:forEvent:是UIControl的實(shí)例方法;

2) 在UIButton類中沒有sendAction:to:forEvent:這個(gè)方法實(shí)現(xiàn)泪幌,通過(guò)class_getInstanceMethod() 獲取的是父類的 Method 對(duì)象盲厌,使用 method_exchangeImplementations() 就把父類的原始實(shí)現(xiàn)(IMP)跟自己的 Swizzle 實(shí)現(xiàn)交換了。這就導(dǎo)致UIControl的其他子類祸泪,如UITabBarButton在被點(diǎn)擊后吗浩,都調(diào)用了UIButton的Swizzle 實(shí)現(xiàn),發(fā)生了嚴(yán)重的Crash問(wèn)題没隘。

說(shuō)明:雖然在UIControl的分類的load方法交換方法實(shí)現(xiàn)懂扼,能解決問(wèn)題,我們將Swizzling的影響擴(kuò)大很多倍,不是理想的做法阀湿。下面介紹解決辦法赶熟。

2、解決辦法

在項(xiàng)目直接使用method_exchangeImplementations很危險(xiǎn)陷嘴,甚至導(dǎo)致Crash映砖,在項(xiàng)目中不建議這么做≡职ぃ可采用的解決辦法有兩種:

方法A

原理:如果類中沒有實(shí)現(xiàn) Original Selector 對(duì)應(yīng)的方法邑退,那就通過(guò)class_addMethod方法為Original Selector增加Swizzle 的實(shí)現(xiàn),通過(guò)class_replaceMethod修改Swizzle Selector 的 實(shí)現(xiàn) 為 Original 的實(shí)現(xiàn)劳澄;如果已經(jīng)有Original Selector 對(duì)應(yīng)的方法(通過(guò)class_addMethod方法添加是失敗的), 這時(shí)才使用method_exchangeImplementations來(lái)直接交換瓜饥。

代碼如下

+ (void)load {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(sendAction:to:forEvent:);
        SEL swizzledSelector = @selector(qs_sendAction:to:forEvent:);
 
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

說(shuō)明1:class_addMethod方法可以為類添加新的方法實(shí)現(xiàn)(IMP),添加成功返回YES.浴骂。否則返回NO。如果選擇器(select)已經(jīng)有對(duì)應(yīng)的方法實(shí)現(xiàn)(IMP), 添加也是失敗的宪潮,利用這點(diǎn)可以檢查是否有源方法實(shí)現(xiàn)溯警,如果沒有利用class_replaceMethod來(lái)將swizzledSelector和originalMethod對(duì)應(yīng)設(shè)置好。

說(shuō)明2:.class_replaceMethod用來(lái)替換類中的方法實(shí)現(xiàn)狡相,會(huì)調(diào)用class_addMethod和method_setImplementation方法(直接設(shè)置某個(gè)方法的IMP)

方法B

原理:RSSwizzle完美避開了在load中使用method_exchangeImplementations交換方法的尷尬梯轻,基于Swizzle模式和class_replaceMethod完美控制了替換方法實(shí)現(xiàn)。

代碼如下

+ (void)load{
    RSSwizzleInstanceMethod([UIButton class], @selector(sendAction:to:forEvent:), RSSWReturnType(void), RSSWArguments(SEL action,id target,UIEvent *event), RSSWReplacement({
           UIButton *btn = self;
            if ([NSDate date].timeIntervalSince1970 - btn.qs_acceptEventTime < btn.qs_acceptEventInterval) {
                return;
            }      
            if (btn.qs_acceptEventInterval > 0) {
                btn.qs_acceptEventTime = [NSDate date].timeIntervalSince1970;
            }        
            RSSWCallOriginal(action,target,event);    
    }), RSSwizzleModeAlways, NULL);
}

說(shuō)明:RSSwizzleInstanceMethod宏實(shí)現(xiàn)方法實(shí)現(xiàn)的替換尽棕,代碼更易閱讀喳挑。

End

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末曹宴,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子歉提,更是在濱河造成了極大的恐慌笛坦,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件苔巨,死亡現(xiàn)場(chǎng)離奇詭異版扩,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)侄泽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門礁芦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人悼尾,你說(shuō)我怎么就攤上這事宴偿∠嫔樱” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵窄刘,是天一觀的道長(zhǎng)窥妇。 經(jīng)常有香客問(wèn)我,道長(zhǎng)娩践,這世上最難降的妖魔是什么活翩? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮翻伺,結(jié)果婚禮上材泄,老公的妹妹穿的比我還像新娘。我一直安慰自己吨岭,他們只是感情好拉宗,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著辣辫,像睡著了一般旦事。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上急灭,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天姐浮,我揣著相機(jī)與錄音,去河邊找鬼葬馋。 笑死卖鲤,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的畴嘶。 我是一名探鬼主播蛋逾,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼窗悯!你這毒婦竟也來(lái)了换怖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蟀瞧,失蹤者是張志新(化名)和其女友劉穎沉颂,沒想到半個(gè)月后鸟整,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體助币,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年岖妄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了切端。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片彻坛。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出昌屉,到底是詐尸還是另有隱情钙蒙,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布间驮,位于F島的核電站躬厌,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏竞帽。R本人自食惡果不足惜扛施,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望屹篓。 院中可真熱鬧疙渣,春花似錦、人聲如沸堆巧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)谍肤。三九已至啦租,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谣沸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工笋颤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留乳附,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓伴澄,卻偏偏與公主長(zhǎng)得像赋除,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子非凌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉举农,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,682評(píng)論 0 9
  • 前言 到了今天終于要"出院"了,要總結(jié)一下住院幾天的收獲敞嗡,談?wù)凴untime到底能為我們開發(fā)帶來(lái)些什么好處颁糟。當(dāng)然它...
    一縷殤流化隱半邊冰霜閱讀 23,356評(píng)論 56 317
  • 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,544評(píng)論 33 466
  • 繼上Runtime梳理(四) 通過(guò)前面的學(xué)習(xí),我們了解到Objective-C的動(dòng)態(tài)特性:Objective-C不...
    小名一峰閱讀 741評(píng)論 0 3
  • “爸爸,天為什么是藍(lán)色的箕肃?” “因?yàn)楹J撬{(lán)色的啊~”海邊一個(gè)小女孩和爸爸一問(wèn)一答道婚脱。 “為什么天的顏色要和海的顏色...
    不見君J閱讀 884評(píng)論 0 0