iOS黑魔法-Method Swizzling

該文章屬於<簡書 — 劉小壯>原創,轉載請註明:

<簡書 — 劉小壯> http://www.jianshu.com/p/ff19c04b34d0


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

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



佔位圖

需求

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

手動添加

直接簡單粗暴的在每個控制器中加入統計,複製、粘貼、複製、粘貼…
上面這種方法太Low了,消耗時間而且以後非常難以維護,會讓後面的開發人員罵死的。

繼承

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

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

Category

我們可以爲UIViewController建一個Category,然後在所有控制器中引入這個Category。當然我們也可以添加一個PCH文件,然後將這個Category添加到PCH文件中。

我們創建一個Category來覆蓋系統方法,系統會優先調用Category中的代碼,然後在調用原類中的代碼。

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

#import "UIViewController+EventGather.h"
@implementation UIViewController (EventGather)
- (void)viewDidLoad {
   NSLog(@"頁面統計:%@", self);
}
@end
Method Swizzling

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

Method Swizzling原理

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

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

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

圖一

圖二

上面圖一中selector2原本對應着IMP2,但是爲了更方便的實現特定業務需求,我們在圖二中添加了selector3IMP3,並且讓selector2指向了IMP3,而selector3則指向了IMP2,這樣就實現了“方法互換”。

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

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


Method Swizzling使用

在實現Method Swizzling時,核心代碼主要就是一個runtime的C語言API:
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) 
 __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
實現思路

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

我們先給UIViewController添加一個Category,然後在Category中的+(void)load方法中添加Method Swizzling方法,我們用來替換的方法也寫在這個Category中。由於load類方法是程序運行時這個類被加載到內存中就調用的一個方法,執行比較早,並且不需要我們手動調用。而且這個方法具有唯一性,也就是隻會被調用一次,不用擔心資源搶奪的問題。

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

#import "UIViewController+swizzling.h"






#import <objc/runtime.h> @implementation UIViewController (swizzling) + (void)load { // 通過class_getInstanceMethod()函數從當前對象中的method list獲取method結構體,如果是類方法就使用class_getClassMethod()函數獲取。 Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad)); Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad)); /** * 我們在這裏使用class_addMethod()函數對Method Swizzling做了一層驗證,如果self沒有實現被交換的方法,會導致失敗。 * 而且self沒有交換的方法實現,但是父類有這個方法,這樣就會調用父類的方法,結果就不是我們想要的結果了。 * 所以我們在這裏通過class_addMethod()的驗證,如果self實現了這個方法,class_addMethod()函數將會返回NO,我們就可以對其進行交換了。 */ if (!class_addMethod([self class], @selector(swizzlingViewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) { method_exchangeImplementations(fromMethod, toMethod); } } // 我們自己實現的方法,也就是和self的viewDidLoad方法進行交換的方法。 - (void)swizzlingViewDidLoad { NSString *str = [NSString stringWithFormat:@"%@", self.class]; // 我們在這裏加一個判斷,將系統的UIViewController的對象剔除掉 if(![str containsString:@"UI"]){ NSLog(@"統計打點 : %@", self.class); } [self swizzlingViewDidLoad]; } @end

看到上面的代碼,肯定有人會問:樓主,你太粗心了,你在swizzlingViewDidLoad方法中又調用了[self swizzlingViewDidLoad];,這難道不會產生遞歸調用嗎?
答:然而….並不會��。

還記得我們上面的圖一和圖二嗎?Method Swizzling的實現原理可以理解爲”方法互換“。假設我們將A和B兩個方法進行互換,向A方法發送消息時執行的卻是B方法,向B方法發送消息時執行的是A方法。

例如我們上面的代碼,系統調用UIViewControllerviewDidLoad方法時,實際上執行的是我們實現的swizzlingViewDidLoad方法。而我們在swizzlingViewDidLoad方法內部調用[self swizzlingViewDidLoad];時,執行的是UIViewControllerviewDidLoad方法。

Method Swizzling類簇

之前我也說到,在我們項目開發過程中,經常因爲NSArray數組越界或者NSDictionarykey或者value值爲nil等問題導致的崩潰,對於這些問題蘋果並不會報一個警告,而是直接崩潰,感覺蘋果這樣確實有點“太狠了”。

由此,我們可以根據上面所學,對NSArrayNSMutableArrayNSDictionaryNSMutableDictionary等類進行Method Swizzling,實現方式還是按照上面的例子來做。但是….你發現Method Swizzling根本就不起作用,代碼也沒寫錯啊,到底是什麼鬼?

這是因爲Method SwizzlingNSArray這些的類簇是不起作用的。因爲這些類簇類,其實是一種抽象工廠的設計模式。抽象工廠內部有很多其它繼承自當前類的子類,抽象工廠類會根據不同情況,創建不同的抽象對象來進行使用。例如我們調用NSArrayobjectAtIndex:方法,這個類會在方法內部判斷,內部創建不同抽象類進行操作。

所以也就是我們對NSArray類進行操作其實只是對父類進行了操作,在NSArray內部會創建其他子類來執行操作,真正執行操作的並不是NSArray自身,所以我們應該對其“真身”進行操作。

下面我們實現了防止NSArray因爲調用objectAtIndex:方法,取下標時數組越界導致的崩潰:
#import "NSArray+LXZArray.h"




#import "objc/runtime.h" @implementation NSArray (LXZArray) + (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) { // 在崩潰後會打印崩潰信息,方便我們調試。 NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__); NSLog(@"%@", [exception callStackSymbols]); return nil; } @finally {} } else { return [self lxz_objectAtIndex:index]; } } @end

大家發現了嗎,__NSArrayI纔是NSArray真正的類,而NSMutableArray又不一樣��。我們可以通過runtime函數獲取真正的類:

objc_getClass("__NSArrayI")
下面我們列舉一些常用的類簇的“真身”:
“真身”
NSArray __NSArrayI
NSMutableArray __NSArrayM
NSDictionary __NSDictionaryI
NSMutableDictionary __NSDictionaryM

其他自行Google….

Method Swizzling封裝

在項目中我們肯定會在很多地方用到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,是可以正常使用並且不會發生異常的。如果進行Method Swizzling的類中,有兩個類有繼承關係的,並且Swizzling了同一個方法。例如同時對NSArrayNSMutableArray中的objectAtIndex:方法都進行了Swizzling,這樣可能會導致父類Swizzling失效的問題。

對於這種問題主要是兩個原因導致的,首先是不要在+ (void)load方法中調用[super load]方法,這會導致父類的Swizzling被重複執行兩次,這樣父類的Swizzling就會失效。例如下面的兩張圖片,你會發現由於NSMutableArray調用了[super load]導致父類NSArraySwizzling代碼被執行了兩次。

錯誤代碼:
#import "NSMutableArray+LXZArrayM.h"
@implementation NSMutableArray (LXZArrayM)
+ (void)load {
    // 這裏不應該調用super,會導致父類被重複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);
}
這裏由於在子類中調用了super,導致NSMutableArray執行時,父類NSArray也被執行了一次。

第一次
父類NSArray執行了第二次Swizzling,這時候就會出現問題,後面會講具體原因。

第二次

這樣就會導致程序運行過程中,子類調用Swizzling的方法是沒有問題的,父類調用同一個方法就會發現Swizzling失效了…..具體原因我們後面講!

還有一個原因就是因爲代碼邏輯導致Swizzling代碼被執行了多次,這也會導致Swizzling失效,其實原理和上面的問題是一樣的,我們下面講講爲什麼會出現這個問題。

問題原因

我們上面提到過Method Swizzling的實現原理就是對類的Dispatch Table進行操作,每進行一次Swizzling就交換一次SELIMP(可以理解爲函數指針),如果Swizzling被執行了多次,就相當於SELIMP被交換了多次。這就會導致第一次執行成功交換了、第二次執行又換回去了、第三次執行…..這樣換來換去的結果,能不能成功就看運氣了��,這也是好多人說Method Swizzling不好用的原因之一。

一圖勝千言:

Dispatch Table 交換流程

從這張圖中我們也可以看出問題產生的原因了,就是Swizzling的代碼被重複執行,爲了避免這樣的原因出現,我們可以通過GCDdispatch_once函數來解決,利用dispatch_once函數內代碼只會執行一次的特性。

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

#import "NSMutableArray+LXZArrayM.h"
@implementation NSMutableArray (LXZArrayM)

+ (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);
    });
}

這裏還要告訴大家一個調試小技巧,已經知道的可以略過��。我們之前說過IMP本質上就是函數指針,所以我們可以通過打印函數指針的方式,查看SELIMP的交換流程。

先來一段測試代碼:
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:));

NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
method_exchangeImplementations(fromMethod, toMethod);

NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
method_exchangeImplementations(fromMethod, toMethod);

NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
method_exchangeImplementations(fromMethod, toMethod);

NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
看到這個打印結果,大家應該明白什麼問題了吧:
2016-04-13 14:16:33.477 [16314:4979302]      0x1851b7020
2016-04-13 14:16:33.479 [16314:4979302]      0x1000fb3c8
2016-04-13 14:16:33.479 [16314:4979302]      0x1000fb3c8
2016-04-13 14:16:33.480 [16314:4979302]      0x1851b7020
2016-04-13 14:16:33.480 [16314:4979302]      0x1851b7020
2016-04-13 14:16:33.480 [16314:4979302]      0x1000fb3c8
2016-04-13 14:16:33.481 [16314:4979302]      0x1000fb3c8
2016-04-13 14:16:33.481 [16314:4979302]      0x1851b7020

Method Swizzling危險嗎?

既然Method Swizzling可以對這個類的Dispatch Table進行操作,操作後的結果對所有當前類及子類都會產生影響,所以有人認爲Method Swizzling是一種危險的技術,用不好很容易導致一些不可預見的bug,這些bug一般都是非常難發現和調試的。

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


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

Demo只是來輔助讀者更好的理解文章中的內容,應該博客結合Demo一起學習,只看Demo還是不能理解更深層的原理Demo中代碼都會有註釋,各位可以打斷點跟着Demo執行流程走一遍,看看各個階段變量的值。

Demo地址劉小壯的Github

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章