(轉(zhuǎn))Method Swizzling 和 AOP 實踐

轉(zhuǎn)自:http://tech.glowing.com/cn/method-swizzling-aop/
上一篇介紹了 Objective-C Messaging饼丘。利用 Objective-C 的 Runtime 特性,我們可以給語言做擴展辽话,幫助解決項目開發(fā)中的一些設(shè)計和技術(shù)問題肄鸽。這一篇,我們來探索一些利用 Objective-C Runtime 的黑色技巧油啤。這些技巧中最具爭議的或許就是 Method Swizzling 典徘。
介紹一個技巧,最好的方式就是提出具體的需求益咬,然后用它跟其他的解決方法做比較逮诲。
所以,先來看看我們的需求:對 App 的用戶行為進行追蹤和分析幽告。簡單說梅鹦,就是當(dāng)用戶看到某個 View
或者點擊某個 Button
的時候,就把這個事件記下來冗锁。
手動添加
最直接粗暴的方式就是在每個 viewDidAppear
里添加記錄事件的代碼齐唆。
@implementation MyViewController ()- (void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; // Custom code // Logging [Logging logWithEventName:@“my view did appear”];}- (void)myButtonClicked:(id)sender{ // Custom code // Logging [Logging logWithEventName:@“my button clicked”];}

這種方式的缺點也很明顯:它破壞了代碼的干凈整潔。因為 Logging
的代碼本身并不屬于 ViewController
里的主要邏輯冻河。隨著項目擴大箍邮、代碼量增加,你的 ViewController
里會到處散布著 Logging
的代碼叨叙。這時锭弊,要找到一段事件記錄的代碼會變得困難,也很容易忘記添加事件記錄的代碼擂错。
你可能會想到用繼承或類別味滞,在重寫的方法里添加事件記錄的代碼。代碼可以是長的這個樣子:
@implementation UIViewController ()- (void)myViewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; // Custom code // Logging [Logging logWithEventName:NSStringFromClass([self class])];}- (void)myButtonClicked:(id)sender{ // Custom code // Logging NSString *name = [NSString stringWithFormat:@“my button in %@ is clicked”, NSStringFromClass([self class])]; [Logging logWithEventName:name];}

Logging
的代碼都很相似,通過繼承或類別重寫相關(guān)方法是可以把它從主要邏輯中剝離出來剑鞍。但同時也帶來新的問題:
你需要繼承 UIViewController
, UITableViewController
, UICollectionViewController
所有這些 ViewController 刹悴,或者給他們添加類別;
每個 ViewController 里的 ButtonClick 方法命名不可能都一樣攒暇;
你不能控制別人如何去實例化你的子類;
對于類別子房,你沒辦法調(diào)用到原來的方法實現(xiàn)形用。大多時候,我們重寫一個方法只是為了添加一些代碼证杭,而不是完全取代它田度。
如果有兩個類別都實現(xiàn)了相同的方法,運行時沒法保證哪一個類別的方法會給調(diào)用解愤。

Method Swizzling
Method Swizzling 利用 Runtime 特性把一個方法的實現(xiàn)與另一個方法的實現(xiàn)進行替換镇饺。
上一篇文章 有講到每個類里都有一個 Dispatch Table ,將方法的名字(SEL)跟方法的實現(xiàn)(IMP送讲,指向 C 函數(shù)的指針)一一對應(yīng)奸笤。Swizzle 一個方法其實就是在程序運行時在 Dispatch Table 里做點改動,讓這個方法的名字(SEL)對應(yīng)到另個 IMP 哼鬓。
首先定義一個類別监右,添加將要 Swizzled 的方法:
@implementation UIViewController (Logging)- (void)swizzled_viewDidAppear:(BOOL)animated{ // call original implementation [self swizzled_viewDidAppear:animated]; // Logging [Logging logWithEventName:NSStringFromClass([self class])];}

代碼看起來可能有點奇怪,像遞歸不是么异希。當(dāng)然不會是遞歸健盒,因為在 runtime 的時候,函數(shù)實現(xiàn)已經(jīng)被交換了称簿。調(diào)用 viewDidAppear:
會調(diào)用你實現(xiàn)的 swizzled_viewDidAppear:
扣癣,而在 swizzled_viewDidAppear:
里調(diào)用 swizzled_viewDidAppear:
實際上調(diào)用的是原來的 viewDidAppear:

接下來實現(xiàn) swizzle 的方法 :
@implementation UIViewController (Logging)void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector) { // the method might not exist in the class, but in its superclass Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); // class_addMethod will fail if original method already exists BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); // the method doesn’t exist and we just added one if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); }}

這里唯一可能需要解釋的是 class_addMethod
憨降。要先嘗試添加原 selector 是為了做一層保護父虑,因為如果這個類沒有實現(xiàn) originalSelector
,但其父類實現(xiàn)了券册,那 class_getInstanceMethod
會返回父類的方法频轿。這樣 method_exchangeImplementations
替換的是父類的那個方法,這當(dāng)然不是你想要的烁焙。所以我們先嘗試添加 orginalSelector
航邢,如果已經(jīng)存在,再用 method_exchangeImplementations
把原方法的實現(xiàn)跟新的方法實現(xiàn)給交換掉骄蝇。
最后膳殷,我們只需要確保在程序啟動的時候調(diào)用 swizzleMethod
方法。比如,我們可以在之前 UIViewController
的 Logging 類別里添加 +load:
方法赚窃,然后在 +load:
里把 viewDidAppear
給替換掉:
@implementation UIViewController (Logging)+ (void)load{ swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));}

一般情況下册招,類別里的方法會重寫掉主類里相同命名的方法。如果有兩個類別實現(xiàn)了相同命名的方法勒极,只有一個方法會被調(diào)用是掰。但 +load:
是個特例,當(dāng)一個類被讀到內(nèi)存的時候辱匿, runtime 會給這個類及它的每一個類別都發(fā)送一個 +load:
消息键痛。
其實,這里還可以更簡化點:直接用新的 IMP 取代原 IMP 匾七,而不是替換絮短。只需要有全局的函數(shù)指針指向原 IMP 就可以。
void (gOriginalViewDidAppear)(id, SEL, BOOL);void newViewDidAppear(UIViewController *self, SEL _cmd, BOOL animated) { // call original implementation gOriginalViewDidAppear(self, _cmd, animated); // Logging [Logging logWithEventName:NSStringFromClass([self class])];}+ (void)load{ Method originalMethod = class_getInstanceMethod(self, @selector(viewDidAppear:)); gOriginalViewDidAppear = (void *)method_getImplementation(originalMethod); if(!class_addMethod(self, @selector(viewDidAppear:), (IMP) newViewDidAppear, method_getTypeEncoding(originalMethod))) { method_setImplementation(originalMethod, (IMP) newViewDidAppear); }}

通過 Method Swizzling 昨忆,我們成功把邏輯代碼跟處理事件記錄的代碼解耦丁频。當(dāng)然除了 Logging ,還有很多類似的事務(wù)邑贴,如 Authentication 和 Caching席里。這些事務(wù)瑣碎,跟主要業(yè)務(wù)邏輯無關(guān)拢驾,在很多地方都有胁勺,又很難抽象出來單獨的模塊。這種程序設(shè)計問題独旷,業(yè)界也給了他們一個名字 - Cross Cutting Concerns署穗。
而像上面例子用 Method Swizzling 動態(tài)給指定的方法添加代碼,以解決 Cross Cutting Concerns 的編程方式叫:Aspect Oriented Programming
Aspect Oriented Programming (面向切面編程)
Wikipedia 里對 AOP 是這么介紹的:
An aspect can alter the behavior of the base code by applying advice (additional behavior) at various join points (points in a program) specified in a quantification or query called a pointcut (that detects whether a given join point matches).

在 Objective-C 的世界里嵌洼,這句話意思就是利用 Runtime 特性給指定的方法添加自定義代碼案疲。有很多方式可以實現(xiàn) AOP ,Method Swizzling 就是其中之一麻养。而且幸運的是褐啡,目前已經(jīng)有一些第三方庫可以讓你不需要了解 Runtime ,就能直接開始使用 AOP 鳖昌。
Aspects 就是一個不錯的 AOP 庫备畦,封裝了 Runtime , Method Swizzling 這些黑色技巧许昨,只提供兩個簡單的API:

  • (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error;- (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error;

使用 Aspects 提供的 API懂盐,我們之前的例子會進化成這個樣子:
@implementation UIViewController (Logging)+ (void)load{ [UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) { NSString *className = NSStringFromClass([[aspectInfo instance] class]); [Logging logWithEventName:className]; } error:NULL];}

你可以用同樣的方式在任何你感興趣的方法里添加自定義代碼,比如 IBAction 的方法里糕档。更好的方式莉恼,你提供一個 Logging 的配置文件作為唯一處理事件記錄的地方:
@implementation AppDelegate (Logging)+ (void)setupLogging{ NSDictionary *config = @{ @"MainViewController": @{ GLLoggingPageImpression: @"page imp - main page", GLLoggingTrackedEvents: @[ @{ GLLoggingEventName: @"button one clicked", GLLoggingEventSelectorName: @"buttonOneClicked:", GLLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) { [Logging logWithEventName:@"button one clicked"]; }, }, @{ GLLoggingEventName: @"button two clicked", GLLoggingEventSelectorName: @"buttonTwoClicked:", GLLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) { [Logging logWithEventName:@"button two clicked"]; }, }, ], }, @"DetailViewController": @{ GLLoggingPageImpression: @"page imp - detail page", } }; [AppDelegate setupWithConfiguration:config];}+ (void)setupWithConfiguration:(NSDictionary *)configs{ // Hook Page Impression [UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) { NSString *className = NSStringFromClass([[aspectInfo instance] class]); [Logging logWithEventName:className]; } error:NULL]; // Hook Events for (NSString *className in configs) { Class clazz = NSClassFromString(className); NSDictionary *config = configs[className]; if (config[GLLoggingTrackedEvents]) { for (NSDictionary *event in config[GLLoggingTrackedEvents]) { SEL selekor = NSSelectorFromString(event[GLLoggingEventSelectorName]); AspectHandlerBlock block = event[GLLoggingEventHandlerBlock]; [clazz aspect_hookSelector:selekor withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) { block(aspectInfo); } error:NULL]; } } }}

然后在 -application:didFinishLaunchingWithOptions:
里調(diào)用 setupLogging

  • (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [self setupLogging]; return YES;}

最后的話
利用 objective-C Runtime 特性和 Aspect Oriented Programming ,我們可以把瑣碎事務(wù)的邏輯從主邏輯中分離出來,作為單獨的模塊俐银。它是對面向?qū)ο缶幊棠J降囊粋€補充尿背。Logging 是個經(jīng)典的應(yīng)用,這里做個拋磚引玉捶惜,發(fā)揮想象力田藐,可以做出其他有趣的應(yīng)用。
使用 Aspects 完整的例子可以從這里獲得:AspectsDemo吱七。
如果你有什么問題和想法坞淮,歡迎留言或者發(fā)郵件給我 peng@glowing.com 進行討論。
Reference
method-swizzling
method replacement for fun and profit
Aspects

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末陪捷,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子诺擅,更是在濱河造成了極大的恐慌市袖,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烁涌,死亡現(xiàn)場離奇詭異苍碟,居然都是意外死亡,警方通過查閱死者的電腦和手機撮执,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門微峰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人抒钱,你說我怎么就攤上這事蜓肆。” “怎么了谋币?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵仗扬,是天一觀的道長。 經(jīng)常有香客問我蕾额,道長早芭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任诅蝶,我火速辦了婚禮退个,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘调炬。我一直安慰自己语盈,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布缰泡。 她就那樣靜靜地躺著黎烈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上照棋,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天资溃,我揣著相機與錄音,去河邊找鬼烈炭。 笑死溶锭,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的符隙。 我是一名探鬼主播趴捅,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼霹疫!你這毒婦竟也來了拱绑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤丽蝎,失蹤者是張志新(化名)和其女友劉穎猎拨,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體屠阻,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡红省,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了国觉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吧恃。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖麻诀,靈堂內(nèi)的尸體忽然破棺而出痕寓,到底是詐尸還是另有隱情,我是刑警寧澤蝇闭,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布厂抽,位于F島的核電站,受9級特大地震影響丁眼,放射性物質(zhì)發(fā)生泄漏筷凤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一苞七、第九天 我趴在偏房一處隱蔽的房頂上張望藐守。 院中可真熱鬧,春花似錦蹂风、人聲如沸卢厂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽慎恒。三九已至任内,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間融柬,已是汗流浹背死嗦。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留粒氧,地道東北人越除。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像外盯,于是被迫代替她去往敵國和親摘盆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359

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

  • 07 JANUARY 2015 上一篇介紹了Objective-C Messaging饱苟。利用 Objective-...
    幻世神碼閱讀 330評論 1 0
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉孩擂,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,725評論 0 9
  • 前言 近期前端移動組因項目需求,需要在用戶行為上進行打點統(tǒng)計箱熬,但由于部分早期SDK在初始設(shè)計時并未考慮到日志記錄這...
    點融黑幫閱讀 666評論 2 1
  • 我們常常會聽說 Objective-C 是一門動態(tài)語言类垦,那么這個「動態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,199評論 0 7
  • 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,568評論 33 466