<簡書 — 劉小壯> http://www.jianshu.com/p/ff19c04b34d0git
公司年末要在新年前發一個版本,最近一直很忙,很久沒有更新博客了。正好如今新版本開發的差很少了,抽空總結一下。因爲最近開發新版本,就避免不了在開發和調試過程當中引發崩潰,以及誘發一些以前的__bug__致使的崩潰。並且項目比較大也很很差排查,正好想起以前研究過的
Method Swizzling
,考慮是否能用這個蘋果的「黑魔法」解決問題,固然用好這個黑魔法並不侷限於解決這些問題....github
就拿咱們公司項目來講吧,咱們公司是作導航的,並且項目規模比較大,各個控制器功能都已經實現。忽然有一天老大過來,說咱們要在全部頁面添加統計功能,也就是用戶進入這個頁面就統計一次。咱們會想到下面的一些方法:編程
直接簡單粗暴的在每一個控制器中加入統計,複製、粘貼、複製、粘貼...
上面這種方法太Low了,消耗時間並且之後很是難以維護,會讓後面的開發人員罵死的。設計模式
咱們可使用OOP
的特性之一,繼承的方式來解決這個問題。建立一個基類,在這個基類中添加統計方法,其餘類都繼承自這個基類。數組
然而,這種方式修改仍是很大,並且定製性不好。之後有新人加入以後,都要囑咐其繼承自這個基類,因此這種方式並不可取。安全
咱們能夠爲UIViewController
建一個Category
,而後在全部控制器中引入這個Category
。固然咱們也能夠添加一個PCH
文件,而後將這個Category
添加到PCH
文件中。函數
咱們建立一個Category
來覆蓋系統方法,系統會優先調用Category
中的代碼,而後在調用原類中的代碼。源碼分析
咱們能夠經過下面的這段僞代碼來看一下:佈局
#import "UIViewController+EventGather.h" @implementation UIViewController (EventGather) - (void)viewDidLoad { NSLog(@"頁面統計:%@", self); } @end
咱們可使用蘋果的「黑魔法」Method Swizzling
,Method Swizzling
本質上就是對IMP
和SEL
進行交換。學習
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
時,核心代碼主要就是一個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
方法。
以前我也說到,在咱們項目開發過程當中,常常由於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
封裝起來,也可使用一些比較成熟的第三方。
在這裏我推薦__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);
在上面的例子中,若是隻是單獨對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
的imp
函數指針,這也就是方法被swizzling
屢次,可能會被換回去的緣由,由於每次調用都會執行一次交換操做。
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
能夠對這個類的Dispatch Table
進行操做,操做後的結果對全部當前類及子類都會產生影響,因此有人認爲Method Swizzling
是一種危險的技術,用很差很容易致使一些不可預見的__bug__,這些__bug__通常都是很是難發現和調試的。
這個問題能夠引用念茜大神的一句話:使用 Method Swizzling 編程就比如切菜時使用鋒利的刀,一些人由於擔憂切到本身因此懼怕鋒利的刀具,但是事實上,使用鈍刀每每更容易出事,而利刀更爲安全。
在這個Demo
中經過Method Swizzling
,簡單實現了一個崩潰攔截功能。實現方式就是將原方法Swizzling
爲本身定義的方法,在執行時先在本身方法中作判斷,根據是否異常再作下一步處理。
Demo
只是來輔助讀者更好的理解文章中的內容,應該博客結合Demo
一塊兒學習,只看Demo
仍是不能理解更深層的原理。Demo
中代碼都會有註釋,各位能夠打斷點跟着Demo
執行流程走一遍,看看各個階段變量的值。
Demo地址:劉小壯的Github
簡書因爲排版的問題,閱讀體驗並很差,佈局、圖片顯示、代碼等不少問題。因此建議到我Github
上,下載Runtime PDF
合集。把全部Runtime
文章總計九篇,都寫在這個PDF
中,並且左側有目錄,方便閱讀。
下載地址:Runtime PDF麻煩各位大佬點個贊,謝謝!😁