轉自:http://www.jianshu.com/p/ff19c04b34d0
需求
就拿我們公司項目來說吧,我們公司是做導航的,而且項目規模比較大,各個控制器功能都已經實現。突然有一天老大過來,說我們要在所有頁面添加統計功能,也就是用戶進入這個頁面就統計一次。我們會想到下面的一些方法:
手動添加
直接簡單粗暴的在每個控制器中加入統計,複製、粘貼、複製、粘貼...
上面這種方法太Low了,消耗時間而且以後非常難以維護,會讓後面的開發人員罵死的。
繼承
我們可以使用OOP
的特性之一,繼承的方式來解決這個問題。創建一個基類,在這個基類中添加統計方法,其他類都繼承自這個基類。
然而,這種方式修改還是很大,而且定製性很差。以後有新人加入之後,都要囑咐其繼承自這個基類,所以這種方式並不可取。
Category
我們可以爲UIViewController
建一個Category
,然後在所有控制器中引入這個Category
。當然我們也可以添加一個PCH
文件,然後將這個Category
添加到PCH
文件中。
我們創建一個Category
來覆蓋系統方法,系統會優先調用Category
中的代碼,然後在調用原類中的代碼。
我們可以通過下面的這段僞代碼來看一下:
#import "UIViewController+EventGather.h"
@implementation UIViewController (EventGather)
- (void)viewDidLoad {
NSLog(@"頁面統計:%@", self);
}
@end
Method Swizzling
我們可以使用蘋果的“黑魔法”Method Swizzling
,Method
Swizzling
本質上就是對IMP
和SEL
進行交換。
Method Swizzling原理
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使用
在實現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];
,這難道不會產生遞歸調用嗎?
答:然而....並不會