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 了。這里我暫時還沒有想到更好的辦法,如果各位讀者想到了更好的方法歡迎一起討論。