公司年底要在新年前發(fā)一個版本鄙早,最近一直很忙,好久沒有更新博客了椅亚。正好現(xiàn)在新版本開發(fā)的差不多了限番,抽空總結(jié)一下。
由于最近開發(fā)新版本呀舔,就避免不了在開發(fā)和調(diào)試過程中引起崩潰弥虐,以及誘發(fā)一些之前的bug導致的崩潰。而且項目比較大也很不好排查,正好想起之前研究過的Method Swizzling霜瘪,考慮是否能用這個蘋果的“黑魔法”解決問題珠插,當然用好這個黑魔法并不局限于解決這些問題......
需求
就拿我們公司項目來說吧,我們公司是做導航的颖对,而且項目規(guī)模比較大捻撑,各個控制器功能都已經(jīng)實現(xiàn)。突然有一天老大過來缤底,說我們要在所有頁面添加統(tǒng)計功能顾患,也就是用戶進入這個頁面就統(tǒng)計一次。我們會想到下面的一些方法:
手動添加
直接簡單粗暴的在每個控制器中加入統(tǒng)計训堆,復(fù)制描验、粘貼、復(fù)制坑鱼、粘貼...
上面這種方法太Low了膘流,消耗時間而且以后非常難以維護,會讓后面的開發(fā)人員罵死的鲁沥。
繼承
我們可以使用OOP的特性之一呼股,繼承的方式來解決這個問題。創(chuàng)建一個基類画恰,在這個基類中添加統(tǒng)計方法彭谁,其他類都繼承自這個基類。
然而允扇,這種方式修改還是很大缠局,而且定制性很差。以后有新人加入之后考润,都要囑咐其繼承自這個基類狭园,所以這種方式并不可取。
Category
我們可以為UIViewController建一個Category糊治,然后在所有控制器中引入這個Category唱矛。當然我們也可以添加一個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 {
NSLog(@"頁面統(tǒng)計:%@", self);
}
@end
Method Swizzling
我們可以使用蘋果的“黑魔法”Method Swizzling窃肠,Method Swizzling本質(zhì)上就是對IMP和SEL進行交換。
Method Swizzling原理
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侮叮。
Method Swizzling使用
在實現(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);
實現(xiàn)思路
就拿上面我們說的頁面統(tǒng)計的需求來說吧,這個需求在很多公司都很常見囊榜,我們下面的Demo就通過Method Swizzling簡單的實現(xiàn)這個需求审胸。
我們先給UIViewController添加一個Category,然后在Category中的+(void)load方法中添加Method Swizzling方法卸勺,我們用來替換的方法也寫在這個Category中歹嘹。由于load類方法是程序運行時這個類被加載到內(nèi)存中就調(diào)用的一個方法,執(zhí)行比較早孔庭,并且不需要我們手動調(diào)用。而且這個方法具有唯一性材蛛,也就是只會被調(diào)用一次圆到,不用擔心資源搶奪的問題。
定義Method Swizzling中我們自定義的方法時卑吭,需要注意盡量加前綴芽淡,以防止和其他地方命名沖突,Method Swizzling的替換方法命名一定要是唯一的豆赏,至少在被替換的類中必須是唯一的挣菲。
#import "UIViewController+swizzling.h"
#import @implementation UIViewController (swizzling)
+ (void)load {
[super load];
// 通過class_getInstanceMethod()函數(shù)從當前對象中的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));
/**
* 我們在這里使用class_addMethod()函數(shù)對Method Swizzling做了一層驗證白胀,如果self沒有實現(xiàn)被交換的方法椭赋,會導致失敗。
* 而且self沒有交換的方法實現(xiàn)或杠,但是父類有這個方法哪怔,這樣就會調(diào)用父類的方法,結(jié)果就不是我們想要的結(jié)果了向抢。
* 所以我們在這里通過class_addMethod()的驗證认境,如果self實現(xiàn)了這個方法,class_addMethod()函數(shù)將會返回NO挟鸠,我們就可以對其進行交換了叉信。
*/
if(!class_addMethod([self class], @selector(viewDidLoad), 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"]){
NSLog(@"統(tǒng)計打點 : %@", self.class);
}
[self swizzlingViewDidLoad];
}
@end
看到上面的代碼,肯定有人會問:樓主枢冤,你太粗心了鸠姨,你在swizzlingViewDidLoad方法中又調(diào)用了[self swizzlingViewDidLoad];,這難道不會產(chǎn)生遞歸調(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等問題導致的崩潰,對于這些問題蘋果并不會報一個警告绞绒,而是直接崩潰婶希,感覺蘋果這樣確實有點“太狠了”。
由此蓬衡,我們可以根據(jù)上面所學喻杈,對NSArray彤枢、NSMutableArray、NSDictionary筒饰、NSMutableDictionary等類進行Method Swizzling缴啡,實現(xiàn)方式還是按照上面的例子來做。但是....你發(fā)現(xiàn)Method Swizzling根本就不起作用龄砰,代碼也沒寫錯啊盟猖,到底是什么鬼?
這是因為Method Swizzling對NSArray這些的類簇是不起作用的换棚。因為這些類簇類式镐,其實是一種抽象工廠的設(shè)計模式。抽象工廠內(nèi)部有很多其它繼承自當前類的子類固蚤,抽象工廠類會根據(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:方法旷偿,取下標時數(shù)組越界導致的崩潰:
#import "NSArray+LXZArray.h"
#import "objc/runtime.h"
@implementation NSArray (LXZArray)
+ (void)load {
[super 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];
}
}
@end
大家發(fā)現(xiàn)了嗎兔仰,__NSArrayI才是NSArray真正的類茫负,而NSMutableArray又不一樣????。我們可以通過runtime函數(shù)獲取真正的類:
objc_getClass("__NSArrayI")
下面我們列舉一些常用的類簇的“真身”:
Method Swizzling封裝
在項目中我們肯定會在很多地方用到Method Swizzling乎赴,而且在使用這個特性時有很多需要注意的地方忍法。我們可以將Method Swizzling封裝起來,也可以使用一些比較成熟的第三方榕吼。
在這里我推薦Github上星最多的一個第三方-jrswizzle
里面核心就兩個類饿序,代碼看起來非常清爽。
#import @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 BOOL ClassMethodSwizzle(Class klass, SEL origSel, SEL altSel);
BOOL MethodSwizzle(Class klass, SEL origSel, SEL altSel);
Method Swizzling危險嗎友题?
既然Method Swizzling可以對這個類的Dispatch Table進行操作,操作后的結(jié)果對所有當前類及子類都會產(chǎn)生影響戴质,所以有人認為Method Swizzling是一種危險的技術(shù)度宦,用不好很容易導致一些不可預(yù)見的bug踢匣,這些bug一般都是非常難發(fā)現(xiàn)和調(diào)試的。
這個問題可以引用念茜大神的一句話:使用 Method Swizzling 編程就好比切菜時使用鋒利的刀戈抄,一些人因為擔心切到自己所以害怕鋒利的刀具离唬,可是事實上,使用鈍刀往往更容易出事划鸽,而利刀更為安全输莺。
討論
這里是一些 Method Swizzling的陷阱:
- Method swizzling is not atomic
- Changes behavior of un-owned code
- Possible naming conflicts
- Swizzling changes the method's arguments
- The order of swizzles matters
- Difficult to understand (looks recursive)
- Difficult to debug
我將逐一分析這些點,增進對Method Swizzling的理解的同時裸诽,并搞懂如何應(yīng)對嫂用。
Method swizzling is not atomic
我所見過的使用method swizzling實現(xiàn)的方法在并發(fā)使用時基本都是安全的。95%的情況里這都不會是個問題丈冬。通常你替換一個方法的實現(xiàn)嘱函,是希望它在整個程序的生命周期里有效的。也就是說埂蕊,你會把 method swizzling 修改方法實現(xiàn)的操作放在一個加號方法 +(void)load里往弓,并在應(yīng)用程序的一開始就調(diào)用執(zhí)行。你將不會碰到并發(fā)問題蓄氧。假如你在 +(void)initialize初始化方法中進行swizzle函似,那么……rumtime可能死于一個詭異的狀態(tài)。
Changes behavior of un-owned code
這是swizzling的一個問題喉童。我們的目標是改變某些代碼撇寞。swizzling方法是一件灰常灰常重要的事泄朴,當你不只是對一個NSButton類的實例進行了修改重抖,而是程序中所有的NSButton實例。因此在swizzling時應(yīng)該多加小心祖灰,但也不用總是去刻意避免钟沛。
想象一下,如果你重寫了一個類的方法局扶,而且沒有調(diào)用父類的這個方法恨统,這可能會引起問題。大多數(shù)情況下三妈,父類方法期望會被調(diào)用(至少文檔是這樣說的)畜埋。如果你在swizzling實現(xiàn)中也這樣做了,這會避免大部分問題畴蒲。還是調(diào)用原始實現(xiàn)吧悠鞍,如若不然,你會費很大力氣去考慮代碼的安全問題模燥。
Possible naming conflicts
命名沖突貫穿整個Cocoa的問題. 我們常常在類名和類別方法名前加上前綴咖祭。不幸的是掩宜,命名沖突仍是個折磨。但是swizzling其實也不必過多考慮這個問題么翰。我們只需要在原始方法命名前做小小的改動來命名就好牺汤,比如通常我們這樣命名:
[cpp] view plaincopy
@interface NSView : NSObject
- (void)setFrame:(NSRect)frame;
@end
@implementation NSView (MyViewAdditions)
- (void)my_setFrame:(NSRect)frame {
// do custom work
[self my_setFrame:frame];
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}
@end
這段代碼運行正確,但是如果my_setFrame: 在別處被定義了會發(fā)生什么呢浩嫌?
這個問題不僅僅存在于swizzling檐迟,這里有一個替代的變通方法:
[cpp] view plaincopy
@implementation NSView (MyViewAdditions)
static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);
static void MySetFrame(id self, SEL _cmd, NSRect frame) {
// do custom work
SetFrameIMP(self, _cmd, frame);
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}
@end
看起來不那么Objectice-C了(用了函數(shù)指針),這樣避免了selector的命名沖突码耐。
最后給出一個較完美的swizzle方法的定義:
[cpp] view plaincopy
typedef IMP *IMPPointer;
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
IMP imp = NULL;
Method method = class_getInstanceMethod(class, original);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(class, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}
}
if (imp && store) { *store = imp; }
return (imp != NULL);
}
@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end
Swizzling changes the method's arguments
我認為這是最大的問題追迟。想正常調(diào)用method swizzling 將會是個問題。
[cpp] view plaincopy
[self my_setFrame:frame];
直接調(diào)用my_setFrame: 伐坏, runtime做的是
[cpp] view plaincopy
objc_msgSend(self, @selector(my_setFrame:), frame);
runtime去尋找my_setFrame:的方法實現(xiàn), _cmd參數(shù)為 my_setFrame: 怔匣,但是事實上runtime找到的方法實現(xiàn)是原始的 setFrame: 的。
一個簡單的解決辦法:使用上面介紹的swizzling定義桦沉。
The order of swizzles matters
多個swizzle方法的執(zhí)行順序也需要注意每瞒。假設(shè) setFrame: 只定義在NSView中,想像一下按照下面的順序執(zhí)行:
[cpp] view plaincopy
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
What happens when the method on NSButton is swizzled? Well most swizzling will ensure that it's not replacing the implementation of setFrame: for all views, so it will pull up the instance method. This will use the existing implementation to re-define setFrame: in the NSButton class so that exchanging implementations doesn't affect all views. The existing implementation is the one defined on NSView. The same thing will happen when swizzling on NSControl (again using the NSView implementation).
When you call setFrame: on a button, it will therefore call your swizzled method, and then jump straight to the setFrame: method originally defined on NSView. The NSControl and NSView swizzled implementations will not be called.
But what if the order were:
[cpp] view plaincopy
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
Since the view swizzling takes place first, the control swizzling will be able to pull up the right method. Likewise, since the control swizzling was before the button swizzling, the button will pull up the control's swizzled implementation of setFrame:. This is a bit confusing, but this is the correct order. How can we ensure this order of things?
Again, just use load to swizzle things. If you swizzle in load and you only make changes to the class being loaded, you'll be safe. The load method guarantees that the super class load method will be called before any subclasses. We'll get the exact right order!
這段貼了原文纯露,硬翻譯太拗口……總結(jié)一下就是:多個有繼承關(guān)系的類的對象swizzle時剿骨,從子類對象開始 。 如果先swizzle父類對象埠褪,那么后面子類對象swizzle時就無法拿到真正的原始方法實現(xiàn)了浓利。
(感謝評論中 qq373127202 的提醒,在此更正一下钞速,十分感謝)
多個有繼承關(guān)系的類的對象swizzle時贷掖,先從父對象開始。 這樣才能保證子類方法拿到父類中的被swizzle的實現(xiàn)渴语。在+(void)load中swizzle不會出錯苹威,就是因為load類方法會默認從父類開始調(diào)用。
Difficult to understand (looks recursive)
(新方法的實現(xiàn))看起來像遞歸驾凶,但是看看上面已經(jīng)給出的 swizzling 封裝方法, 使用起來就很易讀懂.
這個問題是已完全解決的了牙甫!
Difficult to debug
debug時打出的backtrace,其中摻雜著被swizzle的方法名调违,一團糟翱卟浮!上面介紹的swizzle方案技肩,使backtrace中打印出的方法名還是很清晰的且轨。但仍然很難去debug,因為很難記住swizzling影響過什么。給你的代碼寫好文檔(即使只有你一個人會看到)旋奢。養(yǎng)成一個好習慣阿蝶,不會比調(diào)試多線程問題還難的。
結(jié)論
如果使用恰當黄绩,Method swizzling 還是很安全的.一個簡單安全的方法是,僅在load中swizzle玷过。 和許多其他東西一樣爽丹,它也是有危險性的,但理解它了也就可以正確恰當?shù)氖褂盟?/p>