開(kāi)始文章之前先來(lái)拋出一個(gè)問(wèn)題,假如有一個(gè)已經(jīng)成形的項(xiàng)目,希望在進(jìn)入每個(gè)控制器的時(shí)候添加一個(gè)統(tǒng)計(jì),這個(gè)怎么實(shí)現(xiàn)呢?項(xiàng)目很大,各個(gè)控制器的功能都已經(jīng)很完善,不可能每個(gè)手動(dòng)單獨(dú)去添加。
也許我們可以考慮使用繼承
的方式來(lái)解決這個(gè)問(wèn)題。創(chuàng)建一個(gè)基類,在這個(gè)基類中添加統(tǒng)計(jì)方法,其他類都繼承自這個(gè)基類。然而,這種方式修改還是很大,而且定制性很差。以后有新人加入之后,都要囑咐其繼承自這個(gè)基類,所以這種方式并不可取。
這個(gè)時(shí)候我們就可以使用iOS的黑魔法,Method Swizzling來(lái)實(shí)現(xiàn),開(kāi)始之前先來(lái)了解一個(gè)概念:
1. SEL和IMP
SEL : 類成員方法的指針,但不同于C語(yǔ)言中的函數(shù)指針,函數(shù)指針直接保存了方法的地址,但SEL只是方法編號(hào)。
IMP: 一個(gè)函數(shù)指針,保存了方法的地址
IMP和SEL關(guān)系:
每一個(gè)繼承于NSObject的類都能自動(dòng)獲得runtime的支持。在這樣的一個(gè)類中,有一個(gè)isa指針,指向該類定義的數(shù)據(jù)結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體是由編譯器編譯時(shí)為類(需繼承于NSObject)創(chuàng)建的。在這個(gè)結(jié)構(gòu)體中有包括了指向其父類類定義的指針以及 Dispatch table.。Dispatch table是一張SEL和IMP的對(duì)應(yīng)表。
也就是說(shuō)方法編號(hào)SEL最后還是要通過(guò)Dispatch table表尋找到對(duì)應(yīng)的IMP,IMP就是一個(gè)函數(shù)指針,然后執(zhí)行這個(gè)方法。
2. Method Swizzling
還是上面那個(gè)問(wèn)題,我們可以為UIViewController建一個(gè)Category,然后在所有控制器中引入這個(gè)Category。當(dāng)然我們也可以添加一個(gè)PCH文件,然后將這個(gè)Category添加到PCH文件中。
我們創(chuàng)建一個(gè)Category來(lái)覆蓋系統(tǒng)方法,結(jié)合Method Swizzling,系統(tǒng)會(huì)優(yōu)先調(diào)用Category中的代碼,然后在調(diào)用原類中的代碼。
我們可以通過(guò)下面的這段偽代碼來(lái)看一下:
#import "UIViewController+EventGather.h"
@implementation UIViewController (EventGather)
- (void)viewDidLoad {
NSLog(@"頁(yè)面統(tǒng)計(jì):%@", self);
}
@end
2.1 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代碼寫到任何地方,但是只有在這段Method Swilzzling代碼執(zhí)行完畢之后互換才起作用。
首先,讓我們通過(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è)類中都有一個(gè)Dispatch Table,這個(gè)Dispatch Table本質(zhì)是將類中的SEL和IMP(可以理解為函數(shù)指針)進(jìn)行對(duì)應(yīng)。而我們的Method Swizzling就是對(duì)這個(gè)table進(jìn)行了操作,讓SEL對(duì)應(yīng)另一個(gè)IMP。
2.2 Method Swizzling實(shí)現(xiàn)
在實(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);
我們來(lái)繼續(xù)解決文章開(kāi)頭提到的問(wèn)題。
先給UIViewController添加一個(gè)Category,然后在Category中的+(void)load方法中添加Method Swizzling方法,我們用來(lái)替換的方法也寫在這個(gè)Category中。由于load類方法是程序運(yùn)行時(shí)這個(gè)類被加載到內(nèi)存中就調(diào)用的一個(gè)方法,執(zhí)行比較早,并且不需要我們手動(dòng)調(diào)用。而且這個(gè)方法具有唯一性,也就是只會(huì)被調(diào)用一次,不用擔(dān)心資源搶奪的問(wèn)題。
定義Method Swizzling中我們自定義的方法時(shí),需要注意盡量加前綴,以防止和其他地方命名沖突,Method Swizzling的替換方法命名一定要是唯一的,至少在被替換的類中必須是唯一的。
#import "UIViewController+swizzling.h"
#import <objc/runtime.h>
@implementation UIViewController (swizzling)
+ (void)load {
// 通過(guò)class_getInstanceMethod()函數(shù)從當(dāng)前對(duì)象中的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));
/**
* 我們?cè)谶@里使用class_addMethod()函數(shù)對(duì)Method Swizzling做了一層驗(yàn)證,如果self沒(méi)有實(shí)現(xiàn)被交換的方法,會(huì)導(dǎo)致失敗。
* 而且self沒(méi)有交換的方法實(shí)現(xiàn),但是父類有這個(gè)方法,這樣就會(huì)調(diào)用父類的方法,結(jié)果就不是我們想要的結(jié)果了。
* 所以我們?cè)谶@里通過(guò)class_addMethod()的驗(yàn)證,如果self實(shí)現(xiàn)了這個(gè)方法,class_addMethod()函數(shù)將會(huì)返回NO,我們就可以對(duì)其進(jìn)行交換了。
*/
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"]){
NSLog(@"統(tǒng)計(jì)打點(diǎn) : %@", self.class);
}
[self swizzlingViewDidLoad];
}
@end
看到上面的代碼,肯定有人會(huì)擔(dān)心,在swizzlingViewDidLoad方法中又調(diào)用了[self swizzlingViewDidLoad],難道不會(huì)產(chǎn)生遞歸調(diào)用嗎?
并不會(huì)。
還記得我們上面的圖一和圖二嗎?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è)趕wizzlingViewDidLoad方法內(nèi)部調(diào)用[self swizzlingViewDidLoad]時(shí),執(zhí)行的是UIViewController的viewDidLoad方法。
2.3 Method Swizzling類簇(特殊情況)
有一點(diǎn)要說(shuō)明的是,Method Swizzling對(duì)NSArray,NSDictionary,NSMutableArray,NSMutableDictionary這些的類簇是不起作用的。因?yàn)檫@些類簇類,其實(shí)是一種抽象工廠的設(shè)計(jì)模式。抽象工廠內(nèi)部有很多其它繼承自當(dāng)前類的子類,抽象工廠類會(huì)根據(jù)不同情況,創(chuàng)建不同的抽象對(duì)象來(lái)進(jìn)行使用。例如我們調(diào)用NSArray的objectAtIndex:方法,這個(gè)類會(huì)在方法內(nèi)部判斷,內(nèi)部創(chuàng)建不同抽象類進(jìn)行操作。
所以也就是我們對(duì)NSArray類進(jìn)行操作其實(shí)只是對(duì)父類進(jìn)行了操作,在NSArray內(nèi)部會(huì)創(chuàng)建其他子類來(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)致的崩潰:
#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) {
// 這里做一下異常處理,不然都不知道出錯(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];
}
}
@end
下面我們列舉一些常用的類簇的“真身”:
NSArray : __NSArrayI
NSMutableArray :__NSArrayM
NSDictionary :__NSDictionaryI
NSMutableDictionary: __NSDictionaryM
2.4 Method Swizzling常見(jiàn)錯(cuò)誤
在load里面再次調(diào)用[super load]會(huì)導(dǎo)致Method Swizzling被調(diào)用兩次,最終也交換了兩次,導(dǎo)致交換失敗。
為了防止這種情況的出現(xiàn),可以引入GCD里面的函數(shù),如下圖:
#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);
});
}
3. 實(shí)際項(xiàng)目經(jīng)驗(yàn)
最近發(fā)現(xiàn)一個(gè)好用的魔法method_setImplementation
做iPhoneX適配的時(shí)候發(fā)現(xiàn),原有的根據(jù)狀態(tài)欄statusBar判斷網(wǎng)絡(luò)狀態(tài)的函數(shù)由于狀態(tài)欄布局發(fā)生變化失效,暫時(shí)想把這個(gè)方法用另一種方法來(lái)替換,奈何項(xiàng)目里用到的地方比較多,而且希望僅在iPhone X上生效,其他機(jī)型不變。
這個(gè)時(shí)候我們就可以用這個(gè)黑魔法了。
代碼如下:
if (IS_IPHONEX) { // statusBar布局變化了, 先用NetworkReachability代替著, 有空再研究
Method m = class_getInstanceMethod([IFUtil class], @selector(networkTypeFromStatusBar));
method_setImplementation(m, imp_implementationWithBlock(^NETWORK_TYPE(id self){
AFNetworkReachabilityStatus s = [AFNetworkReachabilityManager sharedManager].networkReachabilityStatus;
switch( s ){
case AFNetworkReachabilityStatusNotReachable: {
return NETWORK_TYPE_NONE;
} break;
case AFNetworkReachabilityStatusReachableViaWiFi: {
return NETWORK_TYPE_WIFI;
} break;
case AFNetworkReachabilityStatusReachableViaWWAN: {
return NETWORK_TYPE_3G;
}
default: {
return NETWORK_TYPE_NONE;
} break;
}
}));
}
}
更多詳細(xì)講解移步http://www.lxweimin.com/p/ff19c04b34d0