公司上一個版本 x.x.x 的 app 發布之后,OOM(Out of Memory) free seesion 的值直線下降。目前維持在 73% 左右。一直在思考為什么這
個值下降的這么嚴重。直到發現這篇文章,資料1。意識到是 UIWebView 的內存泄漏導致的。
先說一下為什么之前 UIWebView 內存泄漏但是 fabric 顯示的 OOM free session 值卻并不低。
iOS 8 設備有兩個奇怪的 bug
第一個 bug 已經在 iOS 9 上面解決了(在 iOS 8 設備上,如果您使用了 documentView.webView.mainFrame.javaScriptContext 獲取了網頁的 JSContext,通過頻繁的進入和退出網頁界面,這個 crash 是畢現的)。但是第二 bug iOS 10 也還存在。
關于這兩個 bug 網上的資料都不多。從搜到的僅有的資料來看,后者很可能和 UIWebView 的內存泄漏有關。至少從 crash 堆棧來看,OC 運行時找不到轉發的類。
所以我的想法就很簡單了。直接干涉 UIWebView delegate 的方法轉發。基于 NSProxy 可以很好的實現一個代理 delegate。具體實現可以看這個 Smart Proxy Delegation,或者有興趣可以直接看 AsyncDisplay 和 YYKit 的源代碼,里面都有具體的實現,AsyncDisplay 的源代碼中注釋很豐富,強烈推薦。
換了這種方式之后,這兩個 bug 導致的 crash 就再也沒出現了(意外的也修復好了在 iOS 8 上的 JavaScriptCore crash)。
但是 OOM free session 值卻開始直線下滑了,到現在穩定在 73% 左右。聯想到之前這兩個 crash 占比達到 80% 左右,我猜想之前的 crash 都轉換到了 OOM 上了。
由于 UIWebView 內存泄漏實在太過嚴重,我決定切換到 WKWebView。
如何兼容 iOS 7
切換到 WKWebView 遇到的第一個問題是怎么同時也支持 iOS 7,這是我們的產品經理要求的。
先說一點題外話,蘋果設備默認是支持老版本的系統安裝老版本的 app 的。也就是說如果您的 app 之前是支持 iOS 7 的,但是下一個版本只支持最低 iOS 8了,那么一個 iOS 7 的用戶去 App Store 下載您的 app 的時候,蘋果會自動彈窗提醒用戶將會安裝該 app 的支持 iOS 7的老版本。
也就是說只支持最低 iOS 8設備并不會讓你損失 iOS 7 的用戶,如果您之前是支持 iOS 7的話。
但是萬一真的想支持 iOS 7同時又能實現新的 iOS 特性怎么辦?兩個步驟:
在項目配置的 build phases 中添加你需要的 framework(比如 WebKit.framework),如果該 framework 不支持您的最低 iOS 版本要求,設置 framework 的 status 為 weak
-
當你需要用到該 framework 的類的時候(比如 WKWebView),首先在實現文件中導入 framework 的主頭文件
#import <WebKit/WebKit.h>
當你要生成 WKWebView 對象的時候,先判斷類是否存在,再決定是用 WKWebView 還是 UIWebView
id webView = nil; if([WKWebView class]) { webView = [WKWebView new]; } else { webView = [UIWebView new]; }
同時在實現文件中實現 WKWebView 和 UIWebView 的 delegate。實際運行的時候,運行時會調用正確的 delegate 方法(WKWebView 或者 UIWebView)。
如何使用 WebView
這個比較簡單,我說一下一些有用的實現方式:
KVO 監聽
estimatedProgress
來設置進度條的值KVO 監聽
title
來設置比如說導航欄標題通常設置 webView(無論是 UIWebView 還是 WKWebView) 的 delegate 的時候我會用 proxy 進行封裝,見資料2
在 UIWebView 里面,我會是用 JavaScriptCore 來讓頁面的 JS 調用 OC 本地方法
-
在 WKWebView 里面,我會用 WKWebViewConfiguration 添加一個腳本處理對象給 WKUserContentController,頁面 JS 可以通過調用
window.webkit.messageHandlers.<your handler name>.postMessage({data: data, id: handle});
將數據傳給客戶端本地。代碼如下:WKWebViewConfiguration *config = [WKWebViewConfiguration new]; config.userContentController = [WKUserContentController new]; [config.userContentController addScriptMessageHandler:<Your Handler Object> name:@"<your handler Name>"]; WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0,0,0,0) configuration:config];
然后在<Your Handler Object>上實現 WKScriptMessageHandler ,處理 js 傳過來的數據。參見資料3
處理 Cookie
UIWebView 的 cookie 和 NSHTTPCookieStorage 的單例對象是互通的。所以 UIWebView 的 cookie 基本上處理比較簡單。NSHTTPCookieStorage 的單例對象會自動將 cookie 添加到相應的 UIWebView 的 request 上。每次加載完頁面以后 UIWebView 上生成的 cookie 也會自動保存到 NSHTTPCookieStorage 單例對象。
WKWebView 的 cookie 處理起來就很麻煩了。為了將 NSHTTPCookieStorage 單例對象的 cookie 設置到 WKWebView,需要在 loadRequest 的時候將 cookie 設置到 request 的 http 頭上。代碼如下:
- (void)loadRequest:(NSURLRequest *)req {
NSMutableURLRequest *request = req.mutableCopy;
NSString *urlString = request.URL.absoluteString;
if (urlString && [urlString rangeOfString:@"www.xxx.com"].location != NSNotFound) {
NSHTTPCookie *cookie = // specific cookie to be set;
NSArray* cookies = [NSArray arrayWithObjects: cookie, nil];
NSDictionary * headers = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
[request setAllHTTPHeaderFields:headers];
}
[(WKWebView *)self.wkWebView loadRequest:request];
}
如果有 AJAX 的 request 的話需要設置腳本(原理就是將本地 cookie 通過 js 設置到頁面的 document.cookie):
WKUserContentController* userContentController = WKUserContentController.new;
WKUserScript * cookieScript = [[WKUserScript alloc]
initWithSource: @"document.cookie = 'TeskCookieKey1=TeskCookieValue1';document.cookie = 'TeskCookieKey2=TeskCookieValue2';"
injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
// again, use stringWithFormat: in the above line to inject your values programmatically
[userContentController addUserScript:cookieScript];
WKWebViewConfiguration* webViewConfig = WKWebViewConfiguration.new;
webViewConfig.userContentController = userContentController;
WKWebView * webView = [[WKWebView alloc] initWithFrame:CGRectMake(/*set your values*/) configuration:webViewConfig];
由于采用了新的實現機制,WKWebView 獲取的 cookie 不會被設置到 NSHTTPCookieStorage 單例對象上。如果想要獲取 WKWebView 上面的 cookie,同樣可以是用 document.cookie。但是這種方式無法獲取到 httpOnly 的 cookie。為了獲取到所有的 WKWebView 上的 cookie。可以使用 NSURLProtocol,在 NSURLProtocol 得到調用的時候去取所有的相關 cookie。但是 WKWebView 默認并不走 NSURLProtocol,需要使用私有 API:
Class cls = NSClassFromString(@"WKBrowsingContextController");
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([(id)cls respondsToSelector:sel]) {
// 把 http 和 https 請求交給 NSURLProtocol 處理
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[(id)cls performSelector:sel withObject:@"http"];
[(id)cls performSelector:sel withObject:@"https"];
#pragma clang diagnostic pop
}
[NSURLProtocol registerClass:[CustomURLProtocol class]];
原理請看讓 WKWebView 支持 NSURLProtocol