WKWebView 的使用

weixin_33924312發表於2017-07-17

WKWebView 是蘋果在 WWDC 2014 上推出的新一代 webView 元件,用以替代 UIKit 中笨重難用、記憶體洩漏的 UIWebView。WKWebView擁有60fps滾動重新整理率、和 safari 相同的 JavaScript 引擎等優勢。

簡單的適配方法本文不再贅述,主要來說說適配 WKWebView 過程中填過的坑以及善待解決的技術難題。

1、WKWebView 白屏問題

WKWebView 自詡擁有更快的載入速度,更低的記憶體佔用,但實際上 WKWebView 是一個多程式元件,Network Loading 以及 UI Rendering 在其它程式中執行。初次適配 WKWebView 的時候,我們也驚訝於開啟 WKWebView 後,App 程式記憶體消耗反而大幅下降,但是仔細觀察會發現,Other Process 的記憶體佔用會增加。在一些用 webGL 渲染的複雜頁面,使用 WKWebView 總體的記憶體佔用(App Process Memory + Other Process Memory)不見得比 UIWebView 少很多。

在 UIWebView 上當記憶體佔用太大的時候,App Process 會 crash;而在 WKWebView 上當總體的記憶體佔用比較大的時候,WebContent Process 會 crash,從而出現白屏現象。在 WKWebView 中載入下面的測試連結可以穩定重現白屏現象:

http://people.mozilla.org/~rnewman/fennec/mem.html

這個時候 WKWebView.URL 會變為 nil, 簡單的 reload 重新整理操作已經失效,對於一些長駐的H5頁面影響比較大。

我們最後的解決方案是:

A、藉助 WKNavigtionDelegate

iOS 9以後 WKNavigtionDelegate 新增了一個回撥函式:

- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webViewAPI_AVAILABLE(macosx(10.11),ios(9.0));

當 WKWebView 總體記憶體佔用過大,頁面即將白屏的時候,系統會呼叫上面的回撥函式,我們在該函式裡執行[webView reload](這個時候 webView.URL 取值尚不為 nil)解決白屏問題。在一些高記憶體消耗的頁面可能會頻繁重新整理當前頁面,H5側也要做相應的適配操作。

B、檢測 webView.title 是否為空

並不是所有H5頁面白屏的時候都會呼叫上面的回撥函式,比如,最近遇到在一個高記憶體消耗的H5頁面上 present 系統相機,拍照完畢後返回原來頁面的時候出現白屏現象(拍照過程消耗了大量記憶體,導致記憶體緊張,WebContent Process 被系統掛起),但上面的回撥函式並沒有被呼叫。在WKWebView白屏的時候,另一種現象是 webView.titile 會被置空, 因此,可以在 viewWillAppear 的時候檢測 webView.title 是否為空來 reload 頁面。

綜合以上兩種方法可以解決絕大多數的白屏問題。

2、WKWebView Cookie 問題

Cookie 問題是目前 WKWebView 的一大短板

2.1、WKWebView Cookie儲存

業界普遍認為 WKWebView 擁有自己的私有儲存,不會將 Cookie 存入到標準的 Cookie 容器NSHTTPCookieStorage中。

實踐發現 WKWebView 例項其實也會將 Cookie 儲存於 NSHTTPCookieStorage 中,但儲存時機有延遲,在iOS 8上,當頁面跳轉的時候,當前頁面的 Cookie 會寫入 NSHTTPCookieStorage 中,而在 iOS 10 上,JS 執行 document.cookie 或伺服器 set-cookie 注入的 Cookie 會很快同步到 NSHTTPCookieStorage 中,FireFox 工程師曾建議通過 reset WKProcessPool 來觸發 Cookie 同步到 NSHTTPCookieStorage 中,實踐發現不起作用,並可能會引發當前頁面 session cookie 丟失等問題。

WKWebView Cookie 問題在於 WKWebView 發起的請求不會自動帶上儲存於 NSHTTPCookieStorage 容器中的 Cookie

比如,NSHTTPCookieStorage 中儲存了一個 Cookie:

name=Nicholas;value=test;domain=y.qq.com;expires=Sat,02May201923:38:25GMT;

通過 UIWebView 發起請求http://y.qq.com,則請求頭會自動帶上 cookie: Nicholas=test;

而通過 WKWebView發起請求http://y.qq.com,請求頭不會自動帶上 cookie: Nicholas=test。

2.2、WKProcessPool

蘋果開發者文件對 WKProcessPool 的定義是:A WKProcessPool object represents a pool of Web Content process. 通過讓所有 WKWebView 共享同一個 WKProcessPool 例項,可以實現多個 WKWebView 之間共享 Cookie(session Cookie and persistent Cookie)資料。不過 WKWebView WKProcessPool 例項在 app 殺程式重啟後會被重置,導致 WKProcessPool 中的 Cookie、session Cookie 資料丟失,目前也無法實現 WKProcessPool 例項本地化儲存。

2.3、Workaround

由於許多 H5 業務都依賴於 Cookie 作登入態校驗,而 WKWebView 上請求不會自動攜帶 Cookie, 目前的主要解決方案是:

a、WKWebView loadRequest 前,在 request header 中設定 Cookie, 解決首個請求 Cookie 帶不上的問題;

WKWebView * webView = [WKWebViewnew];

NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://h5.qzone.qq.com/mqzone/index"]];

[request addValue:@"skey=skeyValue"forHTTPHeaderField:@"Cookie"];

[webView loadRequest:request];

b、通過 document.cookie 設定 Cookie 解決後續頁面(同域)Ajax、iframe 請求的 Cookie 問題;

注意:document.cookie()無法跨域設定 cookie

WKUserContentController* userContentController = [WKUserContentControllernew];

WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie = 'skey=skeyValue';"injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];

[userContentController addUserScript:cookieScript];

這種方案無法解決302請求的 Cookie 問題,比如,第一個請求是 www.a.com,我們通過在 request header 裡帶上 Cookie 解決該請求的 Cookie 問題,接著頁面302跳轉到 www.b.com,這個時候 www.b.com 這個請求就可能因為沒有攜帶 cookie 而無法訪問。當然,由於每一次頁面跳轉前都會呼叫回撥函式:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler;

可以在該回撥函式裡攔截302請求,copy request,在 request header 中帶上 cookie 並重新 loadRequest。不過這種方法依然解決不了頁面 iframe 跨域請求的 Cookie 問題,畢竟-[WKWebView loadRequest:]只適合載入 mainFrame 請求。

3、WKWebView NSURLProtocol問題

WKWebView 在獨立於 app 程式之外的程式中執行網路請求,請求資料不經過主程式,因此,在 WKWebView 上直接使用 NSURLProtocol 無法攔截請求。蘋果開源的 webKit2 原始碼暴露了私有API

+ [WKBrowsingContextController registerSchemeForCustomProtocol:]

通過註冊 http(s) scheme 後 WKWebView 將可以使用 NSURLProtocol 攔截 http(s) 請求:

Class cls = NSClassFromString(@"WKBrowsingContextController”);

SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");

if ([(id)cls respondsToSelector:sel]) {

// 註冊http(s) scheme, 把 http和https請求交給 NSURLProtocol處理

[(id)cls performSelector:sel withObject:@"http"];

[(id)cls performSelector:sel withObject:@"https"];

}

但是這種方案目前存在兩個嚴重缺陷:

a、post 請求 body 資料被清空

由於 WKWebView 在獨立程式裡執行網路請求。一旦註冊 http(s) scheme 後,網路請求將從 Network Process 傳送到 App Process,這樣 NSURLProtocol 才能攔截網路請求。在 webkit2 的設計裡使用 MessageQueue 進行程式之間的通訊,Network Process 會將請求 encode 成一個 Message,然後通過 IPC 傳送給 App Process。出於效能的原因,encode 的時候 HTTPBody 和 HTTPBodyStream 這兩個欄位被丟棄掉了

參考蘋果原始碼:

https://github.com/WebKit/webkit/blob/fe39539b83d28751e86077b173abd5b7872ce3f9/Source/WebKit2/Shared/mac/WebCoreArgumentCodersMac.mm#L61-L88(複製連結到瀏覽器中開啟)

及bug report:

https://bugs.webkit.org/show_bug.cgi?id=138169(複製連結到瀏覽器中開啟)

因此,如果通過 registerSchemeForCustomProtocol 註冊了 http(s) scheme, 那麼由 WKWebView 發起的所有 http(s)請求都會通過 IPC 傳給主程式 NSURLProtocol 處理,導致 post 請求 body 被清空

b、對ATS支援不足

測試發現一旦開啟ATS開關:Allow Arbitrary Loads 選項設定為NO,同時通過 registerSchemeForCustomProtocol 註冊了 http(s) scheme,WKWebView 發起的所有 http 網路請求將被阻塞(即便將Allow Arbitrary Loads in Web Content 選項設定為YES);

WKWebView 可以註冊 customScheme, 比如 dynamic://, 因此希望使用離線功能又不使用 post 方式的請求可以通過 customScheme 發起請求,比如 dynamic://www.dynamicalbumlocalimage.com/,然後在 app 程式 NSURLProtocol 攔截這個請求並載入離線資料。不足:使用 post 方式的請求該方案依然不適用,同時需要 H5 側修改請求 scheme 以及 CSP 規則;

4、WKWebView loadRequest 問題

在 WKWebView 上通過 loadRequest 發起的 post 請求 body 資料會丟失:

//同樣是由於程式間通訊效能問題,HTTPBody欄位被丟棄

[request setHTTPMethod:@"POST"];[request setHTTPBody:[@"bodyData"dataUsingEncoding:NSUTF8StringEncoding]];[wkwebview loadRequest: request];

workaround:

假如想通過-[WKWebView loadRequest:]載入 post 請求 request1:http://h5.qzone.qq.com/mqzone/index,可以通過以下步驟實現:

替換請求 scheme,生成新的 post 請求 request2:post://h5.qzone.qq.com/mqzone/index, 同時將 request1 的 body 欄位複製到 request2 的 header 中(WebKit 不會丟棄 header 欄位);

通過-[WKWebView loadRequest:]載入新的 post 請求 request2;

通過 +[WKBrowsingContextController registerSchemeForCustomProtocol:]註冊 scheme:post://;

註冊 NSURLProtocol 攔截請求post://h5.qzone.qq.com/mqzone/index,替換請求 scheme, 生成新的請求 request3:http://h5.qzone.qq.com/mqzone/index,將 request2 header的body 欄位複製到 request3 的 body 中,並使用 NSURLConnection 載入 request3,最後通過 NSURLProtocolClient 將載入結果返回 WKWebView;

5、WKWebView 頁面樣式問題

在 WKWebView 適配過程中,我們發現部分H5頁面元素位置向下偏移被拉伸變形,追蹤後發現主要是H5頁面高度值異常導致:

a. 空間H5頁面有透明導航、透明導航下拉重新整理、全屏等需求,因此之前 webView 整個是從(0, 0)開始佈局,通過調整webView.scrollView.contentInset來適配特殊導航欄需求。而在 WKWebView 上對 contentInset 的調整會反饋到webView.scrollView.contentSize.height的變化上,比如設定webView.scrollView.contentInset.top = a,那麼contentSize.height的值會增加a,導致H5頁面長度增加,頁面元素位置向下偏移;

解決方案是:調整WKWebView佈局方式,避免調整webView.scrollView.contentInset。實際上,即便在 UIWebView 上也不建議直接調整webView.scrollView.contentInset的值,這確實會帶來一些奇怪的問題。如果某些特殊情況下非得調整 contentInset 不可的話,可以通過下面方式讓H5頁面恢復正常顯示:

/**設定contentInset值後通過調整webView.frame讓頁面恢復正常顯示

*參考:http://km.oa.com/articles/show/277372

*/

webView.scrollView.contentInset = UIEdgeInsetsMake(a,0,0,0); webView.frame = CGRectMake(webView.frame.origin.x, webView.frame.origin.y, webView.frame.size.width, webView.frame.size.height - a);

b. 在接入 now 直播的時候,我們發現在 iOS 9 上 WKWebView 會出現頁面被拉伸變形的情況,最後發現是window.innerHeight值不準確導致(在WKWebView上返回了一個非常大的值),而H5同學通過獲取window.innerHeight來設定頁面高度,導致頁面整體被拉伸。通過查閱相關資料發現,這個bug只在 iOS 9 的幾個系統版本上出現,蘋果後來fix了這個bug。我們最後的解決方案是:延遲呼叫window.innerHeight

setTimeout(function(){height = window.innerHeight},0);

or

Use shrink-to-fit meta-tag

6、WKWebView 截圖問題

空間玩吧H5小遊戲有截圖分享的功能,WKWebView 下通過 -[CALayer renderInContext:]實現截圖的方式失效,需要通過以下方式實現截圖功能:

@implementationUIView (ImageSnapshot) - (UIImage*)imageSnapshot {     UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES,self.contentScaleFactor);     [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];     UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();     UIGraphicsEndImageContext();returnnewImage; }@end

然而這種方式依然解決不了 webGL 頁面的截圖問題,筆者已經翻遍蘋果文件,研究過 webKit2 原始碼裡的截圖私有API,依然沒有找到合適的解決方案,同時發現 Safari 以及 Chrome 這兩個全量切換到 WKWebView 的瀏覽器也存在同樣的問題:對webGL 頁面的截圖結果不是空白就是純黑圖片。無奈之下,我們只能約定一個JS介面,讓遊戲開發商實現該介面,具體是通過canvas getImageData()方法取得圖片資料後返回 base64 格式的資料,客戶端在需要截圖的時候,呼叫這個JS介面獲取 base64 String 並轉換成 UIImage。

7、WKWebView crash問題

WKWebView 放量後,外網新增了一些 crash, 其中一類 crash 的主要堆疊如下:

...28UIKit0x0000000190513360UIApplicationMain +208

29Qzone0x0000000101380570main (main.m:181)30libdyld.dylib0x00000001895205b8_dyld_process_info_notify_release +36

Completion handler passed to -[QZWebController webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:] was not called

主要是JS呼叫window.alert()函式引起的,從 crash 堆疊可以看出是 WKWebView 回撥函式:

+ (void) presentAlertOnController:(nonnull UIViewController*)parentController title:(nullable NSString*)title message:(nullable NSString *)message handler:(nonnullvoid(^)())completionHandler;

completionHandler 沒有被呼叫導致的。在適配 WKWebView 的時候,我們需要自己實現該回撥函式,window.alert()才能調起 alert 框,我們最初的實現是這樣的:

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(void))completionHandler {     UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@""message:message preferredStyle:UIAlertControllerStyleAlert];     [alertController addAction:[UIAlertAction actionWithTitle:@"確認"style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]];     [self presentViewController:alertController animated:YES completion:^{}]; }

如果 WKWebView 退出的時候,JS剛好執行了window.alert(), alert 框可能彈不出來,completionHandler 最後沒有被執行,導致 crash;另一種情況是在 WKWebView 一開啟,JS就執行window.alert(),這個時候由於 WKWebView 所在的 UIViewController 出現(push或present)的動畫尚未結束,alert 框可能彈不出來,completionHandler 最後沒有被執行,導致 crash。我們最終的實現大致是這樣的:

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(void))completionHandler {if(/*UIViewController of WKWebView has finish push or present animation*/) {         completionHandler();return;     }     UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@""message:message preferredStyle:UIAlertControllerStyleAlert];     [alertController addAction:[UIAlertAction actionWithTitle:@"確認"style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]];if(/*UIViewController of WKWebView is visible*/)         [self presentViewController:alertController animated:YES completion:^{}];elsecompletionHandler(); }

確保上面兩種情況下 completionHandler 都能被執行,消除了 WKWebView 下彈 alert 框的 crash,WKWebView 下彈 confirm 框的 crash 的原因與解決方式與 alert 類似。

另一個 crash 發生在 WKWebView 退出前呼叫:

-[WKWebView evaluateJavaScript: completionHandler:]

執行JS程式碼的情況下。WKWebView 退出並被釋放後導致completionHandler變成野指標,而此時 javaScript Core 還在執行JS程式碼,待 javaScript Core 執行完畢後會呼叫completionHandler(),導致 crash。這個 crash 只發生在 iOS 8 系統上,參考Apple Open Source,在iOS9及以後系統蘋果已經修復了這個bug,主要是對completionHandler block做了copy(refer:https://trac.webkit.org/changeset/179160);對於iOS 8系統,可以通過在 completionHandler 裡 retain WKWebView 防止 completionHandler 被過早釋放。我們最後用 methodSwizzle hook 了這個系統方法:

+ (void) load {      [self jr_swizzleMethod:NSSelectorFromString(@"evaluateJavaScript:completionHandler:") withMethod:@selector(altEvaluateJavaScript:completionHandler:) error:nil]; }/*

* fix: WKWebView crashes on deallocation if it has pending JavaScript evaluation

*/

- (void)altEvaluateJavaScript:(NSString *)javaScriptString completionHandler:(void(^)(id, NSError *))completionHandler {     id strongSelf = self;     [self altEvaluateJavaScript:javaScriptString completionHandler:^(id r, NSError *e) {         [strongSelf title];if(completionHandler) {             completionHandler(r, e);         }     }]; }

8、其它問題

8.1、視訊自動播放

WKWebView 需要通過WKWebViewConfiguration.mediaPlaybackRequiresUserAction設定是否允許自動播放,但一定要在 WKWebView 初始化之前設定,在 WKWebView 初始化之後設定無效。

8.2、goBack API問題

WKWebView 上呼叫 -[WKWebView goBack], 回退到上一個頁面後不會觸發window.onload()函式、不會執行JS。

8.3、頁面滾動速率

WKWebView 需要通過scrollView delegate調整滾動速率:

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {     scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;}

轉:https://mp.weixin.qq.com/s?__biz=MzA3NTYzODYzMg==&mid=2653578513&idx=1&sn=961bf5394eecde40a43060550b81b0bb&chksm=84b3b716b3c43e00ee39de8cf12ff3f8d475096ffaa05de9c00ff65df62cd73aa1cff606057d&scene=0&key=18f25f38d156819652293ebc9f45c2c764b733698d14b3fc14c929943e23fcbcafe74634a026860fa9dd877aa69ca12c41eb0b389fa33a62b6e31e7d58ea18597ae0a1235330e198d20094c51b88cc13&ascene=0&uin=MTYxNjEyMjAwMA==&devicetype=iMac%20MacBookPro11,5%20OSX%20OSX%2010.12.2%20build%2816C67%29&version=12010210&nettype=WIFI&fontScale=100&pass_ticket=WOq/WYccfl4MsTFLBPFJ0uDFJaQTttFtzZuBNAXgp/0PsHlLsQieFLlcsVkJldKK

相關文章