【原】iOS動態(tài)性(三) Method Swizzling以及AOP編程:在運行時進行代碼注入

概述

今天我們主要討論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)代碼注入拘荡。掌握本質總是不吃虧的臼节。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市珊皿,隨后出現(xiàn)的幾起案子官疲,更是在濱河造成了極大的恐慌,老刑警劉巖亮隙,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件途凫,死亡現(xiàn)場離奇詭異,居然都是意外死亡溢吻,警方通過查閱死者的電腦和手機维费,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來促王,“玉大人犀盟,你說我怎么就攤上這事∮牵” “怎么了阅畴?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長迅耘。 經(jīng)常有香客問我贱枣,道長监署,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任纽哥,我火速辦了婚禮钠乏,結果婚禮上,老公的妹妹穿的比我還像新娘春塌。我一直安慰自己晓避,他們只是感情好,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布只壳。 她就那樣靜靜地躺著俏拱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吼句。 梳的紋絲不亂的頭發(fā)上锅必,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音命辖,去河邊找鬼况毅。 笑死,一個胖子當著我的面吹牛尔艇,可吹牛的內(nèi)容都是我干的尔许。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼终娃,長吁一口氣:“原來是場噩夢啊……” “哼味廊!你這毒婦竟也來了?” 一聲冷哼從身側響起棠耕,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤余佛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后窍荧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辉巡,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年蕊退,在試婚紗的時候發(fā)現(xiàn)自己被綠了郊楣。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡瓤荔,死狀恐怖净蚤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情输硝,我是刑警寧澤今瀑,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響橘荠,放射性物質發(fā)生泄漏屿附。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一砾医、第九天 我趴在偏房一處隱蔽的房頂上張望拿撩。 院中可真熱鬧衣厘,春花似錦如蚜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至型宙,卻和暖如春撬呢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背妆兑。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工魂拦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人搁嗓。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓芯勘,卻偏偏與公主長得像,于是被迫代替她去往敵國和親腺逛。 傳聞我的和親對象是個殘疾皇子荷愕,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容