WKWebView的一些問題彙總

___克里斯___發表於2019-12-12

白屏問題

UIWebView上當記憶體佔用過大時,App會crash; WKWebView上當記憶體佔用過大時,WebContent process會crash,導致白屏。 此時,wkWebView的url變為nil,reload操作已無效。

所以,白屏的根本原因是由記憶體佔用過大引發的App crash問題,轉換成了WebContent process的crash問題。

解決方法

WKNavigationDelegate的回撥方法webViewWebContentProcessDidTerminate

在webViewWebContentProcessDidTerminate回撥中執行webView的reload操作。 當WebContent process即將白屏時會觸發該回撥。 此時,wkWebView的url尚未變成nil,所以reload可以生效。 而在高記憶體消耗的介面可能會頻繁reload。

新增一個web content process崩潰的標記位 在崩潰的時候webViewWebContentProcessDidTerminate設定該標記位,在viewWillAppear中根據該標記位來判斷,然後reload操作。

通過webView.url來判斷

在viewWillAppear的時候對url進行判斷,若為空,則reload。

同時使用KVO來監聽URL為空的情況,

if ([self.webView.scrollView.superview isKindOfClass:[WKWebView class]]) {
    WKWebView *wkWebView = (WKWebView *)(self.webView.scrollView.superview);
    [wkWebView addObserver:self forKeyPath:@"URL" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"URL"]) {
        NSURL *newUrl = change[@"new"];
        NSURL *oldUrl = change[@"old"];
        if ([newUrl isEqual:[NSNull null]] && ![oldUrl isEqual:[NSNull null]]) {
            [self.webView reload];
        }
    }
}
複製程式碼

通過webView.title來判斷

在viewWillAppear的時候對title進行判斷,若為空,則reload。

並非所有白屏的情況都會呼叫上邊的回撥方法。 如在H5介面present出來一個相機拍照,導致web content掛掉,則返回H5介面的時候,wkwebview的title會為空。 若拍照耗了太多記憶體,導致記憶體緊張,WebContent process掛掉, 但上邊的回撥方法webViewWebContentProcessDidTerminate並未被呼叫。 白屏時webView的title會被置空,所以可以在viewWillAppear中檢測webView.title是否為空來reload介面。 不懂?

通過innerHTML來判斷

在viewWillAppear的時候對innerHTML進行判斷,若為空,則reload。

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    __weak typeof(self) weakSelf = self;
    [self.webView evaluateJavaScript:@"document.querySelector('body').innerHTML" completionHandler:^(id result, NSError *error) {
        __strong __typeof(weakSelf) strongSelf = weakSelf;
        if (!result || ([result isKindOfClass:[NSString class]] && [((NSString *)result) length] == 0)) {
            NSLog(@"--- H5頁面載入異常,重新載入中 -----");
            // reload your page
            [strongSelf.webView reload];
        } else {
            NSLog(@"--- H5頁面載入正常 -----");
        }
    }];
}
複製程式碼

Cookie問題

問題在於:

WKWebView發起的請求不會自動帶上儲存於NSHTTPCookieStorage中的cookie。 而UIWebView會自動帶上。

Cookie格式:

name=Nicholas;value=test;domain=y.qq.com;expires=Sat, 02 May 2019 23:38:25 GMT;
複製程式碼

UIWebView設定cookie

var props = Dictionary<HTTPCookiePropertyKey, Any>()
props[HTTPCookiePropertyKey.name] = "tigerobocookie"
props[HTTPCookiePropertyKey.value] = cookieValue // 一串key-value,以;間隔。
props[HTTPCookiePropertyKey.path] = "/"
props[HTTPCookiePropertyKey.domain] = API.tigerResearchWebDomain // 必須設定souyanbao.tigerobo.com

if let cookie = HTTPCookie(properties: props) {
  NSHTTPCookieStorage.shared.setCookie(cookie)
}

xxx
let request = URLRequest(url: url)
webView.loadRequest(request)
複製程式碼

WKWebView設定cookie

而WKWebView使用NSHTTPCookieStorage則不行。

使用WKWebView.configuration.websiteDataStore.httpCookieStore

guard let cookies = HTTPCookie(properties: props) else { return }
let httpCookieStore = wkWebView.configuration.websiteDataStore.httpCookieStore
httpCookieStore.setCookie(cookie) {
  DispatchQueue.main.async {
    self.wkWebView.load(request)
  }
}
複製程式碼

但是httpCookieStore只在iOS 11之後才能用。

在loadRequest之前,在request header中設定cookie,解決首個cookie帶不上的問題

NSURL *url = [NSURL URLWithString:@"http://github.com"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"];

WKWebView *webView = [WKWebView new];
[webView loadRequest:request];
複製程式碼

通過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問題。 可以在webView:decidePolicyForNavigationAction:decisionHandler:回撥方法中攔截302請求,copy request, 在request header中帶上cookie並重新loadRequest。不過依然解決不了頁面iframe跨越請求的cookie問題。 因為loadRequest只適合載入mainFrame請求。

NSURLProtocol可以用於中間人攻擊

WKWebView獨立於app之外的程式(webContent process)執行網路請求,請求資料不經過主程式。 所以WKWebView中無法直接使用NSURLProtocol來攔截請求,

loadRequest問題

WKWebView通過loadRequest發起的post請求body資料會丟失。 因為程式間通訊效能問題,HTTPBody欄位被丟棄

[request setHTTPMethod:@"POST"];
[request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]];
[wkWebView loadRequest:request];
複製程式碼

由此想到的

小程式開發中通過的效能優化關鍵在於setData方法的呼叫。為啥會這麼影響效能呢?

小程式的檢視層目前使用 WebView 作為渲染載體,而邏輯層是由獨立的 JavascriptCore 作為執行環境。在架構上,WebView 和 JavascriptCore 都是獨立的模組,並不具備資料直接共享的通道。

當前,檢視層和邏輯層的資料傳輸,實際上通過兩邊提供的evaluateJavascript所實現。即使用者傳輸的資料(由檢視層去往邏輯層),需要將其轉換為字串形式傳遞,同時把轉換後的資料內容拼接成一份 JS 指令碼,再通過執行 JS 指令碼的形式傳遞到兩邊獨立環境。

而evaluateJavascript的執行會受很多方面的影響,資料到達檢視層並不是實時的。同一程式內的 WebView 實際上會共享一個 JS VM,如果 WebView 內 JS 執行緒正在執行渲染或其他邏輯,會影響 evaluateJavascript 指令碼的實際執行時間,

另外多個 WebView 也會搶佔 JS VM 的執行許可權;另外還有 JS 本身的編譯執行耗時,都是影響資料傳輸速度的因素。

常見的 setData 錯誤操作:

  1. 頻繁的去執行setData。 Android 下使用者在滑動時會感覺到卡頓,操作反饋延遲嚴重;渲染有出現延時。
  2. 每次 setData 都傳遞大量新資料。
  3. 後臺態頁面進行 setData。

為何一定要使用setData這種方式:

因為js語言的限制, 不能檢測到物件屬性的新增或刪除. 不能檢測到單獨對data做修改的操作.

參考

相關文章