概述
今天我們主要討論iOS runtime中的一種黑色技術齐遵,稱為Method Swizzling娩嚼。字面上理解Method Swizzling可能比較晦澀難懂爹橱,畢竟不是中文,不過你可以理解為“移花接木”或者“偷天換日”纵搁。
用途
介紹某種技術的用途吃衅,最簡單的方式就是拋出一些應用場景來引出這種技術的必要性。因此腾誉,這里我舉個例子如下徘层。
假設工程中有很多ViewController,我需要你統(tǒng)計每個頁面間跳轉的次數(shù)利职。要求:對原工程的改動越少越好趣效。
針對以上需求,你可能會立馬想出以下兩種方案:
方案一:
在每個ViewController的viewWillAppear或者viewDidAppear方法中對記錄跳轉次數(shù)的某個全局變量(設為g_viewTransCount)進行計數(shù)自增猪贪,代碼應該是這樣的:
1
2
3
4
5- (void)viewDidAppear:(BOOL)animated
{
[superviewDidAppear:animated];
g_viewTransCount++;
}
每個ViewController類中都需要做此操作跷敬,顯然不合適。因為跳轉次數(shù)統(tǒng)計這種業(yè)務與APP的主業(yè)務并沒有強關聯(lián)热押,上面的代碼會造成耦合度過高西傀。隨著APP業(yè)務的不斷擴大,代碼中這樣的雜質代碼會越來越大桶癣,維護也越來越困難拥褂。而且該方案也違背了我們的要求:對原工程的改動越少越好。因此方案一是個很差的方法鬼廓。于是我們有了方案二肿仑。
方案二:
有沒有某種方法可以不用對每個ViewCotroller都修改呢致盟?有碎税!讓每個ViewController都繼承某個新的ViewController(設為BaseViewController),然后將統(tǒng)計的代碼放到BaseViewCotroller的 viewWillAppear或者viewDidAppear中馏锡。這種方案看似較合理雷蹂,但有以下弊端:
繼承自BaseViewCotroller的ViewController中仍舊需要顯式調用[super viewDidAppear:animated];
需要到所有ViewController的頭文件中更改其superClass為BaseViewController
可見,方案二雖然相比方案一少一些看得到的“代碼雜質”杯道,但對工程的改動同樣是巨大的匪煌,尤其當工程比較龐大時责蝠。
正因為以上方案的不完美,才引出本文的黑科技:Method Swizzling萎庭。
先概括一下在上述情景下使用Method Swizzling有哪些優(yōu)勢:
不需要改動現(xiàn)有工程的任何文件
本次統(tǒng)計的代碼可復用給其他工程
實現(xiàn)
接下來就是激動人心的Coding Time了霜医。讓我們解開Method Swizzling的神秘面紗。直接上代碼驳规,有注釋肴敛。在工程中新建一個UIViewController的category:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37#import "UIViewController+swizzling.h"
#import
@implementationUIViewController (swizzling)
+ (void)load
{
SELorigSel =@selector(viewDidAppear:);
SELswizSel =@selector(swiz_viewDidAppear:);
[UIViewController swizzleMethods:[selfclass] originalSelector:origSel swizzledSelector:swizSel];
}
//exchange implementation of two methods
+ (void)swizzleMethods:(Class)classoriginalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel
{
Method origMethod = class_getInstanceMethod(class, origSel);
Method swizMethod = class_getInstanceMethod(class, swizSel);
//class_addMethod will fail if original method already exists
BOOLdidAddMethod = class_addMethod(class, origSel, method_getImplementation(swizMethod), method_getTypeEncoding(swizMethod));
if(didAddMethod) {
class_replaceMethod(class, swizSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
}else{
//origMethod and swizMethod already exist
method_exchangeImplementations(origMethod, swizMethod);
}
}
- (void)swiz_viewDidAppear:(BOOL)animated
{
NSLog(@"I am in - [swiz_viewDidAppear:]");
//handle viewController transistion counting here, before ViewController instance calls its -[viewDidAppear:] method
//需要注入的代碼寫在此處
[selfswiz_viewDidAppear:animated];
}
@end
上述代碼做了這么一件事:在UIViewController的viewDidAppear:方法調用前插入了跳頁計數(shù)處理,這一切都在運行時完成吗购。對于上述代碼有以下幾處需要介紹的:
+ (void)load方法是一個類方法医男,當某個類的代碼被讀到內(nèi)存后,runtime會給每個類發(fā)送+ (void)load消息捻勉。因此+ (void)load方法是一個調用時機相當早的方法镀梭,而且不管父類還是子類,其+ (void)load方法都會被調用到踱启,很適合用來插入swizzling方法
最核心的代碼要數(shù)+ (void)swizzleMethods:(Class)classoriginalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel了报账。從函數(shù)簽名可以看出,該函數(shù)是為了交換兩個方法內(nèi)部實現(xiàn)埠偿。將目光移到Line23笙什,交換兩個方法的內(nèi)部實現(xiàn)主要依靠兩個runtime API:
1
2class_replaceMethod(class, swizSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
method_exchangeImplementations(origMethod, swizMethod);
再看一下Line32,- (void)swiz_viewDidAppear:(BOOL)animated函數(shù)看起來像死循環(huán)胚想,實際上不會的琐凭。原因請看我在下圖的注釋:
此外,通過斷點可以進一步判斷出view controller的viewDidAppear實際方法體與category的swiz_viewDidAppear方法的執(zhí)行先后順序浊服。為了更直觀地說明二者的順序统屈,我們可以看一下我打出的Log:
通過Log所打印出的順序足以驗證我們的想法。
以上的method swizzling可以應用于iOS的任何類中對其進行代碼注入牙躺,并且絲毫不影響現(xiàn)有工程的代碼愁憔。例如,我再舉個例子(沒辦法孽拷,我就是喜歡舉例子吨掌,但我無非是想讓你掌握的更多一些)。你想統(tǒng)計整個工程中所有按鈕的點擊事件的次數(shù)脓恕,也就是touchUpInside event發(fā)生的次數(shù)膜宋。剛開始你可能會覺得稍微有些沒有頭緒,因為注入代碼的“切入點”相比于UIViewController的viewDidLoad等方法而言不是那么好找炼幔。這時候如果你能仔細考慮以下問題或許能找到思路:
touchUpInside event發(fā)送給什么對象秋茫?
該對象本通過什么途徑接受這個消息?
第一個問題很好回答乃秀,event是發(fā)送給UIButton實例肛著,本質上是發(fā)送給UIControl實例圆兵;
第二個問題你不懂的話就去看看UIControl的頭文件找找線索,于是在頭文件中我們找到這樣一個函數(shù):
1
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event;
看起來很靠近我們的需求枢贿, 事實上的確如此殉农。這要從iOS的事件傳遞機制說起,當你在iOS設備上觸摸一個點時這個觸摸動作被包裝成一個UIEvent按照UIApplication->UIWindow->UIView的順序傳遞下去局荚,當發(fā)現(xiàn)最后的接受者是UIControl時就會發(fā)送上述消息统抬。因此,我們可以對sendAction:方法進行swizzling代碼注入來達到統(tǒng)計按鈕點擊次數(shù)的目的危队。更深入一些聪建,則需要針對不同的action、target茫陆、event的狀態(tài)進行判斷金麸,以達到更精準的統(tǒng)計。關于這一部分內(nèi)容我將在下一篇iOS動態(tài)性系列文章中詳細探討簿盅,敬請期待挥下!
OK,文章就到這里桨醋,小伙伴們洗洗睡吧棚瘟。哈哈,開個玩笑喜最,俗話說偎蘸,“好戲都在后頭”,接下來的部分更好用瞬内∶匝看來以上的method swizzling代碼你是否覺得太復雜了?此外虫蝶,當你嘗試對多個類進行swizzle時會發(fā)現(xiàn)很多代碼是冗余的章咧,每個category文件的框架都長得差不多。那是否有進一步封裝的可能性呢能真?那是必須的赁严。慶幸的是有團隊已經(jīng)幫我們封裝了,我們直接拿來用就可以粉铐。這就是有名的Aspect庫疼约。
AOP編程以及Aspect庫
Aspect庫是對面向切面編程(Aspect Oriented Programming)的實現(xiàn),里面封裝了Runtime的方法秦躯,也封裝了上文的Method Swizzling方法忆谓。因此我們也可以看到,Method Swizzling也是AOP編程的一種踱承。Aspect的用途很廣泛倡缠,這里不具體展開,想了解更多的可以看一下官方github的介紹茎活,已經(jīng)夠詳細了昙沦。這里我們只介紹其基礎應用。Aspect只提供了兩個接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14+ (id)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError**)error {
returnaspect_add((id)self, selector, options, block, error);
}
/// @return A token which allows to later deregister the aspect.
- (id)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError**)error {
returnaspect_add(self, selector, options, block, error);
}
使用起來也非常方便载荔,使用Aspect對本文最初提出的需求“統(tǒng)計每個頁面間跳轉的次數(shù)”進行改造盾饮,代碼變成這樣子:
1
2
3
4
5
6
7[UIViewController aspect_hookSelector:@selector(viewDidLoad)
withOptions:AspectPositionBefore
usingBlock:^(id info){
g_viewTransCount++
NSLog(@"[ASPECT] inject in class instance:%@", [info instance]);
}
error:NULL];
將以上代碼放到AppDelegate的didFinishLaunchingWithOptions函數(shù)最開始處即可,你可以參考我在文末貼出的代碼懒熙,使用一個專門的管理類來管理這些AOP代碼丘损。
相比于上半部分的原始Method Swizzling代碼,使用Aspect有以下好處:
原則上不需要新建任何文件工扎。這點很好理解徘钥,原始Method Swizzling需要新建category文件,當代碼注入的需要較多時會出現(xiàn)過多的文件以及冗余代碼肢娘。
可以對類的實例進行代碼注入呈础,因為Aspect提供了實例方法以及類方法
寫在最后
Method Swizzling以及Runtime的一些特性就是iOS里的黑科技,如果能靈活應用的話可以在保證解決問題的前提下降低模塊之間的耦合度橱健,提高代碼的可復用性而钞。至于Method Swizzling與Aspect庫的選擇因人而異,我個人建議在最初階段先放下Aspect而只用Method Swizzling原始代碼去實現(xiàn)代碼注入拘荡。掌握本質總是不吃虧的臼节。