iOS黑魔法-Method Swizzling(全局hook,行爲統計)


轉自:http://www.jianshu.com/p/ff19c04b34d0

需求

就拿我們公司項目來說吧,我們公司是做導航的,而且項目規模比較大,各個控制器功能都已經實現。突然有一天老大過來,說我們要在所有頁面添加統計功能,也就是用戶進入這個頁面就統計一次。我們會想到下面的一些方法:

手動添加

直接簡單粗暴的在每個控制器中加入統計,複製、粘貼、複製、粘貼...
上面這種方法太Low了,消耗時間而且以後非常難以維護,會讓後面的開發人員罵死的。

繼承

我們可以使用OOP的特性之一,繼承的方式來解決這個問題。創建一個基類,在這個基類中添加統計方法,其他類都繼承自這個基類。

然而,這種方式修改還是很大,而且定製性很差。以後有新人加入之後,都要囑咐其繼承自這個基類,所以這種方式並不可取。

Category

我們可以爲UIViewController建一個Category,然後在所有控制器中引入這個Category。當然我們也可以添加一個PCH文件,然後將這個Category添加到PCH文件中。

我們創建一個Category來覆蓋系統方法,系統會優先調用Category中的代碼,然後在調用原類中的代碼。

我們可以通過下面的這段僞代碼來看一下:

#import "UIViewController+EventGather.h"
@implementation UIViewController (EventGather)
- (void)viewDidLoad {
   NSLog(@"頁面統計:%@", self);
}
@end
Method Swizzling

我們可以使用蘋果的“黑魔法”Method SwizzlingMethod Swizzling本質上就是對IMPSEL進行交換。

Method Swizzling原理

Method Swizzing是發生在運行時的,主要用於在運行時將兩個Method進行交換,我們可以將Method Swizzling代碼寫到任何地方,但是隻有在這段Method Swilzzling代碼執行完畢之後互換才起作用。

而且Method Swizzling也是iOSAOP(面相切面編程)的一種實現方式,我們可以利用蘋果這一特性來實現AOP編程。

首先,讓我們通過兩張圖片來了解一下Method Swizzling的實現原理

圖一

圖二

上面圖一中selector2原本對應着IMP2,但是爲了更方便的實現特定業務需求,我們在圖二中添加了selector3IMP3,並且讓selector2指向了IMP3,而selector3則指向了IMP2,這樣就實現了“方法互換”。

OC語言的runtime特性中,調用一個對象的方法就是給這個對象發送消息。是通過查找接收消息對象的方法列表,從方法列表中查找對應的SEL,這個SEL對應着一個IMP(一個IMP可以對應多個SEL),通過這個IMP找到對應的方法調用。

在每個類中都有一個Dispatch Table,這個Dispatch Table本質是將類中的SELIMP(可以理解爲函數指針)進行對應。而我們的Method Swizzling就是對這個table進行了操作,讓SEL對應另一個IMP


Method Swizzling使用

在實現Method Swizzling時,核心代碼主要就是一個runtime的C語言API:
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) 
 __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
實現思路

就拿上面我們說的頁面統計的需求來說吧,這個需求在很多公司都很常見,我們下面的Demo就通過Method Swizzling簡單的實現這個需求。

我們先給UIViewController添加一個Category,然後在Category中的+(void)load方法中添加Method Swizzling方法,我們用來替換的方法也寫在這個Category中。由於load類方法是程序運行時這個類被加載到內存中就調用的一個方法,執行比較早,並且不需要我們手動調用。而且這個方法具有唯一性,也就是隻會被調用一次,不用擔心資源搶奪的問題。

定義Method Swizzling中我們自定義的方法時,需要注意儘量加前綴,以防止和其他地方命名衝突,Method Swizzling的替換方法命名一定要是唯一的,至少在被替換的類中必須是唯一的。

#import "UIViewController+swizzling.h"
#import <objc/runtime.h>
@implementation UIViewController (swizzling)

+ (void)load {
    // 通過class_getInstanceMethod()函數從當前對象中的method list獲取method結構體,如果是類方法就使用class_getClassMethod()函數獲取。
    Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
    Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad));
    /**
     *  我們在這裏使用class_addMethod()函數對Method Swizzling做了一層驗證,如果self沒有實現被交換的方法,會導致失敗。
     *  而且self沒有交換的方法實現,但是父類有這個方法,這樣就會調用父類的方法,結果就不是我們想要的結果了。
     *  所以我們在這裏通過class_addMethod()的驗證,如果self實現了這個方法,class_addMethod()函數將會返回NO,我們就可以對其進行交換了。
     */
    if (!class_addMethod([self class], @selector(swizzlingViewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
        method_exchangeImplementations(fromMethod, toMethod);
    }
}

// 我們自己實現的方法,也就是和self的viewDidLoad方法進行交換的方法。
- (void)swizzlingViewDidLoad {
    NSString *str = [NSString stringWithFormat:@"%@", self.class];
    // 我們在這裏加一個判斷,將系統的UIViewController的對象剔除掉
    if(![str containsString:@"UI"]){
        NSLog(@"統計打點 : %@", self.class);
    }
    [self swizzlingViewDidLoad];
}
@end

看到上面的代碼,肯定有人會問:樓主,你太粗心了,你在swizzlingViewDidLoad方法中又調用了[self swizzlingViewDidLoad];,這難道不會產生遞歸調用嗎?
答:然而....並不會

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章