【iOS】黑魔法 Method Swizzling 及對代理方法的 hook

iOS的runtime中有一種神奇的黑魔法: Method Swizzling,利用它可以做很多有趣的事情。

Method Swizzling 的優(yōu)點

例如,假設(shè)我們的項目中有這么一個需求,需要在用戶進入每個頁面時進行埋點。那么我們不外乎有如下幾種處理方法:
1、為每個頁面的 viewWillAppear 方法都添加上埋點操作;
2、在項目中構(gòu)造一個 UIViewController 的基類,在基類的 viewWillAppear 方法中實現(xiàn)埋點操作,項目中其他的 UIViewController 都繼承自此基類;
3、為 UIViewController 添加分類,在分類中使用 Method Swizzling 對 viewWillAppear 方法進行 hook,添加埋點操作;

接下來我們依次分析每種方法的利弊。方法1等于是將相同的代碼來回復(fù)制粘貼,不僅耗時,而且代碼重復(fù)度過高;方法2相來來說無疑是優(yōu)秀了很多,極大減少了代碼量,不過這種方案對項目原有代碼影響較大,比較適用于項目初期,如果項目已經(jīng)進行到一定程度,替換基類也會是一個不小的工作量。而方法3不僅代碼量少,而且對項目原有代碼的影響小,稱得上是最佳方案。

簡單的hook

接下來我們先來講講如何對 viewWillAppear 方法進行hook。代碼如下:

#import "UIViewController+Tracking.h"
#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originalMethod = class_getInstanceMethod(self, @selector(viewWillAppear:));
        Method swizzledMethod = class_getInstanceMethod(self, @selector(hook_viewWillAppear:));
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

- (void)hook_viewWillAppear:(BOOL)animated
{
    NSLog(@"開始埋點");
    [self hook_viewWillAppear:animated];
}

@end

load 方法是在整個文件被添加到 runtime 時開始執(zhí)行,父類最先執(zhí)行,然后是子類,最后是 Category,所以我們把 hook 代碼寫在 load 方法里,可以保證在類首次被調(diào)用前我們 hook 掉的方法已經(jīng)變成了我們所想要的實現(xiàn)。dispatch_once 這個 GCD 塊在這里的作用是保證這份代碼在程序運行過程中只會執(zhí)行一次。在 hook_viewWillAppear 這個方法中,我們可以實現(xiàn)我們需求中的埋點功能,最后再調(diào)用原始的方法實現(xiàn)。這里需要注意的是,由于方法交換已經(jīng)生效,所以我們最后的[self hook_viewWillAppear:animated]實際上就是在調(diào)用原始方法實現(xiàn)。

對代理方法進行 hook

以上是利用 Method Swizzling 對方法進行 hook 的步驟。但是我們在開發(fā)過程中可能還會遇到一些特殊的情況,需要對代理方法進行 hook,這時候又該怎么做呢?下面我用一個例子來講解一下。假如我們有一個需求,需要在工程中監(jiān)控所有 UIWebview 中的請求,當發(fā)現(xiàn)請求鏈接中含有某個域名時將該 url 上報給服務(wù)端。這時我們第一時間想到的一定就是在項目里所有 UIWebView 的代理方法中去攔截,但是如果需求中所指的網(wǎng)頁包含我們所使用的 SDK 呢, 又或者說假如我們是 SDK 的提供方,想要監(jiān)控使用我們 SDK 的 app 中的 UIWebView 的網(wǎng)絡(luò)請求呢?這時,我們想要拿到所有 UIWebView 的代理方法似乎成為了不可能。這個時候就到了 Method Swizzling 發(fā)揮作用的時刻了。

至于如何實現(xiàn),這里先貼上代碼供大家參考。

#import "UIWebView+Tracking.h"
#import <objc/runtime.h>

@implementation UIWebView (Tracking)

#pragma mark - load

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method oldMethod = class_getInstanceMethod([self class], @selector(setDelegate:));
        Method newMethod = class_getInstanceMethod([self class], @selector(hook_setDelegate:));
        method_exchangeImplementations(oldMethod, newMethod);
    });
}

#pragma mark - 交換代理方法

- (void)hook_setDelegate:(id<UIWebViewDelegate>)delegate
{
    SEL oldSelector = @selector(webView:shouldStartLoadWithRequest:navigationType:);
    SEL newSelector = @selector(hook_webView:shouldStartLoadWithRequest:navigationType:);
    Method oldMethod_del = class_getInstanceMethod([delegate class], oldSelector);
    Method oldMethod_self = class_getInstanceMethod([self class], oldSelector);
    Method newMethod = class_getInstanceMethod([self class], newSelector);

    // 若未實現(xiàn)代理方法,則先添加代理方法    
    BOOL isSuccess = class_addMethod([delegate class], oldSelector, class_getMethodImplementation([self class], newSelector), method_getTypeEncoding(newMethod));
    if (isSuccess) {
        class_replaceMethod([delegate class], newSelector, class_getMethodImplementation([self class], oldSelector), method_getTypeEncoding(oldMethod_self));
    } else {
        // 若已實現(xiàn)代理方法,則添加 hook 方法并進行交換
        BOOL isVictory = class_addMethod([delegate class], newSelector, class_getMethodImplementation([delegate class], oldSelector), method_getTypeEncoding(oldMethod_del));
        if (isVictory) {
            class_replaceMethod([delegate class], oldSelector, class_getMethodImplementation([self class], newSelector), method_getTypeEncoding(newMethod));
        }
    }
    [self hook_setDelegate:delegate];
}

#pragma mark - 交換的方法

// 原始方法實現(xiàn)
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    return YES;
}

// hook后的方法
- (BOOL)hook_webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    if ([request.URL.host rangeOfString:@"baidu.com"].location != NSNotFound) {
        NSLog(@"攔截到的url為 : %@", request.URL);
    }
    return [self hook_webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
}

這里我們是在設(shè)置代理時拿到 delegate 所屬的類,隨后對該類中的- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType方法進行 hook。

問題

要使用該方法有一個前提就是 UIWebView 必須設(shè)置了代理,否則無法確定代理方法所在的類,也就無法進行 hook 了。這里我暫時還沒有想到更好的辦法,如果各位讀者想到了更好的方法歡迎一起討論。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容