Method Swizzling 和 AOP 實踐

07 JANUARY 2015

上一篇介紹了Objective-C Messaging藏澳。利用 Objective-C 的 Runtime 特性仁锯,我們可以給語言做擴展,幫助解決項目開發(fā)中的一些設(shè)計和技術(shù)問題翔悠。這一篇业崖,我們來探索一些利用 Objective-C Runtime 的黑色技巧。這些技巧中最具爭議的或許就是 Method Swizzling 蓄愁。

介紹一個技巧双炕,最好的方式就是提出具體的需求,然后用它跟其他的解決方法做比較撮抓。

所以妇斤,先來看看我們的需求:對 App 的用戶行為進行追蹤和分析。簡單說丹拯,就是當(dāng)用戶看到某個View或者點擊某個Button的時候站超,就把這個事件記下來。

手動添加

最直接粗暴的方式就是在每個viewDidAppear里添加記錄事件的代碼乖酬。

@implementationMyViewController()- (void)viewDidAppear:(BOOL)animated{? ? [superviewDidAppear: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的代碼生宛。這時,要找到一段事件記錄的代碼會變得困難肮柜,也很容易忘記添加事件記錄的代碼陷舅。

你可能會想到用繼承或類別,在重寫的方法里添加事件記錄的代碼素挽。代碼可以是長的這個樣子:

@implementationUIViewController()- (void)myViewDidAppear:(BOOL)animated{? ? [superviewDidAppear:animated];// Custom code// Logging[Logging logWithEventName:NSStringFromClass([selfclass])];}- (void)myButtonClicked:(id)sender{// Custom code// LoggingNSString*name = [NSStringstringWithFormat:@“my buttonin%@ is clicked”, NSStringFromClass([selfclass])];? ? [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 的方法:

@implementationUIViewController(Logging)- (void)swizzled_viewDidAppear:(BOOL)animated{// call original implementation[selfswizzled_viewDidAppear:animated];// Logging[Logging logWithEventName:NSStringFromClass([selfclass])];}

代碼看起來可能有點奇怪幢哨,像遞歸不是么。當(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 的方法 :

@implementationUIViewController (Logging)voidswizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector){// the method might not exist in the class, but in its superclassMethod originalMethod = class_getInstanceMethod(class,originalSelector);? ? Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);// class_addMethod will fail if original method already existsBOOL didAddMethod = class_addMethod(class,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));// the method doesn’t exist and we just added oneif(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給替換掉:

@implementationUIViewController(Logging)+ (void)load{? ? swizzleMethod([selfclass],@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);voidnewViewDidAppear(UIViewController*self, SEL _cmd,BOOLanimated)? {// call original implementationgOriginalViewDidAppear(self, _cmd, animated);// Logging[Logging logWithEventName:NSStringFromClass([selfclass])];}+ (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)aspect_hookSelector:(SEL)selectorwithOptions:(AspectOptions)optionsusingBlock:(id)blockerror:(NSError**)error;-(id)aspect_hookSelector:(SEL)selectorwithOptions:(AspectOptions)optionsusingBlock:(id)blockerror:(NSError**)error;

使用 Aspects 提供的 API,我們之前的例子會進化成這個樣子:

@implementationUIViewController(Logging)+ (void)load{? ? [UIViewControlleraspect_hookSelector:@selector(viewDidAppear:)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? withOptions:AspectPositionAfter? ? ? ? ? ? ? ? ? ? ? ? ? ? ? usingBlock:^(id aspectInfo) {NSString*className = NSStringFromClass([[aspectInfo instance] class]);? ? ? ? [Logging logWithEventName:className];? ? ? ? ? ? ? ? ? ? ? ? ? ? ? } error:NULL];}

你可以用同樣的方式在任何你感興趣的方法里添加自定義代碼奸披,比如 IBAction 的方法里昏名。更好的方式,你提供一個 Logging 的配置文件作為唯一處理事件記錄的地方:

@implementationAppDelegate(Logging)+ (void)setupLogging{NSDictionary*config = @{@"MainViewController": @{? ? ? ? ? ? GLLoggingPageImpression:@"page imp - main page",? ? ? ? ? ? GLLoggingTrackedEvents: @[? ? ? ? ? ? ? ? @{? ? ? ? ? ? ? ? ? ? GLLoggingEventName:@"button one clicked",? ? ? ? ? ? ? ? ? ? GLLoggingEventSelectorName:@"buttonOneClicked:",? ? ? ? ? ? ? ? ? ? GLLoggingEventHandlerBlock: ^(id aspectInfo) {? ? ? ? ? ? ? ? ? ? ? ? [Logging logWithEventName:@"button one clicked"];? ? ? ? ? ? ? ? ? ? },? ? ? ? ? ? ? ? },? ? ? ? ? ? ? ? @{? ? ? ? ? ? ? ? ? ? GLLoggingEventName:@"button two clicked",? ? ? ? ? ? ? ? ? ? GLLoggingEventSelectorName:@"buttonTwoClicked:",? ? ? ? ? ? ? ? ? ? GLLoggingEventHandlerBlock: ^(id aspectInfo) {? ? ? ? ? ? ? ? ? ? ? ? [Logging logWithEventName:@"button two clicked"];? ? ? ? ? ? ? ? ? ? },? ? ? ? ? ? ? ? },? ? ? ? ? ],? ? ? ? },@"DetailViewController": @{? ? ? ? ? ? GLLoggingPageImpression:@"page imp - detail page",? ? ? ? }? ? };? ? [AppDelegate setupWithConfiguration:config];}+ (void)setupWithConfiguration:(NSDictionary*)configs{// Hook Page Impression[UIViewControlleraspect_hookSelector:@selector(viewDidAppear:)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? withOptions:AspectPositionAfter? ? ? ? ? ? ? ? ? ? ? ? ? ? ? usingBlock:^(id aspectInfo) {NSString*className = NSStringFromClass([[aspectInfo instance] class]);? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? [Logging logWithEventName:className];? ? ? ? ? ? ? ? ? ? ? ? ? ? ? } error:NULL];// Hook Eventsfor(NSString*classNameinconfigs) {? ? ? ? Class clazz = NSClassFromString(className);NSDictionary*config = configs[className];if(config[GLLoggingTrackedEvents]) {for(NSDictionary*eventinconfig[GLLoggingTrackedEvents]) {? ? ? ? ? ? ? ? SEL selekor = NSSelectorFromString(event[GLLoggingEventSelectorName]);? ? ? ? ? ? ? ? AspectHandlerBlock block = event[GLLoggingEventHandlerBlock];? ? ? ? ? ? ? ? [clazz aspect_hookSelector:selekor? ? ? ? ? ? ? ? ? ? ? ? ? ? ? withOptions:AspectPositionAfter? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? usingBlock:^(id aspectInfo) {? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? block(aspectInfo);? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? } error:NULL];? ? ? ? ? ? }? ? ? ? }? ? }}

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

- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {// Override point for customization after application launch.[selfsetupLogging];returnYES;}

最后的話

利用 objective-C Runtime 特性和 Aspect Oriented Programming 阵面,我們可以把瑣碎事務(wù)的邏輯從主邏輯中分離出來轻局,作為單獨的模塊。它是對面向?qū)ο缶幊棠J降囊粋€補充样刷。Logging 是個經(jīng)典的應(yīng)用仑扑,這里做個拋磚引玉,發(fā)揮想象力颂斜,可以做出其他有趣的應(yīng)用夫壁。

使用 Aspects 完整的例子可以從這里獲得:AspectsDemo拾枣。

如果你有什么問題和想法沃疮,歡迎留言或者發(fā)郵件給我 peng@glowing.com 進行討論。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末梅肤,一起剝皮案震驚了整個濱河市司蔬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌姨蝴,老刑警劉巖俊啼,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異左医,居然都是意外死亡授帕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門浮梢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來跛十,“玉大人,你說我怎么就攤上這事秕硝〗嬗常” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵远豺,是天一觀的道長奈偏。 經(jīng)常有香客問我,道長躯护,這世上最難降的妖魔是什么惊来? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮棺滞,結(jié)果婚禮上裁蚁,老公的妹妹穿的比我還像新娘内狸。我一直安慰自己,他們只是感情好厘擂,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布昆淡。 她就那樣靜靜地躺著,像睡著了一般刽严。 火紅的嫁衣襯著肌膚如雪昂灵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天舞萄,我揣著相機與錄音眨补,去河邊找鬼。 笑死倒脓,一個胖子當(dāng)著我的面吹牛撑螺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播崎弃,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼甘晤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了饲做?” 一聲冷哼從身側(cè)響起线婚,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎盆均,沒想到半個月后塞弊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡泪姨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年游沿,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肮砾。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡诀黍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出唇敞,到底是詐尸還是另有隱情蔗草,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布疆柔,位于F島的核電站咒精,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏旷档。R本人自食惡果不足惜模叙,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鞋屈。 院中可真熱鬧范咨,春花似錦故觅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至替蛉,卻和暖如春贯溅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背躲查。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工它浅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人镣煮。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓姐霍,卻偏偏與公主長得像,于是被迫代替她去往敵國和親典唇。 傳聞我的和親對象是個殘疾皇子镊折,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

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

  • 轉(zhuǎn)自:http://tech.glowing.com/cn/method-swizzling-aop/上一篇介紹了...
    反調(diào)唱唱閱讀 367評論 1 1
  • 轉(zhuǎn)自 http://tech.glowing.com/cn/method-swizzling-aop/上一篇介紹了...
    ClarkWang_001閱讀 409評論 0 3
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,679評論 0 9
  • 我們常常會聽說 Objective-C 是一門動態(tài)語言蚓聘,那么這個「動態(tài)」表現(xiàn)在哪呢腌乡?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,174評論 0 7
  • 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,544評論 33 466