該文章屬於<簡書 — 劉小壯>原創,轉載請註明:
<簡書 — 劉小壯> 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 Swizzling
,Method Swizzling
本質上就是對IMP
和SEL
進行交換。
Method Swizzling原理
Method Swizzing
是發生在運行時的,主要用於在運行時將兩個Method
進行交換,我們可以將Method Swizzling
代碼寫到任何地方,但是只有在這段Method Swilzzling
代碼執行完畢之後互換才起作用。
而且Method Swizzling
也是iOS中AOP
(面相切面編程)的一種實現方式,我們可以利用蘋果這一特性來實現AOP
編程。
首先,讓我們通過兩張圖片來了解一下Method Swizzling
的實現原理
上面圖一中selector2
原本對應着IMP2
,但是爲了更方便的實現特定業務需求,我們在圖二中添加了selector3
和IMP3
,並且讓selector2
指向了IMP3
,而selector3
則指向了IMP2
,這樣就實現了“方法互換”。
在OC
語言的runtime
特性中,調用一個對象的方法就是給這個對象發送消息。是通過查找接收消息對象的方法列表,從方法列表中查找對應的SEL
,這個SEL
對應着一個IMP
(一個IMP
可以對應多個SEL
),通過這個IMP
找到對應的方法調用。
在每個類中都有一個Dispatch Table
,這個Dispatch Table
本質是將類中的SEL
和IMP
(可以理解爲函數指針)進行對應。而我們的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方法。
例如我們上面的代碼,系統調用UIViewController
的viewDidLoad
方法時,實際上執行的是我們實現的swizzlingViewDidLoad
方法。而我們在swizzlingViewDidLoad
方法內部調用[self swizzlingViewDidLoad];
時,執行的是UIViewController
的viewDidLoad
方法。
Method Swizzling類簇
之前我也說到,在我們項目開發過程中,經常因爲NSArray
數組越界或者NSDictionary
的key
或者value
值爲nil
等問題導致的崩潰,對於這些問題蘋果並不會報一個警告,而是直接崩潰,感覺蘋果這樣確實有點“太狠了”。
由此,我們可以根據上面所學,對NSArray
、NSMutableArray
、NSDictionary
、NSMutableDictionary
等類進行Method Swizzling
,實現方式還是按照上面的例子來做。但是….你發現Method Swizzling
根本就不起作用,代碼也沒寫錯啊,到底是什麼鬼?
這是因爲Method Swizzling
對NSArray
這些的類簇是不起作用的。因爲這些類簇類,其實是一種抽象工廠的設計模式。抽象工廠內部有很多其它繼承自當前類的子類,抽象工廠類會根據不同情況,創建不同的抽象對象來進行使用。例如我們調用NSArray
的objectAtIndex:
方法,這個類會在方法內部判斷,內部創建不同抽象類進行操作。
所以也就是我們對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 錯誤剖析
在上面的例子中,如果只是單獨對NSArray
或NSMutableArray
中的單個類進行Method Swizzling
,是可以正常使用並且不會發生異常的。如果進行Method Swizzling
的類中,有兩個類有繼承關係的,並且Swizzling
了同一個方法。例如同時對NSArray
和NSMutableArray
中的objectAtIndex:
方法都進行了Swizzling
,這樣可能會導致父類Swizzling
失效的問題。
對於這種問題主要是兩個原因導致的,首先是不要在+ (void)load
方法中調用[super load]
方法,這會導致父類的Swizzling
被重複執行兩次,這樣父類的Swizzling
就會失效。例如下面的兩張圖片,你會發現由於NSMutableArray
調用了[super load]
導致父類NSArray
的Swizzling
代碼被執行了兩次。
錯誤代碼:
#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
就交換一次SEL
和IMP
(可以理解爲函數指針),如果Swizzling
被執行了多次,就相當於SEL
和IMP
被交換了多次。這就會導致第一次執行成功交換了、第二次執行又換回去了、第三次執行…..這樣換來換去的結果,能不能成功就看運氣了��,這也是好多人說Method Swizzling
不好用的原因之一。
一圖勝千言:
從這張圖中我們也可以看出問題產生的原因了,就是Swizzling
的代碼被重複執行,爲了避免這樣的原因出現,我們可以通過GCD的dispatch_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
本質上就是函數指針,所以我們可以通過打印函數指針的方式,查看SEL
和IMP
的交換流程。
先來一段測試代碼:
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