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 *)webView API_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, 02 May 2019 23:38:25 GMT;
    通過 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 = [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 設置 Cookie 解決后續頁面(同域)Ajax、iframe 請求的 Cookie 問題;
注意:document.cookie()無法跨域設置 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,我們通過在 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,可以通過以下步驟實現:
    1. 替換請求 scheme,生成新的 post 請求 request2: post://h5.qzone.qq.com/mqzone/index, 同時將 request1 的 body 字段復制到 request2 的 header 中(WebKit 不會丟棄 header 字段);
    2. 通過-[WKWebView loadRequest:]加載新的 post 請求 request2;
    3. 通過 +[WKBrowsingContextController registerSchemeForCustomProtocol:]注冊 scheme: post://;
    4. 注冊 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
      <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:]實現截屏的方式失效,需要通過以下方式實現截屏功能:
      @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 頁面的截屏問題,筆者已經翻遍蘋果文檔,研究過 webKit2 源碼里的截屏私有API,依然沒有找到合適的解決方案,同時發現 Safari 以及 Chrome 這兩個全量切換到 WKWebView 的瀏覽器也存在同樣的問題:對webGL 頁面的截屏結果不是空白就是純黑圖片。無奈之下,我們只能約定一個JS接口,讓游戲開發商實現該接口,具體是通過 canvas getImageData()方法取得圖片數據后返回 base64 格式的數據,客戶端在需要截圖的時候,調用這個JS接口獲取 base64 String 并轉換成 UIImage。
    7、WKWebView crash問題
    WKWebView 放量后,外網新增了一些 crash, 其中一類 crash 的主要堆棧如下:
    ...
    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調用window.alert()函數引起的,從 crash 堆??梢钥闯鍪?WKWebView 回調函數:
  • (void) presentAlertOnController:(nonnull UIViewController)parentController title:(nullable NSString)title message:(nullable NSString *)message handler:(nonnull void (^)())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:^{}];
    else
    completionHandler();
    }
    確保上面兩種情況下 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;
    }
    9、結語
    本文總結了在 WKWebView 上踩過的一些坑。雖然 WKWebView 坑比較多,但是相對 UIWebView 在內存消耗、穩定性方面還是有很大的優勢。盡管蘋果對 WKWebView 的開發進度過于緩慢,但相信 WKWebView 才是未來。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容