該文章屬于劉小壯原創(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 Swizzling
,Method Swizzling
本質(zhì)上就是對IMP
和SEL
進行交換。
原理
Method Swizzing
是發(fā)生在運行時的,主要用于在運行時將兩個Method
進行交換,我們可以將Method Swizzling
代碼寫到任何地方,但是只有在這段Method Swilzzling
代碼執(zhí)行完畢之后互換才起作用。
而且Method Swizzling
也是iOS
中AOP
(面相切面編程)的一種實現(xiàn)方式,我們可以利用蘋果這一特性來實現(xiàn)AOP
編程。
原理分析
首先,讓我們通過兩張圖片來了解一下Method Swizzling
的實現(xiàn)原理
上面圖一中selector2
原本對應(yīng)著IMP2
,但是為了更方便的實現(xiàn)特定業(yè)務(wù)需求,我們在圖二中添加了selector3
和IMP3
,并且讓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ì)是將類中的SEL
和IMP
(可以理解為函數(shù)指針)進行對應(yīng)。而我們的Method Swizzling
就是對這個table
進行了操作,讓SEL
對應(yīng)另一個IMP
。
使用
在實現(xiàn)Method Swizzling
時,核心代碼主要就是一個runtime
的C
語言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è)我們將A
和B
兩個方法進行互換,向A方法發(fā)送消息時執(zhí)行的卻是B
方法,向B
方法發(fā)送消息時執(zhí)行的是A
方法。
例如我們上面的代碼,系統(tǒng)調(diào)用UIViewController
的viewDidLoad
方法時,實際上執(zhí)行的是我們實現(xiàn)的swizzlingViewDidLoad
方法。而我們在swizzlingViewDidLoad
方法內(nèi)部調(diào)用[self swizzlingViewDidLoad];
時,執(zhí)行的是UIViewController
的viewDidLoad
方法。
Method Swizzling類簇
之前我也說到,在我們項目開發(fā)過程中,經(jīng)常因為NSArray
數(shù)組越界或者NSDictionary
的key
或者value
值為nil
等問題導(dǎo)致的崩潰,對于這些問題蘋果并不會報一個警告,而是直接崩潰,感覺蘋果這樣確實有點太狠了。
由此,我們可以根據(jù)上面所學(xué),對NSArray
、NSMutableArray
、NSDictionary
、NSMutableDictionary
等類進行Method Swizzling
,實現(xiàn)方式還是按照上面的例子來做。但是,你發(fā)現(xiàn)Method Swizzling
根本就不起作用。
這是因為Method Swizzling
對NSArray
這些的類簇是不起作用的。因為這些類簇類,其實是一種抽象工廠的設(shè)計模式。抽象工廠內(nèi)部有很多其它繼承自當(dāng)前類的子類,抽象工廠類會根據(jù)不同情況,創(chuàng)建不同的抽象對象來進行使用。例如我們調(diào)用NSArray
的objectAtIndex:
方法,這個類會在方法內(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 錯誤剖析
在上面的例子中,如果只是單獨對NSArray
或NSMutableArray
中的單個類進行Method Swizzling
,是可以正常使用并且不會發(fā)生異常的。如果進行Method Swizzling
的類中,有兩個類有繼承關(guān)系的,并且Swizzling
了同一個方法。例如同時對NSArray
和NSMutableArray
中的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)致父類NSArray
的Swizzling
代碼被執(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
就交換一次SEL
和IMP
(可以理解為函數(shù)指針),如果Swizzling
被執(zhí)行了多次,就相當(dāng)于SEL
和IMP
被交換了多次。這就會導(dǎo)致第一次執(zhí)行成功交換了、第二次執(zhí)行又換回去了、第三次執(zhí)行.....這樣換來換去的結(jié)果,能不能成功就看運氣了??,這也是好多人說Method Swizzling
不好用的原因之一。
交換過程
從這張圖中我們也可以看出問題產(chǎn)生的原因了,就是Swizzling
的代碼被重復(fù)執(zhí)行,為了避免這樣的原因出現(xiàn),我們可以通過GCD
的dispatch_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)很簡單。核心代碼就是交換兩個Method
的imp
函數(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
麻煩各位大佬點個贊,謝謝!??