1.WKWebView白屏問題
WKWebView自我擁有更快的加載速度挽绩,更低的內(nèi)存占用霹俺,但實(shí)際上WKWebView是一個多進(jìn)程組件脖阵,網(wǎng)絡(luò)加載以及UI渲染在其它進(jìn)程中執(zhí)行拇派。初次適配WKWebView的時候拍屑,我們也驚訝于打開WKWebView其他進(jìn)程的內(nèi)存占用會增加途戒。在一些用于webGL渲染的復(fù)雜頁面比UIWebView少很多。
在UIWebView上當(dāng)內(nèi)存占用太大的時候僵驰,App Process會崩潰;而在WKWebView上當(dāng)總體的內(nèi)存占用比較大的時候喷斋,WebContent Process會崩潰,從而出現(xiàn)白屏現(xiàn)象蒜茴。在WKWebView中加載下面的測試鏈接可以穩(wěn)定重現(xiàn)白屏現(xiàn)象:
這個時候WKWebView.URL會變?yōu)閚il星爪,簡單的reload刷新操作已經(jīng)失效,對于一些長駐的H5頁面影響比較大粉私。
我們最后的解決方案是:
A 借助WKNavigtionDelegate
iOS 9以后WKNavigtionDelegate新增了一個回調(diào)函數(shù):
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_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取值尚不為零)解決白屏問題崔泵。在一些高內(nèi)存消耗的頁面可能會頻繁刷新當(dāng)前頁面秒赤,H5側(cè)也要做相應(yīng)的適配操作。
B 檢測webView.title是否為空
并不是所有H5頁面白屏的時候都會調(diào)用上面的回調(diào)函數(shù)憎瘸,比如入篮,最近遇到在一個高內(nèi)存消耗的H5頁面上現(xiàn)在系統(tǒng)相機(jī),拍照完畢后返回原來頁面的時候出現(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是否為空來重裝頁面肮帐。
綜合以上兩種方法可以解決絕大多數(shù)的白屏問題。
2 WKWebView Cookie問題
Cookie問題是目前WKWebView的一大短板
2.1 WKWebView Cookie存儲
業(yè)界普遍認(rèn)為WKWebView擁有自己的私有存儲边器,不會將Cookie存儲到標(biāo)準(zhǔn)的Cookie容器NSHTTPCookieStorage中训枢。
實(shí)踐發(fā)現(xiàn)WKWebView實(shí)例其實(shí)也會將Cookie存儲于NSHTTPCookieStorage中,但存儲時機(jī)有延遲忘巧,在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工程師曾建議通過重置WKProcessPool來觸發(fā)Cookie同步到NSHTTPCookieStorage中际长,實(shí)踐發(fā)現(xiàn)不起作用耸采,并可能會引發(fā)當(dāng)前頁面會話cookie丟失等問題。
WKWebView Cookie問題在于WKWebView發(fā)起的請求不會自動帶上存儲于NSHTTPCookieStorage容器中的Cookie工育。
比如虾宇,NSHTTPCookieStorage中存儲了一個Cookie:
name=Nicholas;value=test;domain=y.qq.com;expires=Sat, 02 May 2019 23:38:25 GMT;
通過UIWebView發(fā)起請求http://y.qq.com翅娶,則請求頭會自動帶上cookie:Nicholas = test;
而通過WKWebView發(fā)起請求http://y.qq.com文留,請求頭不會自動帶上cookie :尼古拉斯=測試好唯。
2.2 WKProcessPool
蘋果開發(fā)者文檔對WKProcessPool的定義是:WKProcessPool對象代表一個Web Content進(jìn)程池竭沫。通過讓所有WKWebView共享同一個WKProcessPool實(shí)例,可以實(shí)現(xiàn)多個WKWebView之間共享Cookie(會話Cookie和持久Cookie)數(shù)據(jù)骑篙。不過WKWebView WKProcessPool實(shí)例在app殺進(jìn)程重啟后會被重置蜕提,導(dǎo)致WKProcessPool中的Cookie,session Cookie數(shù)據(jù)丟失靶端,目前也無法實(shí)現(xiàn)WKProcessPool實(shí)例本地化保存谎势。
2.3 解決方法
由于許多H5業(yè)務(wù)都依賴于Cookie作登錄態(tài)校驗(yàn)凛膏,而WKWebView上請求不會自動攜帶Cookie,目前的主要解決方案是:
a WKWebView loadRequest前脏榆,在請求頭中設(shè)置Cookie猖毫,解決首個請求Cookie帶不上的問題;
WKWebView * webView = [WKWebView new]; 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 = [WKUserContentController new]; WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie = 'skey=skeyValue';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
這請方案無法解決302請求的Cookie問題须喂,比如吁断,第一個請求是www.a.com,我們通過請求標(biāo)題里帶上Cookie解決該請求的Cookie問題坞生,接著頁面302跳轉(zhuǎn)到www.b.com 仔役,這個時候www.b.com這個請求就可能因?yàn)闆]有攜帶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請求又兵,復(fù)制請求,在請求標(biāo)題中帶上cookie并重新加載請求卒废。不過這種方法依然解決不知頁面iframe跨域請求的Cookie問題沛厨,畢竟 - [WKWebView loadRequest:]只適合加載mainFrame請求。
3 WKWebView NSURLProtocol問題
WKWebView在獨(dú)立于app進(jìn)程之外的進(jìn)程中執(zhí)行網(wǎng)絡(luò)請求升熊,請求數(shù)據(jù)不經(jīng)過主進(jìn)程俄烁,因此,在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"];
}
但是這種方案目前存在兩個嚴(yán)重缺陷:
a post請求身體數(shù)據(jù)被清空
由于WKWebView在獨(dú)立進(jìn)程里執(zhí)行網(wǎng)絡(luò)請求页屠。一旦注冊http(s)scheme后,網(wǎng)絡(luò)請求將從網(wǎng)絡(luò)流程發(fā)送到App Process蓖柔,這樣NSURLProtocol才能攔截網(wǎng)絡(luò)請求辰企。在webkit2的設(shè)計(jì)里使用MessageQueue進(jìn)行進(jìn)程之間的通信,網(wǎng)絡(luò)進(jìn)程會求請求編碼成一個消息况鸣,然后通過IPC發(fā)送給App Process牢贸。出于性能的原因,編碼的時候HTTPBody和HTTPBodyStream這兩個字段被丟棄掉了
參考蘋果源碼:
及bug報(bào)告:
https://bugs.webkit.org/show_bug.cgi?id=138169
因此镐捧,如果通過registerSchemeForCustomProtocol注冊了http(s)scheme潜索,那么由WKWebView發(fā)起的所有http(s)請求都會通過IPC傳給主進(jìn)程N(yùn)SURLProtocol處理,導(dǎo)致post請求body被清空 ;
B 對ATS支持不足
測試發(fā)現(xiàn)一旦打開ATS開關(guān):允許任意加載選項(xiàng)設(shè)置為NO懂酱,同時通過registerSchemeForCustomProtocol注冊了http(s)方案竹习,WKWebView發(fā)起的所有http網(wǎng)絡(luò)請求將被阻塞(即便將允許Web內(nèi)容中的任意加載選項(xiàng)設(shè)置為YES) ;
WKWebView可以注冊customScheme,比如動態(tài)://列牺,因此希望使用離線功能又不使用交方式的請求可以通過customScheme發(fā)起請求整陌,比如動態(tài)://www.dynamicalbumlocalimage.com/,然后在應(yīng)用程序進(jìn)程N(yùn)SURLProtocol攔截這個請求并加載離線數(shù)據(jù)。不足:使用post方式的請求該方案依然不適用泌辫,同時需要H5側(cè)修改請求方案以及CSP規(guī)則;
4 WKWebView loadRequest問題
在WKWebView上通過loadRequest發(fā)起的post請求body數(shù)據(jù)會丟失:
//同樣是由于進(jìn)程間通信性能問題随夸,HTTPBody字段被丟棄[request setHTTPMethod:@"POST"];
[request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]];
[wkwebview loadRequest: request];
解決方法:
假如想通過 - [WKWebView loadRequest:]加載帖請求請求1:http://h5.qzone.qq.com/mqzone/index ,可以通過以下步驟實(shí)現(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:]注冊方案:post:// ;
注冊NSURLProtocol攔截請求帖子://h5.qzone.qq.com/mqzone/index,替換請求方案殿遂,生成新的請求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
的值會增加一幸乒,導(dǎo)致H5頁面長度增加,頁面元素位置向下偏移;
解決方案是:調(diào)整WKWebView布局方式唇牧,避免調(diào)整webView.scrollView.contentInset
罕扎。實(shí)際上,即便在UIWebView上也不建議直接調(diào)整webView.scrollView.contentInset
的值丐重,這確實(shí)會帶來一些奇怪的問題腔召。如果某些特殊情況下非得調(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 在接入現(xiàn)在直播的時候扮惦,我們發(fā)現(xiàn)在iOS 9上WKWebView會出現(xiàn)頁面被拉伸變形的情況臀蛛,最后發(fā)現(xiàn)是window.innerHeight
值不準(zhǔn)確導(dǎo)致(在WKWebView上返回了一個非常大的值),而H5同學(xué)通過獲取window.innerHeight
來設(shè)置頁面高度崖蜜,導(dǎo)致頁面整體被拉伸浊仆。通過查閱相關(guān)資料發(fā)現(xiàn),這個bug只在iOS 9的幾個系統(tǒng)版本上出現(xiàn)豫领,蘋果后來修復(fù)了這個bug抡柿。我們最后的解決方案是:延遲調(diào)用窗口。 innerHeight
setTimeout(function(){height = window.innerHeight},0);
要么
Use shrink-to-fit meta-tag
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1, shrink-to-fit=no">
6 WKWebView截屏問題
空間玩吧H5小游戲有截屏分享的功能氏堤,WKWebView下通過 - [CALayer renderInContext:]實(shí)現(xiàn)截屏的方式失效沙绝,需要通過以下方式實(shí)現(xiàn)截屏功能:
@implementation UIView (ImageSnapshot)
- (UIImage*)imageSnapshot {
UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES,self.contentScaleFactor);
[self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
@end
然而這種方式依然解決不了webGL頁面的截屏問題,筆者已經(jīng)翻譯蘋果文檔鼠锈,研究過webKit2源碼里的截屏私有API闪檬,依然沒有找到合適的解決方案,同時發(fā)現(xiàn)Safari以及Chrome這兩個全量切換到WKWebView的瀏覽器也存在同樣的問題:對webGL頁面的截屏結(jié)果不是空白就是純黑圖片购笆。無奈之下粗悯,我們只能約定一個JS接口,讓游戲開發(fā)商實(shí)現(xiàn)該接口同欠,具體是通過canvas getImageData()
方法取得圖片數(shù)據(jù)后返回base64格式的數(shù)據(jù)样傍,客戶端在需要截圖的時候,調(diào)用這個JS接口獲取base64字符串并轉(zhuǎn)換成UIImage铺遂。
7 WKWebView崩潰問題
WKWebView放量后衫哥,外網(wǎng)新增了一些崩潰,其中一類崩潰的主要堆棧如下:
...
28 UIKit 0x0000000190513360 UIApplicationMain + 208 29 Qzone 0x0000000101380570 main (main.m:181)
30 libdyld.dylib 0x00000001895205b8 _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:(nonnull void (^)())completionHandler;
completionHandler沒有被調(diào)用導(dǎo)致的。在適配WKWebView的時候粮坞,我們需要自己實(shí)現(xiàn)該回調(diào)函數(shù)蚊荣,window.alert()
才能調(diào)起alert框,我們最初的實(shí)現(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:@"確認(rèn)" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]];
[self presentViewController:alertController animated:YES completion:^{}];
}
如果WKWebView退出的時候莫杈,JS剛好執(zhí)行window.alert()
互例,alertHandler最后沒有被執(zhí)行,導(dǎo)致崩潰;另一種情況是在WKWebView一打開筝闹,JS就執(zhí)行window.alert()
媳叨,這個時候由于WKWebView所在的UIViewController出現(xiàn)( push or present)的動畫尚未結(jié)束,alert框可能彈不出來关顷,completionHandler最后沒有被執(zhí)行肩杈,導(dǎo)致crash。我們最終的實(shí)現(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:@"確認(rèn)" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]];
if (/*UIViewController of WKWebView is visible*/)
[self presentViewController:alertController animated:YES completion:^{}];
else
completionHandler();
}
確保上面兩種情況下completionHandler都能被執(zhí)行解寝,消除了WKWebView下彈警報(bào)框的崩潰扩然,WKWebView下彈確認(rèn)框的崩潰的原因與解決方式和警類類似。
另一個crash發(fā)生在WKWebView退出前調(diào)用:
-[WKWebView evaluateJavaScript: completionHandler:]
執(zhí)行JS代碼的情況下.WKWebView退出并被釋放后導(dǎo)致completionHandler
變成野指針聋伦,而此時javaScript核心還在執(zhí)行JS代碼夫偶,待javaScript核心執(zhí)行完畢后會調(diào)用completionHandler()
,導(dǎo)致崩潰觉增。這個崩潰只發(fā)生在iOS 8系統(tǒng)上租冠,參考Apple Open Source辞嗡,在iOS9及以后系統(tǒng)蘋果已經(jīng)修復(fù)了這個bug,主要是對completionHandler block
做了copy(參考:https://trac.webkit.org/changeset/179160 );對于iOS 8系統(tǒng),可以通過在completionHandler里保留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;
}