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

概述

今天我們主要討論iOS runtime中的一種黑色技術,稱為Method Swizzling。字面上理解Method Swizzling可能比較晦澀難懂,畢竟不是中文,不過你可以理解為“移花接木”或者“偷天換日”。

用途

介紹某種技術的用途,最簡單的方式就是拋出一些應用場景來引出這種技術的必要性。因此,這里我舉個例子如下。

假設工程中有很多ViewController,我需要你統計每個頁面間跳轉的次數。要求:對原工程的改動越少越好。

針對以上需求,你可能會立馬想出以下兩種方案:

方案一:

在每個ViewController的viewWillAppear或者viewDidAppear方法中對記錄跳轉次數的某個全局變量(設為g_viewTransCount)進行計數自增,代碼應該是這樣的:

1

2

3

4

5- (void)viewDidAppear:(BOOL)animated

{

[superviewDidAppear:animated];

g_viewTransCount++;

}

每個ViewController類中都需要做此操作,顯然不合適。因為跳轉次數統計這種業務與APP的主業務并沒有強關聯,上面的代碼會造成耦合度過高。隨著APP業務的不斷擴大,代碼中這樣的雜質代碼會越來越大,維護也越來越困難。而且該方案也違背了我們的要求:對原工程的改動越少越好。因此方案一是個很差的方法。于是我們有了方案二。

方案二:

有沒有某種方法可以不用對每個ViewCotroller都修改呢?有!讓每個ViewController都繼承某個新的ViewController(設為BaseViewController),然后將統計的代碼放到BaseViewCotroller的 viewWillAppear或者viewDidAppear中。這種方案看似較合理,但有以下弊端:

繼承自BaseViewCotroller的ViewController中仍舊需要顯式調用[super viewDidAppear:animated];

需要到所有ViewController的頭文件中更改其superClass為BaseViewController

可見,方案二雖然相比方案一少一些看得到的“代碼雜質”,但對工程的改動同樣是巨大的,尤其當工程比較龐大時。

正因為以上方案的不完美,才引出本文的黑科技:Method Swizzling。

先概括一下在上述情景下使用Method Swizzling有哪些優勢:

不需要改動現有工程的任何文件

本次統計的代碼可復用給其他工程

實現

接下來就是激動人心的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:方法調用前插入了跳頁計數處理,這一切都在運行時完成。對于上述代碼有以下幾處需要介紹的:

+ (void)load方法是一個類方法,當某個類的代碼被讀到內存后,runtime會給每個類發送+ (void)load消息。因此+ (void)load方法是一個調用時機相當早的方法,而且不管父類還是子類,其+ (void)load方法都會被調用到,很適合用來插入swizzling方法

最核心的代碼要數+ (void)swizzleMethods:(Class)classoriginalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel了。從函數簽名可以看出,該函數是為了交換兩個方法內部實現。將目光移到Line23,交換兩個方法的內部實現主要依靠兩個runtime API:

1

2class_replaceMethod(class, swizSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));

method_exchangeImplementations(origMethod, swizMethod);

再看一下Line32,- (void)swiz_viewDidAppear:(BOOL)animated函數看起來像死循環,實際上不會的。原因請看我在下圖的注釋:

此外,通過斷點可以進一步判斷出view controller的viewDidAppear實際方法體與category的swiz_viewDidAppear方法的執行先后順序。為了更直觀地說明二者的順序,我們可以看一下我打出的Log:

通過Log所打印出的順序足以驗證我們的想法。

以上的method swizzling可以應用于iOS的任何類中對其進行代碼注入,并且絲毫不影響現有工程的代碼。例如,我再舉個例子(沒辦法,我就是喜歡舉例子,但我無非是想讓你掌握的更多一些)。你想統計整個工程中所有按鈕的點擊事件的次數,也就是touchUpInside event發生的次數。剛開始你可能會覺得稍微有些沒有頭緒,因為注入代碼的“切入點”相比于UIViewController的viewDidLoad等方法而言不是那么好找。這時候如果你能仔細考慮以下問題或許能找到思路:

touchUpInside event發送給什么對象?

該對象本通過什么途徑接受這個消息?

第一個問題很好回答,event是發送給UIButton實例,本質上是發送給UIControl實例;

第二個問題你不懂的話就去看看UIControl的頭文件找找線索,于是在頭文件中我們找到這樣一個函數:

1

- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event;

看起來很靠近我們的需求, 事實上的確如此。這要從iOS的事件傳遞機制說起,當你在iOS設備上觸摸一個點時這個觸摸動作被包裝成一個UIEvent按照UIApplication->UIWindow->UIView的順序傳遞下去,當發現最后的接受者是UIControl時就會發送上述消息。因此,我們可以對sendAction:方法進行swizzling代碼注入來達到統計按鈕點擊次數的目的。更深入一些,則需要針對不同的action、target、event的狀態進行判斷,以達到更精準的統計。關于這一部分內容我將在下一篇iOS動態性系列文章中詳細探討,敬請期待!

OK,文章就到這里,小伙伴們洗洗睡吧。哈哈,開個玩笑,俗話說,“好戲都在后頭”,接下來的部分更好用。看來以上的method swizzling代碼你是否覺得太復雜了?此外,當你嘗試對多個類進行swizzle時會發現很多代碼是冗余的,每個category文件的框架都長得差不多。那是否有進一步封裝的可能性呢?那是必須的。慶幸的是有團隊已經幫我們封裝了,我們直接拿來用就可以。這就是有名的Aspect庫。

AOP編程以及Aspect庫

Aspect庫是對面向切面編程(Aspect Oriented Programming)的實現,里面封裝了Runtime的方法,也封裝了上文的Method Swizzling方法。因此我們也可以看到,Method Swizzling也是AOP編程的一種。Aspect的用途很廣泛,這里不具體展開,想了解更多的可以看一下官方github的介紹,已經夠詳細了。這里我們只介紹其基礎應用。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對本文最初提出的需求“統計每個頁面間跳轉的次數”進行改造,代碼變成這樣子:

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函數最開始處即可,你可以參考我在文末貼出的代碼,使用一個專門的管理類來管理這些AOP代碼。

相比于上半部分的原始Method Swizzling代碼,使用Aspect有以下好處:

原則上不需要新建任何文件。這點很好理解,原始Method Swizzling需要新建category文件,當代碼注入的需要較多時會出現過多的文件以及冗余代碼。

可以對類的實例進行代碼注入,因為Aspect提供了實例方法以及類方法

寫在最后

Method Swizzling以及Runtime的一些特性就是iOS里的黑科技,如果能靈活應用的話可以在保證解決問題的前提下降低模塊之間的耦合度,提高代碼的可復用性。至于Method Swizzling與Aspect庫的選擇因人而異,我個人建議在最初階段先放下Aspect而只用Method Swizzling原始代碼去實現代碼注入。掌握本質總是不吃虧的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容