探秘Method Swizzling

該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請(qǐng)注明:劉小壯


配圖

公司年底要在新年前發(fā)一個(gè)版本,最近一直很忙,好久沒(méi)有更新博客了。正好現(xiàn)在新版本開(kāi)發(fā)的差不多了,抽空總結(jié)一下。

由于最近開(kāi)發(fā)新版本,就避免不了在開(kāi)發(fā)和調(diào)試過(guò)程中引起崩潰,以及誘發(fā)一些之前的bug導(dǎo)致的崩潰。而且項(xiàng)目比較大也很不好排查,正好想起之前研究過(guò)的Method Swizzling,考慮是否能用這個(gè)蘋(píng)果的“黑魔法”解決問(wèn)題,當(dāng)然用好這個(gè)黑魔法并不局限于解決這些問(wèn)題。


需求

就拿我們公司項(xiàng)目來(lái)說(shuō)吧,我們公司是做導(dǎo)航的,而且項(xiàng)目規(guī)模比較大,各個(gè)控制器功能都已經(jīng)實(shí)現(xiàn)。突然有一天老大過(guò)來(lái),說(shuō)我們要在所有頁(yè)面添加統(tǒng)計(jì)功能,也就是用戶進(jìn)入這個(gè)頁(yè)面就統(tǒng)計(jì)一次。我們會(huì)想到下面的一些方法:

手動(dòng)添加

直接簡(jiǎn)單粗暴的在每個(gè)控制器中加入統(tǒng)計(jì),復(fù)制、粘貼、復(fù)制、粘貼。但這種方法并不太好,消耗時(shí)間而且以后非常難以維護(hù),會(huì)讓后面的開(kāi)發(fā)人員罵死的。

繼承

我們可以使用OOP的特性之一,繼承的方式來(lái)解決這個(gè)問(wèn)題。創(chuàng)建一個(gè)基類(lèi),在這個(gè)基類(lèi)中添加統(tǒng)計(jì)方法,其他類(lèi)都繼承自這個(gè)基類(lèi)。

然而,這種方式修改還是很大,而且定制性很差。以后有新人加入之后,都要囑咐其繼承自這個(gè)基類(lèi),所以這種方式并不可取。

Category

我們可以為UIViewController建一個(gè)Category,然后在所有控制器中引入這個(gè)Category。當(dāng)然我們也可以添加一個(gè)PCH文件,然后將這個(gè)Category添加到PCH文件中。

我們創(chuàng)建一個(gè)Category來(lái)覆蓋系統(tǒng)方法,系統(tǒng)會(huì)優(yōu)先調(diào)用Category中的代碼,然后在調(diào)用原類(lèi)中的代碼。

我們可以通過(guò)下面的這段偽代碼來(lái)看一下。

#import "UIViewController+EventGather.h"

@implementation UIViewController (EventGather)

- (void)viewDidLoad {
   
}
@end
Method Swizzling

我們可以使用蘋(píng)果的“黑魔法”Method Swizzling,Method Swizzling本質(zhì)上就是對(duì)IMPSEL進(jìn)行交換。

原理

Method Swizzing是發(fā)生在運(yùn)行時(shí)的,主要用于在運(yùn)行時(shí)將兩個(gè)Method進(jìn)行交換,我們可以將Method Swizzling代碼寫(xiě)到任何地方,但是只有在這段Method Swilzzling代碼執(zhí)行完畢之后互換才起作用。

而且Method Swizzling也是iOSAOP(面相切面編程)的一種實(shí)現(xiàn)方式,我們可以利用蘋(píng)果這一特性來(lái)實(shí)現(xiàn)AOP編程。

原理分析

首先,讓我們通過(guò)兩張圖片來(lái)了解一下Method Swizzling的實(shí)現(xiàn)原理

圖一
圖二

上面圖一中selector2原本對(duì)應(yīng)著IMP2,但是為了更方便的實(shí)現(xiàn)特定業(yè)務(wù)需求,我們?cè)趫D二中添加了selector3IMP3,并且讓selector2指向了IMP3,而selector3則指向了IMP2,這樣就實(shí)現(xiàn)了“方法互換”。

OC語(yǔ)言的runtime特性中,調(diào)用一個(gè)對(duì)象的方法就是給這個(gè)對(duì)象發(fā)送消息。是通過(guò)查找接收消息對(duì)象的方法列表,從方法列表中查找對(duì)應(yīng)的SEL,這個(gè)SEL對(duì)應(yīng)著一個(gè)IMP(一個(gè)IMP可以對(duì)應(yīng)多個(gè)SEL),通過(guò)這個(gè)IMP找到對(duì)應(yīng)的方法調(diào)用。

在每個(gè)類(lèi)中都有一個(gè)Dispatch Table,這個(gè)Dispatch Table本質(zhì)是將類(lèi)中的SELIMP(可以理解為函數(shù)指針)進(jìn)行對(duì)應(yīng)。而我們的Method Swizzling就是對(duì)這個(gè)table進(jìn)行了操作,讓SEL對(duì)應(yīng)另一個(gè)IMP。

使用

在實(shí)現(xiàn)Method Swizzling時(shí),核心代碼主要就是一個(gè)runtimeC語(yǔ)言API。

OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
代碼示例

就拿上面我們說(shuō)的頁(yè)面統(tǒng)計(jì)的需求來(lái)說(shuō)吧,這個(gè)需求在很多公司都很常見(jiàn),我們下面的Demo就通過(guò)Method Swizzling簡(jiǎn)單的實(shí)現(xiàn)這個(gè)需求。

我們先給UIViewController添加一個(gè)Category,然后在Category中的load方法中添加Method Swizzling方法,我們用來(lái)替換的方法也寫(xiě)在這個(gè)Category中。由于load類(lèi)方法是程序運(yùn)行時(shí)這個(gè)類(lèi)被加載到內(nèi)存中就調(diào)用的一個(gè)方法,執(zhí)行比較早,并且不需要我們手動(dòng)調(diào)用。而且這個(gè)方法具有唯一性,也就是只會(huì)被調(diào)用一次,不用擔(dān)心資源搶奪的問(wèn)題。

定義Method Swizzling中我們自定義的方法時(shí),需要注意盡量加前綴,以防止和其他地方命名沖突,Method Swizzling的替換方法命名一定要是唯一的,至少在被替換的類(lèi)中必須是唯一的。

+ (void)load {
    // 通過(guò)class_getInstanceMethod()函數(shù)從當(dāng)前對(duì)象中的method list獲取method結(jié)構(gòu)體,如果是類(lèi)方法就使用class_getClassMethod()函數(shù)獲取。
    Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
    Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad));
    
    if (!class_addMethod([self class], @selector(swizzlingViewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
        method_exchangeImplementations(fromMethod, toMethod);
    }
}

// 我們自己實(shí)現(xiàn)的方法,也就是和self的viewDidLoad方法進(jìn)行交換的方法。
- (void)swizzlingViewDidLoad {
    NSString *str = [NSString stringWithFormat:@"%@", self.class];
    // 我們?cè)谶@里加一個(gè)判斷,將系統(tǒng)的UIViewController的對(duì)象剔除掉
    if(![str containsString:@"UI"]){
        
    }
    [self swizzlingViewDidLoad];
}
@end

我們?cè)?code>load方法中使用class_addMethod函數(shù)對(duì)Method Swizzling做了一層驗(yàn)證,如果self沒(méi)有實(shí)現(xiàn)被交換的方法,會(huì)導(dǎo)致失敗。

而且,self沒(méi)有交換的方法實(shí)現(xiàn),但是父類(lèi)有這個(gè)方法,這樣就會(huì)調(diào)用父類(lèi)的方法,結(jié)果就不是我們想要的結(jié)果了。所以,我們?cè)谶@里通過(guò)class_addMethod的驗(yàn)證,如果self實(shí)現(xiàn)了這個(gè)方法,class_addMethod函數(shù)將會(huì)返回NO,我們就可以對(duì)其進(jìn)行交換了。

上面的代碼雖然在當(dāng)前方法中,又調(diào)用了當(dāng)前方法,但不會(huì)導(dǎo)致遞歸調(diào)用。Method Swizzling的實(shí)現(xiàn)原理可以理解為”方法互換“。假設(shè)我們將AB兩個(gè)方法進(jìn)行互換,向A方法發(fā)送消息時(shí)執(zhí)行的卻是B方法,向B方法發(fā)送消息時(shí)執(zhí)行的是A方法。

例如我們上面的代碼,系統(tǒng)調(diào)用UIViewControllerviewDidLoad方法時(shí),實(shí)際上執(zhí)行的是我們實(shí)現(xiàn)的swizzlingViewDidLoad方法。而我們?cè)?code>swizzlingViewDidLoad方法內(nèi)部調(diào)用[self swizzlingViewDidLoad];時(shí),執(zhí)行的是UIViewControllerviewDidLoad方法。

Method Swizzling類(lèi)簇

之前我也說(shuō)到,在我們項(xiàng)目開(kāi)發(fā)過(guò)程中,經(jīng)常因?yàn)?code>NSArray數(shù)組越界或者NSDictionarykey或者value值為nil等問(wèn)題導(dǎo)致的崩潰,對(duì)于這些問(wèn)題蘋(píng)果并不會(huì)報(bào)一個(gè)警告,而是直接崩潰,感覺(jué)蘋(píng)果這樣確實(shí)有點(diǎn)太狠了。

由此,我們可以根據(jù)上面所學(xué),對(duì)NSArrayNSMutableArray、NSDictionary、NSMutableDictionary等類(lèi)進(jìn)行Method Swizzling,實(shí)現(xiàn)方式還是按照上面的例子來(lái)做。但是,你發(fā)現(xiàn)Method Swizzling根本就不起作用。

這是因?yàn)?code>Method Swizzling對(duì)NSArray這些的類(lèi)簇是不起作用的。因?yàn)檫@些類(lèi)簇類(lèi),其實(shí)是一種抽象工廠的設(shè)計(jì)模式。抽象工廠內(nèi)部有很多其它繼承自當(dāng)前類(lèi)的子類(lèi),抽象工廠類(lèi)會(huì)根據(jù)不同情況,創(chuàng)建不同的抽象對(duì)象來(lái)進(jìn)行使用。例如我們調(diào)用NSArrayobjectAtIndex:方法,這個(gè)類(lèi)會(huì)在方法內(nèi)部判斷,內(nèi)部創(chuàng)建不同抽象類(lèi)進(jìn)行操作。

所以也就是我們對(duì)NSArray類(lèi)進(jìn)行操作其實(shí)只是對(duì)父類(lèi)進(jìn)行了操作,在NSArray內(nèi)部會(huì)創(chuàng)建其他子類(lèi)來(lái)執(zhí)行操作,真正執(zhí)行操作的并不是NSArray自身,所以我們應(yīng)該對(duì)其“真身”進(jìn)行操作。

代碼示例

下面我們實(shí)現(xiàn)了防止NSArray因?yàn)檎{(diào)用objectAtIndex:方法,取下標(biāo)時(shí)數(shù)組越界導(dǎo)致的崩潰:

+ (void)load {
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:));
    method_exchangeImplementations(fromMethod, toMethod);
}

- (id)lxz_objectAtIndex:(NSUInteger)index {
    if (self.count-1 < index) {
        // 這里做一下異常處理,不然都不知道出錯(cuò)了。
        @try {
            return [self lxz_objectAtIndex:index];
        }
        @catch (NSException *exception) {
            // 在崩潰后會(huì)打印崩潰信息,方便我們調(diào)試。
            NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
    }
        @finally {}
    } else {
        return [self lxz_objectAtIndex:index];
    }
}

根據(jù)上面代碼可以發(fā)現(xiàn),__NSArrayI才是NSArray真正的類(lèi),而NSMutableArray又不一樣。我們可以通過(guò)runtime函數(shù)獲取真正的類(lèi)。

objc_getClass("__NSArrayI");
舉例

下面我們列舉一些常用的類(lèi)簇。

類(lèi) 類(lèi)名
NSArray __NSArrayI
NSMutableArray __NSArrayM
NSDictionary __NSDictionaryI
NSMutableDictionary __NSDictionaryM

其他請(qǐng)大家自行Google。

JRSwizzle

在項(xiàng)目中我們肯定會(huì)在很多地方用到Method Swizzling,而且在使用這個(gè)特性時(shí)有很多需要注意的地方。我們可以將Method Swizzling封裝起來(lái),也可以使用一些比較成熟的第三方。

在這里我推薦Github上星最多的一個(gè)第三方-jrswizzle。里面核心就兩個(gè)類(lèi),代碼看起來(lái)非常清爽。

#import <Foundation/Foundation.h>
@interface NSObject (JRSwizzle)
+ (BOOL)jr_swizzleMethod:(SEL)origSel_ withMethod:(SEL)altSel_ error:(NSError**)error_;
+ (BOOL)jr_swizzleClassMethod:(SEL)origSel_ withClassMethod:(SEL)altSel_ error:(NSError**)error_;
@end

// MethodSwizzle類(lèi)
#import <objc/objc.h>
BOOL ClassMethodSwizzle(Class klass, SEL origSel, SEL altSel);
BOOL MethodSwizzle(Class klass, SEL origSel, SEL altSel);

Method Swizzling 錯(cuò)誤剖析

在上面的例子中,如果只是單獨(dú)對(duì)NSArrayNSMutableArray中的單個(gè)類(lèi)進(jìn)行Method Swizzling,是可以正常使用并且不會(huì)發(fā)生異常的。如果進(jìn)行Method Swizzling的類(lèi)中,有兩個(gè)類(lèi)有繼承關(guān)系的,并且Swizzling了同一個(gè)方法。例如同時(shí)對(duì)NSArrayNSMutableArray中的objectAtIndex:方法都進(jìn)行了Swizzling,這樣可能會(huì)導(dǎo)致父類(lèi)Swizzling失效的問(wèn)題。

對(duì)于這種問(wèn)題主要是兩個(gè)原因?qū)е碌?,首先是不要?code>load方法中調(diào)用[super load]方法,這會(huì)導(dǎo)致父類(lèi)的Swizzling被重復(fù)執(zhí)行兩次,這樣父類(lèi)的Swizzling就會(huì)失效。例如下面的兩張圖片,你會(huì)發(fā)現(xiàn)由于NSMutableArray調(diào)用了[super load]導(dǎo)致父類(lèi)NSArraySwizzling代碼被執(zhí)行了兩次。

錯(cuò)誤代碼
+ (void)load {
    // 這里不應(yīng)該調(diào)用super,會(huì)導(dǎo)致父類(lèi)被重復(fù)Swizzling
    [super load];
    
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(lxz_objectAtIndexM:));
    method_exchangeImplementations(fromMethod, toMethod);
}

這里由于在子類(lèi)中調(diào)用了super,導(dǎo)致NSMutableArray執(zhí)行時(shí),父類(lèi)NSArray也被執(zhí)行了一次。

第一次

父類(lèi)NSArray執(zhí)行了第二次Swizzling,這時(shí)候就會(huì)出現(xiàn)問(wèn)題,后面會(huì)講具體原因。

第二次

這樣就會(huì)導(dǎo)致程序運(yùn)行過(guò)程中,子類(lèi)調(diào)用Swizzling的方法是沒(méi)有問(wèn)題的,父類(lèi)調(diào)用同一個(gè)方法就會(huì)發(fā)現(xiàn)Swizzling失效了,具體原因我們后面講。

還有一個(gè)原因就是因?yàn)榇a邏輯導(dǎo)致Swizzling代碼被執(zhí)行了多次,這也會(huì)導(dǎo)致Swizzling失效,其實(shí)原理和上面的問(wèn)題是一樣的,我們下面講講為什么會(huì)出現(xiàn)這個(gè)問(wèn)題。

問(wèn)題原因

我們上面提到過(guò)Method Swizzling的實(shí)現(xiàn)原理就是對(duì)類(lèi)的Dispatch Table進(jìn)行操作,每進(jìn)行一次Swizzling就交換一次SELIMP(可以理解為函數(shù)指針),如果Swizzling被執(zhí)行了多次,就相當(dāng)于SELIMP被交換了多次。這就會(huì)導(dǎo)致第一次執(zhí)行成功交換了、第二次執(zhí)行又換回去了、第三次執(zhí)行.....這樣換來(lái)?yè)Q去的結(jié)果,能不能成功就看運(yùn)氣了??,這也是好多人說(shuō)Method Swizzling不好用的原因之一。

交換過(guò)程
Dispatch Table 交換流程

從這張圖中我們也可以看出問(wèn)題產(chǎn)生的原因了,就是Swizzling的代碼被重復(fù)執(zhí)行,為了避免這樣的原因出現(xiàn),我們可以通過(guò)GCDdispatch_once函數(shù)來(lái)解決,利用dispatch_once函數(shù)內(nèi)代碼只會(huì)執(zhí)行一次的特性。

在每個(gè)Method Swizzling的地方,加上dispatch_once函數(shù)保證代碼只被執(zhí)行一次。當(dāng)然在實(shí)際使用中也可以對(duì)下面代碼進(jìn)行封裝,這里只是給一個(gè)示例代碼。

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
        Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(lxz_objectAtIndexM:));
        method_exchangeImplementations(fromMethod, toMethod);
    });
}

源碼分析

下面是Method Swizzling的實(shí)現(xiàn)源碼,從源碼來(lái)看,其實(shí)內(nèi)部實(shí)現(xiàn)很簡(jiǎn)單。核心代碼就是交換兩個(gè)Methodimp函數(shù)指針,這也就是方法被swizzling多次,可能會(huì)被換回去的原因,因?yàn)槊看握{(diào)用都會(huì)執(zhí)行一次交換操作。

void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;

    rwlock_writer_t lock(runtimeLock);

    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;

    flushCaches(nil);

    updateCustomRR_AWZ(nil, m1);
    updateCustomRR_AWZ(nil, m2);
}

Method Swizzling危險(xiǎn)嗎

既然Method Swizzling可以對(duì)這個(gè)類(lèi)的Dispatch Table進(jìn)行操作,操作后的結(jié)果對(duì)所有當(dāng)前類(lèi)及子類(lèi)都會(huì)產(chǎn)生影響,所以有人認(rèn)為Method Swizzling是一種危險(xiǎn)的技術(shù),用不好很容易導(dǎo)致一些不可預(yù)見(jiàn)的bug,這些bug一般都是非常難發(fā)現(xiàn)和調(diào)試的。

這個(gè)問(wèn)題可以引用念茜大神的一句話:“使用Method Swizzling編程就好比切菜時(shí)使用鋒利的刀,一些人因?yàn)閾?dān)心切到自己所以害怕鋒利的刀具,可是事實(shí)上,使用鈍刀往往更容易出事,而利刀更為安全?!?/p>


在這個(gè)Demo中通過(guò)Method Swizzling,簡(jiǎn)單實(shí)現(xiàn)了一個(gè)崩潰攔截功能。實(shí)現(xiàn)方式就是將原方法Swizzling為自己定義的方法,在執(zhí)行時(shí)先在自己方法中做判斷,根據(jù)是否異常再做下一步處理。

Demo只是來(lái)輔助讀者更好的理解文章中的內(nèi)容,應(yīng)該博客結(jié)合Demo一起學(xué)習(xí),只看Demo還是不能理解更深層的原理。Demo中代碼都會(huì)有注釋?zhuān)魑豢梢源驍帱c(diǎn)跟著Demo執(zhí)行流程走一遍,看看各個(gè)階段變量的值。

Demo地址:劉小壯的Github


簡(jiǎn)書(shū)由于排版的問(wèn)題,閱讀體驗(yàn)并不好,布局、圖片顯示、代碼等很多問(wèn)題。所以建議到我Github上,下載Runtime PDF合集。把所有Runtime文章總計(jì)九篇,都寫(xiě)在這個(gè)PDF中,而且左側(cè)有目錄,方便閱讀。

Runtime PDF

下載地址:Runtime PDF
麻煩各位大佬點(diǎn)個(gè)贊,謝謝!??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 剛開(kāi)始學(xué)習(xí)IOS的時(shí)候,聽(tīng)說(shuō)黑魔法很強(qiáng)大,正如它的名字一樣,可以做很多不可思議的事情,一直到今天才徹底靜下心去了解...
    東了個(gè)尼閱讀 1,832評(píng)論 0 3
  • 需求: 本身項(xiàng)目已經(jīng)很大,要新增頁(yè)面訪問(wèn)統(tǒng)計(jì)功能。 Method Swizzling原理:Method Swizz...
    塞北孤雁閱讀 894評(píng)論 0 0
  • 前言: 今天我們?cè)賮?lái)了解另外一個(gè)體現(xiàn)OC動(dòng)態(tài)特性的技術(shù),向來(lái)有IOS黑魔法之稱的Method Swizzling,...
    cxlhaha閱讀 786評(píng)論 0 3
  • 開(kāi)始文章之前先來(lái)拋出一個(gè)問(wèn)題,假如有一個(gè)已經(jīng)成形的項(xiàng)目,希望在進(jìn)入每個(gè)控制器的時(shí)候添加一個(gè)統(tǒng)計(jì),這個(gè)怎么實(shí)現(xiàn)呢?項(xiàng)...
    iCuiCui閱讀 923評(píng)論 0 0
  • Method Swizzling 是什么 Method Swizzling是objective-c中的黑魔法,算是...
    進(jìn)擊的阿牛哥閱讀 1,973評(píng)論 0 6

友情鏈接更多精彩內(nèi)容