導(dǎo)語
WKWebView 是蘋果在 WWDC 2014 上推出的新一代 webView 組件,用以替代 UIKit 中笨重難用疤坝、內(nèi)存泄漏的 UIWebView。WKWebView擁有60fps滾動刷新率馆铁、和 safari 相同的 JavaScript 引擎等優(yōu)勢跑揉。
簡單的適配方法本文不再贅述,主要來說說適配 WKWebView 過程中填過的坑以及善待解決的技術(shù)難題埠巨。
1畔裕、WKWebView 白屏問題
WKWebView 自詡擁有更快的加載速度衣撬,更低的內(nèi)存占用,但實際上 WKWebView 是一個多進程組件扮饶,Network Loading 以及 UI Rendering 在其它進程中執(zhí)行具练。初次適配 WKWebView 的時候,我們也驚訝于打開 WKWebView 后甜无,App 進程內(nèi)存消耗反而大幅下降扛点,但是仔細觀察會發(fā)現(xiàn),Other Process 的內(nèi)存占用會增加岂丘。在一些用 webGL 渲染的復(fù)雜頁面陵究,使用 WKWebView 總體的內(nèi)存占用(App Process Memory + Other Process Memory)不見得比 UIWebView 少很多。
在 UIWebView 上當(dāng)內(nèi)存占用太大的時候奥帘,App Process 會 crash铜邮;而在 WKWebView 上當(dāng)總體的內(nèi)存占用比較大的時候,WebContent Process 會 crash寨蹋,從而出現(xiàn)白屏現(xiàn)象松蒜。在 WKWebView 中加載下面的測試鏈接可以穩(wěn)定重現(xiàn)白屏現(xiàn)象:
http://people.mozilla.org/~rnewman/fennec/mem.html
這個時候 WKWebView.URL 會變?yōu)?nil, 簡單的 reload 刷新操作已經(jīng)失效,對于一些長駐的H5頁面影響比較大已旧。
我們最后的解決方案是:
A秸苗、借助 WKNavigtionDelegate
iOS 9以后 WKNavigtionDelegate 新增了一個回調(diào)函數(shù):
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webViewAPI_AVAILABLE(macosx(10.11),ios(9.0));
當(dāng) WKWebView 總體內(nèi)存占用過大,頁面即將白屏的時候运褪,系統(tǒng)會調(diào)用上面的回調(diào)函數(shù)惊楼,我們在該函數(shù)里執(zhí)行[webView reload](這個時候 webView.URL 取值尚不為 nil)解決白屏問題。在一些高內(nèi)存消耗的頁面可能會頻繁刷新當(dāng)前頁面秸讹,H5側(cè)也要做相應(yīng)的適配操作檀咙。
B、檢測 webView.title 是否為空
并不是所有H5頁面白屏的時候都會調(diào)用上面的回調(diào)函數(shù)璃诀,比如弧可,最近遇到在一個高內(nèi)存消耗的H5頁面上 present 系統(tǒng)相機,拍照完畢后返回原來頁面的時候出現(xiàn)白屏現(xiàn)象(拍照過程消耗了大量內(nèi)存文虏,導(dǎo)致內(nèi)存緊張侣诺,WebContent Process 被系統(tǒng)掛起),但上面的回調(diào)函數(shù)并沒有被調(diào)用氧秘。在WKWebView白屏的時候年鸳,另一種現(xiàn)象是 webView.titile 會被置空, 因此,可以在 viewWillAppear 的時候檢測 webView.title 是否為空來 reload 頁面丸相。
綜合以上兩種方法可以解決絕大多數(shù)的白屏問題搔确。
2、WKWebView Cookie 問題
Cookie 問題是目前 WKWebView 的一大短板
2.1、WKWebView Cookie存儲
業(yè)界普遍認為 WKWebView 擁有自己的私有存儲膳算,不會將 Cookie 存入到標準的 Cookie 容器NSHTTPCookieStorage中座硕。
實踐發(fā)現(xiàn) WKWebView 實例其實也會將 Cookie 存儲于 NSHTTPCookieStorage 中,但存儲時機有延遲涕蜂,在iOS 8上华匾,當(dāng)頁面跳轉(zhuǎn)的時候,當(dāng)前頁面的 Cookie 會寫入 NSHTTPCookieStorage 中机隙,而在 iOS 10 上蜘拉,JS 執(zhí)行 document.cookie 或服務(wù)器 set-cookie 注入的 Cookie 會很快同步到 NSHTTPCookieStorage 中,F(xiàn)ireFox 工程師曾建議通過 reset WKProcessPool 來觸發(fā) Cookie 同步到 NSHTTPCookieStorage 中有鹿,實踐發(fā)現(xiàn)不起作用旭旭,并可能會引發(fā)當(dāng)前頁面 session cookie 丟失等問題。
WKWebView Cookie 問題在于 WKWebView 發(fā)起的請求不會自動帶上存儲于 NSHTTPCookieStorage 容器中的 Cookie葱跋。
比如持寄,NSHTTPCookieStorage 中存儲了一個 Cookie:
name=Nicholas;value=test;domain=y.qq.com;expires=Sat,02May201923:38:25GMT;
通過 UIWebView 發(fā)起請求http://y.qq.com娱俺,則請求頭會自動帶上 cookie: Nicholas=test稍味;
而通過 WKWebView發(fā)起請求http://y.qq.com,請求頭不會自動帶上 cookie: Nicholas=test矢否。
2.2仲闽、WKProcessPool
蘋果開發(fā)者文檔對 WKProcessPool 的定義是:A WKProcessPool object represents a pool of Web Content process. 通過讓所有 WKWebView 共享同一個 WKProcessPool 實例脑溢,可以實現(xiàn)多個 WKWebView 之間共享 Cookie(session Cookie and persistent Cookie)數(shù)據(jù)僵朗。不過 WKWebView WKProcessPool 實例在 app 殺進程重啟后會被重置,導(dǎo)致 WKProcessPool 中的 Cookie屑彻、session Cookie 數(shù)據(jù)丟失验庙,目前也無法實現(xiàn) WKProcessPool 實例本地化保存。
2.3社牲、Workaround
由于許多 H5 業(yè)務(wù)都依賴于 Cookie 作登錄態(tài)校驗粪薛,而 WKWebView 上請求不會自動攜帶 Cookie, 目前的主要解決方案是:
a、WKWebView loadRequest 前搏恤,在 request header 中設(shè)置 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 設(shè)置 Cookie 解決后續(xù)頁面(同域)Ajax熟空、iframe 請求的 Cookie 問題藤巢;
注意:document.cookie()無法跨域設(shè)置 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跳轉(zhuǎn)到 www.b.com,這個時候 www.b.com 這個請求就可能因為沒有攜帶 cookie 而無法訪問绍刮。當(dāng)然温圆,由于每一次頁面跳轉(zhuǎn)前都會調(diào)用回調(diào)函數(shù):
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler;
可以在該回調(diào)函數(shù)里攔截302請求,copy request孩革,在 request header 中帶上 cookie 并重新 loadRequest岁歉。不過這種方法依然解決不了頁面 iframe 跨域請求的 Cookie 問題,畢竟-[WKWebView loadRequest:]只適合加載 mainFrame 請求膝蜈。
3刨裆、WKWebView NSURLProtocol問題
WKWebView 在獨立于 app 進程之外的進程中執(zhí)行網(wǎng)絡(luò)請求,請求數(shù)據(jù)不經(jīng)過主進程彬檀,因此帆啃,在 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 數(shù)據(jù)被清空
由于 WKWebView 在獨立進程里執(zhí)行網(wǎng)絡(luò)請求努潘。一旦注冊 http(s) scheme 后,網(wǎng)絡(luò)請求將從 Network Process 發(fā)送到 App Process坤学,這樣 NSURLProtocol 才能攔截網(wǎng)絡(luò)請求疯坤。在 webkit2 的設(shè)計里使用 MessageQueue 進行進程之間的通信,Network Process 會將請求 encode 成一個 Message,然后通過 IPC 發(fā)送給 App Process深浮。出于性能的原因压怠,encode 的時候 HTTPBody 和 HTTPBodyStream 這兩個字段被丟棄掉了
參考蘋果源碼:
https://github.com/WebKit/webkit/blob/fe39539b83d28751e86077b173abd5b7872ce3f9/Source/WebKit2/Shared/mac/WebCoreArgumentCodersMac.mm#L61-L88(復(fù)制鏈接到瀏覽器中打開)
及bug report:
https://bugs.webkit.org/show_bug.cgi?id=138169(復(fù)制鏈接到瀏覽器中打開)
因此,如果通過 registerSchemeForCustomProtocol 注冊了 http(s) scheme, 那么由 WKWebView 發(fā)起的所有 http(s)請求都會通過 IPC 傳給主進程 NSURLProtocol 處理飞苇,導(dǎo)致 post 請求 body 被清空菌瘫;
b、對ATS支持不足
測試發(fā)現(xiàn)一旦打開ATS開關(guān):Allow Arbitrary Loads 選項設(shè)置為NO布卡,同時通過 registerSchemeForCustomProtocol 注冊了 http(s) scheme雨让,WKWebView 發(fā)起的所有 http 網(wǎng)絡(luò)請求將被阻塞(即便將Allow Arbitrary Loads in Web Content 選項設(shè)置為YES);
WKWebView 可以注冊 customScheme, 比如 dynamic://, 因此希望使用離線功能又不使用 post 方式的請求可以通過 customScheme 發(fā)起請求忿等,比如 dynamic://www.dynamicalbumlocalimage.com/栖忠,然后在 app 進程 NSURLProtocol 攔截這個請求并加載離線數(shù)據(jù)。不足:使用 post 方式的請求該方案依然不適用贸街,同時需要 H5 側(cè)修改請求 scheme 以及 CSP 規(guī)則庵寞;
4、WKWebView loadRequest 問題
在 WKWebView 上通過 loadRequest 發(fā)起的 post 請求 body 數(shù)據(jù)會丟失:
//同樣是由于進程間通信性能問題薛匪,HTTPBody字段被丟棄
[request setHTTPMethod:@"POST"];[request setHTTPBody:[@"bodyData"dataUsingEncoding:NSUTF8StringEncoding]];[wkwebview loadRequest: request];
workaround:
假如想通過-[WKWebView loadRequest:]加載 post 請求 request1:http://h5.qzone.qq.com/mqzone/index,可以通過以下步驟實現(xiàn):
替換請求 scheme捐川,生成新的 post 請求 request2:post://h5.qzone.qq.com/mqzone/index, 同時將 request1 的 body 字段復(fù)制到 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 字段復(fù)制到 request3 的 body 中蛋辈,并使用 NSURLConnection 加載 request3属拾,最后通過 NSURLProtocolClient 將加載結(jié)果返回 WKWebView;
5将谊、WKWebView 頁面樣式問題
在 WKWebView 適配過程中,我們發(fā)現(xiàn)部分H5頁面元素位置向下偏移或被拉伸變形渐白,追蹤后發(fā)現(xiàn)主要是H5頁面高度值異常導(dǎo)致:
a. 空間H5頁面有透明導(dǎo)航尊浓、透明導(dǎo)航下拉刷新、全屏等需求纯衍,因此之前 webView 整個是從(0, 0)開始布局栋齿,通過調(diào)整webView.scrollView.contentInset來適配特殊導(dǎo)航欄需求。而在 WKWebView 上對 contentInset 的調(diào)整會反饋到webView.scrollView.contentSize.height的變化上襟诸,比如設(shè)置webView.scrollView.contentInset.top = a瓦堵,那么contentSize.height的值會增加a,導(dǎo)致H5頁面長度增加,頁面元素位置向下偏移歌亲;
解決方案是:調(diào)整WKWebView布局方式菇用,避免調(diào)整webView.scrollView.contentInset。實際上陷揪,即便在 UIWebView 上也不建議直接調(diào)整webView.scrollView.contentInset的值惋鸥,這確實會帶來一些奇怪的問題。如果某些特殊情況下非得調(diào)整 contentInset 不可的話悍缠,可以通過下面方式讓H5頁面恢復(fù)正常顯示:
/**設(shè)置contentInset值后通過調(diào)整webView.frame讓頁面恢復(fù)正常顯示
*參考: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 直播的時候卦绣,我們發(fā)現(xiàn)在 iOS 9 上 WKWebView 會出現(xiàn)頁面被拉伸變形的情況,最后發(fā)現(xiàn)是window.innerHeight值不準確導(dǎo)致(在WKWebView上返回了一個非常大的值)飞蚓,而H5同學(xué)通過獲取window.innerHeight來設(shè)置頁面高度滤港,導(dǎo)致頁面整體被拉伸。通過查閱相關(guān)資料發(fā)現(xiàn)趴拧,這個bug只在 iOS 9 的幾個系統(tǒng)版本上出現(xiàn)溅漾,蘋果后來fix了這個bug。我們最后的解決方案是:延遲調(diào)用window.innerHeight
setTimeout(function(){height = window.innerHeight},0);
or
Use shrink-to-fit meta-tag
6八堡、WKWebView 截屏問題
空間玩吧H5小游戲有截屏分享的功能樟凄,WKWebView 下通過 -[CALayer renderInContext:]實現(xiàn)截屏的方式失效聘芜,需要通過以下方式實現(xiàn)截屏功能:
@implementationUIView (ImageSnapshot) - (UIImage*)imageSnapshot {? ? ?UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES,self.contentScaleFactor);? ? ?[self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];? ? ?UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();? ? ?UIGraphicsEndImageContext();returnnewImage; }@end
然而這種方式依然解決不了 webGL 頁面的截屏問題兄渺,筆者已經(jīng)翻遍蘋果文檔,研究過 webKit2 源碼里的截屏私有API汰现,依然沒有找到合適的解決方案挂谍,同時發(fā)現(xiàn) Safari 以及 Chrome 這兩個全量切換到 WKWebView 的瀏覽器也存在同樣的問題:對webGL 頁面的截屏結(jié)果不是空白就是純黑圖片。無奈之下瞎饲,我們只能約定一個JS接口口叙,讓游戲開發(fā)商實現(xiàn)該接口,具體是通過canvas getImageData()方法取得圖片數(shù)據(jù)后返回 base64 格式的數(shù)據(jù)嗅战,客戶端在需要截圖的時候妄田,調(diào)用這個JS接口獲取 base64 String 并轉(zhuǎn)換成 UIImage俺亮。
7、WKWebView crash問題
WKWebView 放量后疟呐,外網(wǎng)新增了一些 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調(diào)用window.alert()函數(shù)引起的脚曾,從 crash 堆棧可以看出是 WKWebView 回調(diào)函數(shù):
+ (void) presentAlertOnController:(nonnull UIViewController*)parentController title:(nullable NSString*)title message:(nullable NSString *)message handler:(nonnullvoid(^)())completionHandler;
completionHandler 沒有被調(diào)用導(dǎo)致的启具。在適配 WKWebView 的時候本讥,我們需要自己實現(xiàn)該回調(diào)函數(shù),window.alert()才能調(diào)起 alert 框鲁冯,我們最初的實現(xiàn)是這樣的:
- (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剛好執(zhí)行了window.alert(), alert 框可能彈不出來,completionHandler 最后沒有被執(zhí)行薯演,導(dǎo)致 crash撞芍;另一種情況是在 WKWebView 一打開,JS就執(zhí)行window.alert()跨扮,這個時候由于 WKWebView 所在的 UIViewController 出現(xiàn)(push或present)的動畫尚未結(jié)束勤庐,alert 框可能彈不出來,completionHandler 最后沒有被執(zhí)行好港,導(dǎo)致 crash愉镰。我們最終的實現(xiàn)大致是這樣的:
- (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 都能被執(zhí)行,消除了 WKWebView 下彈 alert 框的 crash钧汹,WKWebView 下彈 confirm 框的 crash 的原因與解決方式與 alert 類似丈探。
另一個 crash 發(fā)生在 WKWebView 退出前調(diào)用:
-[WKWebView evaluateJavaScript: completionHandler:]
執(zhí)行JS代碼的情況下。WKWebView 退出并被釋放后導(dǎo)致completionHandler變成野指針拔莱,而此時 javaScript Core 還在執(zhí)行JS代碼碗降,待 javaScript Core 執(zhí)行完畢后會調(diào)用completionHandler(),導(dǎo)致 crash塘秦。這個 crash 只發(fā)生在 iOS 8 系統(tǒng)上讼渊,參考Apple Open Source,在iOS9及以后系統(tǒng)蘋果已經(jīng)修復(fù)了這個bug尊剔,主要是對completionHandler block做了copy(refer:https://trac.webkit.org/changeset/179160)爪幻;對于iOS 8系統(tǒng),可以通過在 completionHandler 里 retain WKWebView 防止 completionHandler 被過早釋放须误。我們最后用 methodSwizzle hook 了這個系統(tǒng)方法:
+ (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設(shè)置是否允許自動播放京痢,但一定要在 WKWebView 初始化之前設(shè)置奶甘,在 WKWebView 初始化之后設(shè)置無效。
8.2祭椰、goBack API問題
WKWebView 上調(diào)用 -[WKWebView goBack], 回退到上一個頁面后不會觸發(fā)window.onload()函數(shù)臭家、不會執(zhí)行JS疲陕。
8.3、頁面滾動速率
WKWebView 需要通過scrollView delegate調(diào)整滾動速率:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { ? ? scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;}
9钉赁、結(jié)語
本文總結(jié)了在 WKWebView 上踩過的一些坑鸭轮。雖然 WKWebView 坑比較多,但是相對 UIWebView 在內(nèi)存消耗橄霉、穩(wěn)定性方面還是有很大的優(yōu)勢窃爷。盡管蘋果對 WKWebView 的開發(fā)進度過于緩慢,但相信 WKWebView 才是未來姓蜂。
如果您覺得我們的內(nèi)容還不錯按厘,就請掃描二維碼贊賞作者并轉(zhuǎn)發(fā)到朋友圈,和小伙伴一起分享吧~