探秘Method Swizzling

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


配圖

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

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


需求

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

手動添加

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

繼承

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

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

Category

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

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

我們可以通過下面的這段偽代碼來看一下。

#import "UIViewController+EventGather.h"

@implementation UIViewController (EventGather)

- (void)viewDidLoad {
   
}
@end
Method Swizzling

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

原理

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

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

原理分析

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

圖一
圖二

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

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

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

使用

在實現(xiàn)Method Swizzling時,核心代碼主要就是一個runtimeC語言API

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

就拿上面我們說的頁面統(tǒng)計的需求來說吧,這個需求在很多公司都很常見,我們下面的Demo就通過Method Swizzling簡單的實現(xiàn)這個需求。

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

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

+ (void)load {
    // 通過class_getInstanceMethod()函數(shù)從當(dāng)前對象中的method list獲取method結(jié)構(gòu)體,如果是類方法就使用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);
    }
}

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

我們在load方法中使用class_addMethod函數(shù)對Method Swizzling做了一層驗證,如果self沒有實現(xiàn)被交換的方法,會導(dǎo)致失敗。

而且,self沒有交換的方法實現(xiàn),但是父類有這個方法,這樣就會調(diào)用父類的方法,結(jié)果就不是我們想要的結(jié)果了。所以,我們在這里通過class_addMethod的驗證,如果self實現(xiàn)了這個方法,class_addMethod函數(shù)將會返回NO,我們就可以對其進行交換了。

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

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

Method Swizzling類簇

之前我也說到,在我們項目開發(fā)過程中,經(jīng)常因為NSArray數(shù)組越界或者NSDictionarykey或者value值為nil等問題導(dǎo)致的崩潰,對于這些問題蘋果并不會報一個警告,而是直接崩潰,感覺蘋果這樣確實有點太狠了。

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

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

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

代碼示例

下面我們實現(xiàn)了防止NSArray因為調(diào)用objectAtIndex:方法,取下標(biāo)時數(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) {
        // 這里做一下異常處理,不然都不知道出錯了。
        @try {
            return [self lxz_objectAtIndex:index];
        }
        @catch (NSException *exception) {
            // 在崩潰后會打印崩潰信息,方便我們調(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真正的類,而NSMutableArray又不一樣。我們可以通過runtime函數(shù)獲取真正的類。

objc_getClass("__NSArrayI");
舉例

下面我們列舉一些常用的類簇。

類名
NSArray __NSArrayI
NSMutableArray __NSArrayM
NSDictionary __NSDictionaryI
NSMutableDictionary __NSDictionaryM

其他請大家自行Google

JRSwizzle

在項目中我們肯定會在很多地方用到Method Swizzling,而且在使用這個特性時有很多需要注意的地方。我們可以將Method Swizzling封裝起來,也可以使用一些比較成熟的第三方。

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

#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類
#import <objc/objc.h>
BOOL ClassMethodSwizzle(Class klass, SEL origSel, SEL altSel);
BOOL MethodSwizzle(Class klass, SEL origSel, SEL altSel);

Method Swizzling 錯誤剖析

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

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

錯誤代碼
+ (void)load {
    // 這里不應(yīng)該調(diào)用super,會導(dǎo)致父類被重復(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);
}

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

第一次

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

第二次

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

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

問題原因

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

交換過程
Dispatch Table 交換流程

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

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

+ (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的實現(xiàn)源碼,從源碼來看,其實內(nèi)部實現(xiàn)很簡單。核心代碼就是交換兩個Methodimp函數(shù)指針,這也就是方法被swizzling多次,可能會被換回去的原因,因為每次調(diào)用都會執(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危險嗎

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

這個問題可以引用念茜大神的一句話:“使用Method Swizzling編程就好比切菜時使用鋒利的刀,一些人因為擔(dān)心切到自己所以害怕鋒利的刀具,可是事實上,使用鈍刀往往更容易出事,而利刀更為安全。”


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

Demo只是來輔助讀者更好的理解文章中的內(nèi)容,應(yīng)該博客結(jié)合Demo一起學(xué)習(xí),只看Demo還是不能理解更深層的原理。Demo中代碼都會有注釋,各位可以打斷點跟著Demo執(zhí)行流程走一遍,看看各個階段變量的值。

Demo地址:劉小壯的Github


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

Runtime PDF

下載地址:Runtime PDF
麻煩各位大佬點個贊,謝謝!??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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