之前寫過一篇文章是關於基於NSURLProtocol做的DNS解析,其中對NSURLProtocol也有了簡單的介紹,我們都知道他可以攔截所有基於URL Loading System 中的請求,但是對於WKWebview裡面所發出的請求即使他是http/https 也無能為力,先來簡單的瞭解下WKWebView.
WKWebview
iOS8以後,蘋果推出了新框架Webkit,提供了替換UIWebView的元件WKWebView。各種UIWebView的問題沒有了,速度更快了,佔用記憶體少了,一句話,WKWebView是App內部載入網頁的最佳選擇!我們做開發最關係的是記憶體問題,基本上網上所有的資料都在說WKWebview的記憶體佔用會更少,但是到底少了多少我這邊做了下測試,同樣是載入163的首頁
從上圖看出記憶體大概能優化百分之八十左右,而且從網頁的滑動上也確實有所改善。這麼明顯的效能提升但是蘋果並沒有完全放棄UIWebView也一定有他的道理,就拿本文要講的NSURLProtocol攔截請求來說,WKWebview的相容並不UIWebView好,還需要開發者做一些操作。
WebKit原始碼分析
由於WKWebview是基於webkit核心來做的,所以我們在使用的時候需要匯入一個這樣的東西。
#import <WebKit/WebKit.h>複製程式碼
通過這個我們可以猜到WKWebview中所有的請求以及一些邏輯肯定走的都是webkit裡面的東西,所以他對於網頁的載入之之類的操作也不會走系統本省的URL Loading System,這麼說來他的請求不能被NSURLProtocol攔截也是理所當然的了。不過WKWebview是否真的和NSURLProtocol一點關係都沒有還需要去研究,幸好webkit是開源的,github上很容易找到原始碼(大小大概是1G多點的zip,花了我將近一天時間來看)。拉下程式碼直接搜尋NSURLProtocol,看看有沒有有關的資訊
看來的確是有和NSURLProtocol有關係,後面通過斷點的呼叫棧中也找到了
+ [NSURLProtocol canInitWithRequest:]複製程式碼
這樣的字樣,再通過網上查一些資料也證實了我的猜想,其實WKWebview在一開始時候是會呼叫到NSURLProtocol中的入口方法canInitWithRequest的,但是就沒有然後了,也就是說WKWebview是和NSURLProtocol有一定關聯,只是在NSURLProtocol的入口處返回NO所以導致NSURLProtocol不接管WKWebview的請求。我們點進webkit原始碼中的CustomProtocol可以看到,整體的結構我們都差不多,但是我注意到每個CustomProtocol的入口函式都有這樣一個判斷:
(粉色的可以暫時認定為是它內部的一個custom字串)通過這個可以猜想,WKWebview並不是不走NSURLProtocol,而是需要滿足他的一個規則,他才會在入口函式這裡返回YES來給你放行,這個規則便是你所請求的URL的Scheme要和它內部配置的CustomScheme相同。不過這裡有一個疑問,蘋果在使用webkit時候為什麼會把http/https這樣大眾化的scheme過濾掉,看來他是不建議開發者來使用NSURLProtocol。接下來我們來看這個CustomScheme,既然蘋果內部規定好的那麼一定能通過某種方式來註冊一個自己的scheme,實在不行就hook嘛。通過翻他的原始碼發現最終都指向一句程式碼
[WKBrowsingContextController registerSchemeForCustomProtocol:testScheme];複製程式碼
方法實現為
+ (void)registerSchemeForCustomProtocol:(NSString *)scheme
{
WebProcessPool::registerGlobalURLSchemeAsHavingCustomProtocolHandlers(scheme);
}複製程式碼
void WebProcessPool::registerGlobalURLSchemeAsHavingCustomProtocolHandlers(const String& urlScheme)
{
if (!urlScheme)
return;
globalURLSchemesWithCustomProtocolHandlers().add(urlScheme);
for (auto* processPool : allProcessPools())
processPool->registerSchemeForCustomProtocol(urlScheme);
}複製程式碼
通過方法名字可以看出這個就是那個向webkit註冊CustomScheme的方法,只要我們在註冊完我們自己的CustomProtocol之後在呼叫該方法應該就可以了。通過他的原始碼也進一步印證了我的猜想(他也是這麼寫的)
具體實施
找到了方法就要去實施,不過因為registerSchemeForCustomProtocol是WKBrowsingContextController的類方法,所以只能用WKBrowsingContextController去呼叫,但是在webkit的標頭檔案發現WKBrowsingContextController並沒有開放出來,所以我們採用NSClassFromString和NSSelectorFromString方法來拿到類和對應的方法,整體程式碼如下
//註冊自己的protocol
[NSURLProtocol registerClass:[CustomProtocol class]];
//建立WKWebview
WKWebViewConfiguration * config = [[WKWebViewConfiguration alloc] init];
WKWebView * wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height) configuration:config];
[wkWebView loadRequest:webViewReq];
[self.view addSubview:wkWebView];
//註冊scheme
Class cls = NSClassFromString(@"WKBrowsingContextController");
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([cls respondsToSelector:sel]) {
// 通過http和https的請求,同理可通過其他的Scheme 但是要滿足ULR Loading System
[cls performSelector:sel withObject:@"http"];
[cls performSelector:sel withObject:@"https"];
}複製程式碼
實現效果。我將網頁中所有的圖片替換成了柴犬圖片
#####值得注意
因為WKBrowsingContextController和registerSchemeForCustomProtocol應該是私有的所以使用時候需要對字串做下處理,用加密的方式或者其他就可以了,實測可以過稽核的。我的文章簡書和掘金同步更新