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

公司年底要在新年前發(fā)一個(gè)版本,最近一直很忙,好久沒(méi)有更新博客了。正好現(xiàn)在新版本開(kāi)發(fā)的差不多了,抽空總結(jié)一下。
由于最近開(kāi)發(fā)新版本,就避免不了在開(kāi)發(fā)和調(diào)試過(guò)程中引起崩潰,以及誘發(fā)一些之前的bug導(dǎo)致的崩潰。而且項(xiàng)目比較大也很不好排查,正好想起之前研究過(guò)的Method Swizzling,考慮是否能用這個(gè)蘋(píng)果的“黑魔法”解決問(wèn)題,當(dāng)然用好這個(gè)黑魔法并不局限于解決這些問(wèn)題。
需求
就拿我們公司項(xiàng)目來(lái)說(shuō)吧,我們公司是做導(dǎo)航的,而且項(xiàng)目規(guī)模比較大,各個(gè)控制器功能都已經(jīng)實(shí)現(xiàn)。突然有一天老大過(guò)來(lái),說(shuō)我們要在所有頁(yè)面添加統(tǒng)計(jì)功能,也就是用戶進(jìn)入這個(gè)頁(yè)面就統(tǒng)計(jì)一次。我們會(huì)想到下面的一些方法:
手動(dòng)添加
直接簡(jiǎn)單粗暴的在每個(gè)控制器中加入統(tǒng)計(jì),復(fù)制、粘貼、復(fù)制、粘貼。但這種方法并不太好,消耗時(shí)間而且以后非常難以維護(hù),會(huì)讓后面的開(kāi)發(fā)人員罵死的。
繼承
我們可以使用OOP的特性之一,繼承的方式來(lái)解決這個(gè)問(wèn)題。創(chuàng)建一個(gè)基類(lèi),在這個(gè)基類(lèi)中添加統(tǒng)計(jì)方法,其他類(lèi)都繼承自這個(gè)基類(lèi)。
然而,這種方式修改還是很大,而且定制性很差。以后有新人加入之后,都要囑咐其繼承自這個(gè)基類(lèi),所以這種方式并不可取。
Category
我們可以為UIViewController建一個(gè)Category,然后在所有控制器中引入這個(gè)Category。當(dāng)然我們也可以添加一個(gè)PCH文件,然后將這個(gè)Category添加到PCH文件中。
我們創(chuàng)建一個(gè)Category來(lái)覆蓋系統(tǒng)方法,系統(tǒng)會(huì)優(yōu)先調(diào)用Category中的代碼,然后在調(diào)用原類(lèi)中的代碼。
我們可以通過(guò)下面的這段偽代碼來(lái)看一下。
#import "UIViewController+EventGather.h"
@implementation UIViewController (EventGather)
- (void)viewDidLoad {
}
@end
Method Swizzling
我們可以使用蘋(píng)果的“黑魔法”Method Swizzling,Method Swizzling本質(zhì)上就是對(duì)IMP和SEL進(jìn)行交換。
原理
Method Swizzing是發(fā)生在運(yùn)行時(shí)的,主要用于在運(yùn)行時(shí)將兩個(gè)Method進(jìn)行交換,我們可以將Method Swizzling代碼寫(xiě)到任何地方,但是只有在這段Method Swilzzling代碼執(zhí)行完畢之后互換才起作用。
而且Method Swizzling也是iOS中AOP(面相切面編程)的一種實(shí)現(xiàn)方式,我們可以利用蘋(píng)果這一特性來(lái)實(shí)現(xiàn)AOP編程。
原理分析
首先,讓我們通過(guò)兩張圖片來(lái)了解一下Method Swizzling的實(shí)現(xiàn)原理


上面圖一中selector2原本對(duì)應(yīng)著IMP2,但是為了更方便的實(shí)現(xiàn)特定業(yè)務(wù)需求,我們?cè)趫D二中添加了selector3和IMP3,并且讓selector2指向了IMP3,而selector3則指向了IMP2,這樣就實(shí)現(xiàn)了“方法互換”。
在OC語(yǔ)言的runtime特性中,調(diào)用一個(gè)對(duì)象的方法就是給這個(gè)對(duì)象發(fā)送消息。是通過(guò)查找接收消息對(duì)象的方法列表,從方法列表中查找對(duì)應(yīng)的SEL,這個(gè)SEL對(duì)應(yīng)著一個(gè)IMP(一個(gè)IMP可以對(duì)應(yīng)多個(gè)SEL),通過(guò)這個(gè)IMP找到對(duì)應(yīng)的方法調(diào)用。
在每個(gè)類(lèi)中都有一個(gè)Dispatch Table,這個(gè)Dispatch Table本質(zhì)是將類(lèi)中的SEL和IMP(可以理解為函數(shù)指針)進(jìn)行對(duì)應(yīng)。而我們的Method Swizzling就是對(duì)這個(gè)table進(jìn)行了操作,讓SEL對(duì)應(yīng)另一個(gè)IMP。
使用
在實(shí)現(xiàn)Method Swizzling時(shí),核心代碼主要就是一個(gè)runtime的C語(yǔ)言API。
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
代碼示例
就拿上面我們說(shuō)的頁(yè)面統(tǒng)計(jì)的需求來(lái)說(shuō)吧,這個(gè)需求在很多公司都很常見(jiàn),我們下面的Demo就通過(guò)Method Swizzling簡(jiǎn)單的實(shí)現(xiàn)這個(gè)需求。
我們先給UIViewController添加一個(gè)Category,然后在Category中的load方法中添加Method Swizzling方法,我們用來(lái)替換的方法也寫(xiě)在這個(gè)Category中。由于load類(lèi)方法是程序運(yùn)行時(shí)這個(gè)類(lèi)被加載到內(nèi)存中就調(diào)用的一個(gè)方法,執(zhí)行比較早,并且不需要我們手動(dòng)調(diào)用。而且這個(gè)方法具有唯一性,也就是只會(huì)被調(diào)用一次,不用擔(dān)心資源搶奪的問(wèn)題。
定義Method Swizzling中我們自定義的方法時(shí),需要注意盡量加前綴,以防止和其他地方命名沖突,Method Swizzling的替換方法命名一定要是唯一的,至少在被替換的類(lèi)中必須是唯一的。
+ (void)load {
// 通過(guò)class_getInstanceMethod()函數(shù)從當(dāng)前對(duì)象中的method list獲取method結(jié)構(gòu)體,如果是類(lèi)方法就使用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);
}
}
// 我們自己實(shí)現(xiàn)的方法,也就是和self的viewDidLoad方法進(jìn)行交換的方法。
- (void)swizzlingViewDidLoad {
NSString *str = [NSString stringWithFormat:@"%@", self.class];
// 我們?cè)谶@里加一個(gè)判斷,將系統(tǒng)的UIViewController的對(duì)象剔除掉
if(![str containsString:@"UI"]){
}
[self swizzlingViewDidLoad];
}
@end
我們?cè)?code>load方法中使用class_addMethod函數(shù)對(duì)Method Swizzling做了一層驗(yàn)證,如果self沒(méi)有實(shí)現(xiàn)被交換的方法,會(huì)導(dǎo)致失敗。
而且,self沒(méi)有交換的方法實(shí)現(xiàn),但是父類(lèi)有這個(gè)方法,這樣就會(huì)調(diào)用父類(lèi)的方法,結(jié)果就不是我們想要的結(jié)果了。所以,我們?cè)谶@里通過(guò)class_addMethod的驗(yàn)證,如果self實(shí)現(xiàn)了這個(gè)方法,class_addMethod函數(shù)將會(huì)返回NO,我們就可以對(duì)其進(jìn)行交換了。
上面的代碼雖然在當(dāng)前方法中,又調(diào)用了當(dāng)前方法,但不會(huì)導(dǎo)致遞歸調(diào)用。Method Swizzling的實(shí)現(xiàn)原理可以理解為”方法互換“。假設(shè)我們將A和B兩個(gè)方法進(jìn)行互換,向A方法發(fā)送消息時(shí)執(zhí)行的卻是B方法,向B方法發(fā)送消息時(shí)執(zhí)行的是A方法。
例如我們上面的代碼,系統(tǒng)調(diào)用UIViewController的viewDidLoad方法時(shí),實(shí)際上執(zhí)行的是我們實(shí)現(xiàn)的swizzlingViewDidLoad方法。而我們?cè)?code>swizzlingViewDidLoad方法內(nèi)部調(diào)用[self swizzlingViewDidLoad];時(shí),執(zhí)行的是UIViewController的viewDidLoad方法。
Method Swizzling類(lèi)簇
之前我也說(shuō)到,在我們項(xiàng)目開(kāi)發(fā)過(guò)程中,經(jīng)常因?yàn)?code>NSArray數(shù)組越界或者NSDictionary的key或者value值為nil等問(wèn)題導(dǎo)致的崩潰,對(duì)于這些問(wèn)題蘋(píng)果并不會(huì)報(bào)一個(gè)警告,而是直接崩潰,感覺(jué)蘋(píng)果這樣確實(shí)有點(diǎn)太狠了。
由此,我們可以根據(jù)上面所學(xué),對(duì)NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等類(lèi)進(jìn)行Method Swizzling,實(shí)現(xiàn)方式還是按照上面的例子來(lái)做。但是,你發(fā)現(xiàn)Method Swizzling根本就不起作用。
這是因?yàn)?code>Method Swizzling對(duì)NSArray這些的類(lèi)簇是不起作用的。因?yàn)檫@些類(lèi)簇類(lèi),其實(shí)是一種抽象工廠的設(shè)計(jì)模式。抽象工廠內(nèi)部有很多其它繼承自當(dāng)前類(lèi)的子類(lèi),抽象工廠類(lèi)會(huì)根據(jù)不同情況,創(chuàng)建不同的抽象對(duì)象來(lái)進(jìn)行使用。例如我們調(diào)用NSArray的objectAtIndex:方法,這個(gè)類(lèi)會(huì)在方法內(nèi)部判斷,內(nèi)部創(chuàng)建不同抽象類(lèi)進(jìn)行操作。
所以也就是我們對(duì)NSArray類(lèi)進(jìn)行操作其實(shí)只是對(duì)父類(lèi)進(jìn)行了操作,在NSArray內(nèi)部會(huì)創(chuàng)建其他子類(lèi)來(lái)執(zhí)行操作,真正執(zhí)行操作的并不是NSArray自身,所以我們應(yīng)該對(duì)其“真身”進(jìn)行操作。
代碼示例
下面我們實(shí)現(xiàn)了防止NSArray因?yàn)檎{(diào)用objectAtIndex:方法,取下標(biāo)時(shí)數(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) {
// 這里做一下異常處理,不然都不知道出錯(cuò)了。
@try {
return [self lxz_objectAtIndex:index];
}
@catch (NSException *exception) {
// 在崩潰后會(huì)打印崩潰信息,方便我們調(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真正的類(lèi),而NSMutableArray又不一樣。我們可以通過(guò)runtime函數(shù)獲取真正的類(lèi)。
objc_getClass("__NSArrayI");
舉例
下面我們列舉一些常用的類(lèi)簇。
| 類(lèi) | 類(lèi)名 |
|---|---|
| NSArray | __NSArrayI |
| NSMutableArray | __NSArrayM |
| NSDictionary | __NSDictionaryI |
| NSMutableDictionary | __NSDictionaryM |
其他請(qǐng)大家自行Google。
JRSwizzle
在項(xiàng)目中我們肯定會(huì)在很多地方用到Method Swizzling,而且在使用這個(gè)特性時(shí)有很多需要注意的地方。我們可以將Method Swizzling封裝起來(lái),也可以使用一些比較成熟的第三方。
在這里我推薦Github上星最多的一個(gè)第三方-jrswizzle。里面核心就兩個(gè)類(lèi),代碼看起來(lái)非常清爽。
#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類(lèi)
#import <objc/objc.h>
BOOL ClassMethodSwizzle(Class klass, SEL origSel, SEL altSel);
BOOL MethodSwizzle(Class klass, SEL origSel, SEL altSel);
Method Swizzling 錯(cuò)誤剖析
在上面的例子中,如果只是單獨(dú)對(duì)NSArray或NSMutableArray中的單個(gè)類(lèi)進(jìn)行Method Swizzling,是可以正常使用并且不會(huì)發(fā)生異常的。如果進(jìn)行Method Swizzling的類(lèi)中,有兩個(gè)類(lèi)有繼承關(guān)系的,并且Swizzling了同一個(gè)方法。例如同時(shí)對(duì)NSArray和NSMutableArray中的objectAtIndex:方法都進(jìn)行了Swizzling,這樣可能會(huì)導(dǎo)致父類(lèi)Swizzling失效的問(wèn)題。
對(duì)于這種問(wèn)題主要是兩個(gè)原因?qū)е碌?,首先是不要?code>load方法中調(diào)用[super load]方法,這會(huì)導(dǎo)致父類(lèi)的Swizzling被重復(fù)執(zhí)行兩次,這樣父類(lèi)的Swizzling就會(huì)失效。例如下面的兩張圖片,你會(huì)發(fā)現(xiàn)由于NSMutableArray調(diào)用了[super load]導(dǎo)致父類(lèi)NSArray的Swizzling代碼被執(zhí)行了兩次。
錯(cuò)誤代碼
+ (void)load {
// 這里不應(yīng)該調(diào)用super,會(huì)導(dǎo)致父類(lèi)被重復(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);
}
這里由于在子類(lèi)中調(diào)用了super,導(dǎo)致NSMutableArray執(zhí)行時(shí),父類(lèi)NSArray也被執(zhí)行了一次。

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

這樣就會(huì)導(dǎo)致程序運(yùn)行過(guò)程中,子類(lèi)調(diào)用Swizzling的方法是沒(méi)有問(wèn)題的,父類(lèi)調(diào)用同一個(gè)方法就會(huì)發(fā)現(xiàn)Swizzling失效了,具體原因我們后面講。
還有一個(gè)原因就是因?yàn)榇a邏輯導(dǎo)致Swizzling代碼被執(zhí)行了多次,這也會(huì)導(dǎo)致Swizzling失效,其實(shí)原理和上面的問(wèn)題是一樣的,我們下面講講為什么會(huì)出現(xiàn)這個(gè)問(wèn)題。
問(wèn)題原因
我們上面提到過(guò)Method Swizzling的實(shí)現(xiàn)原理就是對(duì)類(lèi)的Dispatch Table進(jìn)行操作,每進(jìn)行一次Swizzling就交換一次SEL和IMP(可以理解為函數(shù)指針),如果Swizzling被執(zhí)行了多次,就相當(dāng)于SEL和IMP被交換了多次。這就會(huì)導(dǎo)致第一次執(zhí)行成功交換了、第二次執(zhí)行又換回去了、第三次執(zhí)行.....這樣換來(lái)?yè)Q去的結(jié)果,能不能成功就看運(yùn)氣了??,這也是好多人說(shuō)Method Swizzling不好用的原因之一。
交換過(guò)程

從這張圖中我們也可以看出問(wèn)題產(chǎn)生的原因了,就是Swizzling的代碼被重復(fù)執(zhí)行,為了避免這樣的原因出現(xiàn),我們可以通過(guò)GCD的dispatch_once函數(shù)來(lái)解決,利用dispatch_once函數(shù)內(nèi)代碼只會(huì)執(zhí)行一次的特性。
在每個(gè)Method Swizzling的地方,加上dispatch_once函數(shù)保證代碼只被執(zhí)行一次。當(dāng)然在實(shí)際使用中也可以對(duì)下面代碼進(jìn)行封裝,這里只是給一個(gè)示例代碼。
+ (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的實(shí)現(xiàn)源碼,從源碼來(lái)看,其實(shí)內(nèi)部實(shí)現(xiàn)很簡(jiǎn)單。核心代碼就是交換兩個(gè)Method的imp函數(shù)指針,這也就是方法被swizzling多次,可能會(huì)被換回去的原因,因?yàn)槊看握{(diào)用都會(huì)執(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危險(xiǎn)嗎
既然Method Swizzling可以對(duì)這個(gè)類(lèi)的Dispatch Table進(jìn)行操作,操作后的結(jié)果對(duì)所有當(dāng)前類(lèi)及子類(lèi)都會(huì)產(chǎn)生影響,所以有人認(rèn)為Method Swizzling是一種危險(xiǎn)的技術(shù),用不好很容易導(dǎo)致一些不可預(yù)見(jiàn)的bug,這些bug一般都是非常難發(fā)現(xiàn)和調(diào)試的。
這個(gè)問(wèn)題可以引用念茜大神的一句話:“使用Method Swizzling編程就好比切菜時(shí)使用鋒利的刀,一些人因?yàn)閾?dān)心切到自己所以害怕鋒利的刀具,可是事實(shí)上,使用鈍刀往往更容易出事,而利刀更為安全?!?/p>
在這個(gè)Demo中通過(guò)Method Swizzling,簡(jiǎn)單實(shí)現(xiàn)了一個(gè)崩潰攔截功能。實(shí)現(xiàn)方式就是將原方法Swizzling為自己定義的方法,在執(zhí)行時(shí)先在自己方法中做判斷,根據(jù)是否異常再做下一步處理。
Demo只是來(lái)輔助讀者更好的理解文章中的內(nèi)容,應(yīng)該博客結(jié)合Demo一起學(xué)習(xí),只看Demo還是不能理解更深層的原理。Demo中代碼都會(huì)有注釋?zhuān)魑豢梢源驍帱c(diǎn)跟著Demo執(zhí)行流程走一遍,看看各個(gè)階段變量的值。
Demo地址:劉小壯的Github
簡(jiǎn)書(shū)由于排版的問(wèn)題,閱讀體驗(yàn)并不好,布局、圖片顯示、代碼等很多問(wèn)題。所以建議到我Github上,下載Runtime PDF合集。把所有Runtime文章總計(jì)九篇,都寫(xiě)在這個(gè)PDF中,而且左側(cè)有目錄,方便閱讀。

下載地址:Runtime PDF
麻煩各位大佬點(diǎn)個(gè)贊,謝謝!??