WKWebView

本文系Smallfan(程序猿小風(fēng)扇)原創(chuàng)內(nèi)容,轉(zhuǎn)載請(qǐng)?jiān)谖恼麻_(kāi)頭顯眼處注明作者和出處阿蝶。

分析

在iPhone 6s冰木、iOS 10.3.2中,對(duì) http://www.qq.com 進(jìn)行10次請(qǐng)求夸浅,得到如下數(shù)據(jù):

次數(shù) UIWebView 內(nèi)存消耗 WKWebView 內(nèi)存(APP)消耗 UIWebView 請(qǐng)求耗時(shí) WKWebView 請(qǐng)求耗時(shí)
1 67.47 MB 0.81 MB 4.13 s 0.80 s
2 58.23 MB 0.86 MB 1.16 s 0.54 s
3 57.83 MB 0.50 MB 1.14 s 0.56 s
4 59.38 MB 0.88 MB 1.08 s 1.07 s
5 59.70 MB 0.75 MB 1.07 s 0.71 s
6 64.05 MB 0.83 MB 1.47 s 0.65 s
7 59.45 MB 0.81 MB 1.11 s 0.63 s
8 57.55 MB 0.45 MB 1.15 s 0.64 s
9 58.47 MB 0.77 MB 1.17s 0.75 s
10 58.89 MB 0.84 MB 1.11 s 0.70 s

UIWebView平均內(nèi)存消耗:54.13 MB
WKWebView平均(APP)內(nèi)存消耗:0.75 MB
UIWebView平均請(qǐng)求耗時(shí):1.46 s
WKWebView平均請(qǐng)求耗時(shí):0.7 s

綜上可得:WKWebView在請(qǐng)求耗時(shí)上為UIWebView的50%左右,內(nèi)存上更是完勝固耘。但實(shí)際上 WKWebView是一個(gè)多進(jìn)程組件题篷,網(wǎng)絡(luò)請(qǐng)求以及UI渲染在其它進(jìn)程中執(zhí)行。仔細(xì)觀察會(huì)發(fā)現(xiàn):加載時(shí)厅目,App進(jìn)程內(nèi)存消耗雖非常小甚至反而大幅下降番枚,但Other Process的內(nèi)存占用會(huì)增加。所以:在UIWebView上當(dāng)內(nèi)存占用太大的時(shí)候损敷,App Process會(huì)crash葫笼;而在WKWebView上當(dāng)總體的內(nèi)存占用比較大的時(shí)候,WebContent Process會(huì)crash拗馒,從而出現(xiàn)白屏現(xiàn)象路星。

Tip: 在一些用webGL渲染的復(fù)雜頁(yè)面,使用WKWebView總體的內(nèi)存占用(App Process Memory + Other Process Memory)不見(jiàn)得比UIWebView少很多诱桂。

考慮全部替換WKWebView風(fēng)險(xiǎn)過(guò)高洋丐,可通過(guò)Server端在APP啟動(dòng)時(shí)下發(fā)URL列表的方式實(shí)現(xiàn)WKWebView的灰度能力呈昔。通過(guò)封裝繼承 UIViewSFWebView ,實(shí)現(xiàn)UIWebView與WKWebView雙核能力WebView友绝。

特性

關(guān)于WKWebView特性:

  • 在性能堤尾、穩(wěn)定性、功能方面有很大提升迁客;
  • 允許JavaScript的Nitro庫(kù)加載并使用(UIWebView中限制)郭宝;
  • 支持了更多的HTML5特性;
  • 高達(dá)60fps的滾動(dòng)刷新率以及內(nèi)置手勢(shì)掷漱;
  • 將UIWebView 和 UIWebViewDelegate 重構(gòu)成了14類與3個(gè)協(xié)議粘室;

一些問(wèn)題及解決方案

1.白屏問(wèn)題

在UIWebView上當(dāng)內(nèi)存占用太大的時(shí)候,App Process會(huì)crash卜范;而在WKWebView上當(dāng)總體的內(nèi)存占用比較大的時(shí)候衔统,WebContent Process會(huì)crash,從而出現(xiàn)白屏現(xiàn)象先朦。

實(shí)驗(yàn)鏈接: http://people.mozilla.org/~rnewman/fennec/mem.html

這個(gè)時(shí)候WKWebView.URL會(huì)變?yōu)閚il, 簡(jiǎn)單的 reload 刷新操作已經(jīng)失效缰冤,對(duì)于一些長(zhǎng)駐的H5頁(yè)面影響比較大犬缨。
解決方案:

  • 借助 WKNavigtionDelegate(僅適用iOS9以上)
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0));

當(dāng)WKWebView總體內(nèi)存占用過(guò)大喳魏,頁(yè)面即將白屏的時(shí)候,系統(tǒng)會(huì)調(diào)用上面的回調(diào)函數(shù)怀薛,我們?cè)谠摵瘮?shù)里執(zhí)行 [webView reload] (這個(gè)時(shí)候webView.URL取值尚不為nil)解決白屏問(wèn)題刺彩。在一些高內(nèi)存消耗的頁(yè)面可能會(huì)頻繁刷新當(dāng)前頁(yè)面,H5側(cè)也要做相應(yīng)的適配操作枝恋。

  • 檢測(cè) webView.title 是否為空(需在初始化時(shí)設(shè)置默認(rèn)webView.title)

并不是所有H5頁(yè)面白屏的時(shí)候都會(huì)調(diào)用上面的回調(diào)函數(shù)创倔,比如在一個(gè)高內(nèi)存消耗的H5頁(yè)面上present 系統(tǒng)相機(jī),拍照完畢后返回原來(lái)頁(yè)面的時(shí)候出現(xiàn)白屏現(xiàn)象(拍照過(guò)程消耗了大量?jī)?nèi)存焚碌,導(dǎo)致內(nèi)存緊張,WebContent Process 被系統(tǒng)掛起)十电,但上面的回調(diào)函數(shù)并沒(méi)有被調(diào)用知押。在WKWebView白屏的時(shí)候,另一種現(xiàn)象是webView.title會(huì)被置空, 因此鹃骂,可以在viewWillAppear的時(shí)候檢測(cè)webView.title是否為空來(lái) reload 頁(yè)面台盯。

綜合以上兩種方法可以解決絕大多數(shù)的白屏問(wèn)題。

2.Cookie問(wèn)題

2.1 Cookie的私有存儲(chǔ)問(wèn)題

業(yè)界普遍認(rèn)為WKWebView擁有自己的私有存儲(chǔ)畏线,不會(huì)將cookie存入到標(biāo)準(zhǔn)的cookie容器NSHTTPCookieStorage中静盅。
實(shí)踐發(fā)現(xiàn):在iOS 8上,當(dāng)頁(yè)面跳轉(zhuǎn)的時(shí)候寝殴,當(dāng)前頁(yè)面的cookie會(huì)寫入 NSHTTPCookieStorage 中蒿叠,而在iOS 10上明垢,JS執(zhí)行 document.cookie 或服務(wù)器 set-cookie 注入的cookie會(huì)很快同步到 NSHTTPCookieStorage 中.

FireFox工程師曾建議通過(guò) reset WKProcessPool來(lái)觸發(fā)cookie同步到 NSHTTPCookieStorage 中,實(shí)踐發(fā)現(xiàn)不起作用市咽,并可能會(huì)引發(fā)當(dāng)前頁(yè)面 session cookie 丟失等問(wèn)題袖外。

2.2 請(qǐng)求不會(huì)自動(dòng)帶上容器中Cookie問(wèn)題

WKWebView發(fā)起的請(qǐng)求不會(huì)自動(dòng)帶上存儲(chǔ)于 NSHTTPCookieStorage 容器中的cookie。
比如魂务,NSHTTPCookieStorage 中存儲(chǔ)了一個(gè)cookie:

name=Nicholas;value=test;domain=www.smallfan.net;expires=Sat, 02 May 2019 23:38:25 GMT曼验;

通過(guò)UIWebView發(fā)起請(qǐng)求http://www.smallfan.net,則請(qǐng)求頭會(huì)自動(dòng)帶上cookie: Nicholas=test粘姜;
而通過(guò)WKWebView發(fā)起請(qǐng)求http://www.smallfan.net鬓照,請(qǐng)求頭不會(huì)自動(dòng)帶上cookie: Nicholas=test。
解決方案:

  • A. WKWebView loadRequest 前孤紧,在request的header中設(shè)置cookie, 解決首個(gè)請(qǐng)求cookie帶不上的問(wèn)題豺裆。
WKWebView *webView = [WKWebView new];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://www.fxiaoke.com"]];
[request addValue:@"uid=1000" forHTTPHeaderField:@"Cookie"];
[webView loadRequest:request];
  • B.通過(guò)·document.cookie·設(shè)置cookie解決后續(xù)頁(yè)面(同域)Ajax、iframe請(qǐng)求的cookie問(wèn)題号显。
- (NSString *)shareHttpCookieFromStorage:(NSURL *)url {

    NSMutableArray *array = [NSMutableArray array];
    for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:url]) {

        NSString *value = [NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value];
        value = [NSString stringWithFormat:@"document.cookie = '%@'", value];
        [array addObject:value];
    }

    NSString *header = @"";
    header = [array componentsJoinedByString:@";"];

    return header;
}

- (void)addCookiesWithUrl:(NSURL *)url {
    WKUserContentController *userContentController = [WKUserContentController new]; 
    WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:[self shareHttpCookieFromStorage]  injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                                         forMainFrameOnly:NO];
    [controller addUserScript:cookieScript];
    [userContentController addUserScript:cookieScript];
    _wkWebView = [[WKWebView alloc] initWithFrame:self.bounds configuration:configuration];
    ...
}

注意:因?yàn)镹SHTTPCookieStorage是整個(gè)APP共享的單例臭猜,包含了所有domain的cookie,在WKWebView初始化時(shí)押蚤,務(wù)必提前獲取URL載入對(duì)應(yīng)的cookie蔑歌,防止因cookies泄漏造成可模仿登錄等安全漏洞。

B方案無(wú)法解決302請(qǐng)求(跨域)的cookie問(wèn)題揽碘,可以攔截頁(yè)面每次跳轉(zhuǎn)都會(huì)調(diào)用的回調(diào)函數(shù):
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler來(lái)實(shí)現(xiàn)復(fù)制request對(duì)象次屠,在request header中帶上cookie并重新 loadRequest

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

    NSMutableURLRequest *newReq = [navigationAction.request mutableCopy];
    NSMutableArray *array = [NSMutableArray array];
    for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:navagationAction.request.URL]) {
        NSString *value = [NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value];
        [array addObject:value];
    }

    NSString *cookie = [array componentsJoinedByString:@";"];
    [newReq setValue:cookie forHTTPHeaderField:@"Cookie"];
    [webView loadRequest:newReq];
}

缺陷:依然解決不了頁(yè)面iframe跨域請(qǐng)求的cookie問(wèn)題雳刺,畢竟-[WKWebView loadRequest:]只適合加載mainFrame請(qǐng)求劫灶。

2.3 WKProcessPool無(wú)法本地化保存(離線緩存)

蘋果開(kāi)發(fā)者文檔對(duì)WKProcessPool的定義是:A WKProcessPool object represents a pool of Web Content process. 通過(guò)讓所有WKWebView共享同一個(gè)WKProcessPool實(shí)例,可以實(shí)現(xiàn)多個(gè)WKWebView之間共享cookie數(shù)據(jù)掖桦。不過(guò)WKProcessPool實(shí)例在app殺進(jìn)程重啟后會(huì)被重置本昏,導(dǎo)致WKProcessPool中的cookie、session cookie數(shù)據(jù)丟失枪汪,目前也無(wú)法實(shí)現(xiàn)WKProcessPool實(shí)例本地化保存涌穆。
注意:由于WKWebView在請(qǐng)求過(guò)程中用戶可能退出界面銷毀對(duì)象,當(dāng)請(qǐng)求回調(diào)時(shí)由于接收處理對(duì)象不存在料饥,造成Bad Access crash蒲犬,所以可將WKProcessPool設(shè)為單例
附使用方式:

static WKProcessPool *_sharedWKProcessPoolInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    _sharedWKProcessPoolInstance = [[WKProcessPool alloc] init];
});

self.processPool = _sharedWKProcessPoolInstance;

WKWebViewConfiguration *configuration1 = [[WKWebViewConfiguration alloc] init];
configuration1.processPool = self.processPool;
WKWebView *webView1 = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration1];
...
WKWebViewConfiguration *configuration2 = [[WKWebViewConfiguration alloc] init];
configuration2.processPool = self.processPool;
WKWebView *webView2 = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration2];
...

3.NSURLProtocol問(wèn)題

WKWebView在獨(dú)立于 app 進(jìn)程之外的進(jìn)程中執(zhí)行網(wǎng)絡(luò)請(qǐng)求,請(qǐng)求數(shù)據(jù)不經(jīng)過(guò)主進(jìn)程岸啡,因此原叮,在WKWebView上直接使用 NSURLProtocol 無(wú)法攔截請(qǐng)求。蘋果開(kāi)源的 Webkit2 源碼暴露了私有API:

+ [WKBrowsingContextController registerSchemeForCustomProtocol:]

通過(guò)注冊(cè) http(s) scheme 后WKWebView將可以使用 NSURLProtocol 攔截http(s)請(qǐng)求:

//僅iOS8.4以上可用
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.4)  {
    Class cls = NSClassFromString(@"WKBrowsingContextController”); 
    SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");

    if ([(id)cls respondsToSelector:sel]) {
        #pragma clang diagnostic push
        #pragma clang diagnostic ignored "-Warc-performSelector-leaks"

            // 注冊(cè)http(s) scheme, 把 http和https請(qǐng)求交給 NSURLProtocol處理 
            [(id)cls performSelector:sel withObject:@"http"];
            [(id)cls performSelector:sel withObject:@"https"];

        #pragma clang diagnostic pop
        }
    }
}

這種方案目前存在以下嚴(yán)重缺陷:
post請(qǐng)求body數(shù)據(jù)被清空
由于WKWebView在獨(dú)立進(jìn)程里執(zhí)行網(wǎng)絡(luò)請(qǐng)求。一旦注冊(cè)http(s) scheme后奋隶,網(wǎng)絡(luò)請(qǐng)求將從Network Process發(fā)送到App Process擂送,這樣 NSURLProtocol 才能攔截網(wǎng)絡(luò)請(qǐng)求。在webkit2的設(shè)計(jì)里使用MessageQueue進(jìn)行進(jìn)程之間的通信唯欣,Network Process會(huì)將請(qǐng)求encode成一個(gè)Message,然后通過(guò) IPC 發(fā)送給 App Process嘹吨。出于性能的原因,encode的時(shí)候HTTPBody和HTTPBodyStream這兩個(gè)字段被丟棄掉了境氢。
因此蟀拷,如果通過(guò) registerSchemeForCustomProtocol 注冊(cè)了http(s) scheme, 那么由WKWebView發(fā)起的所有http(s)請(qǐng)求都會(huì)通過(guò) IPC 傳給主進(jìn)程 NSURLProtocol 處理,導(dǎo)致post請(qǐng)求body被清空萍聊。
解決方案:

3.1 如果沒(méi)有開(kāi)啟ATS

可以注冊(cè)customScheme, 比如smallfan://, 因此希望使用離線功能又不使用post方式的請(qǐng)求可以通過(guò) customScheme 發(fā)起請(qǐng)求问芬,比如 smallfan://webCache/HelloWorld ,然后在App進(jìn)程 NSURLProtocol 攔截這個(gè)請(qǐng)求并加載離線數(shù)據(jù)寿桨。不足:使用post方式的請(qǐng)求該方案依然不適用此衅,同時(shí)需要HTML5側(cè)修改請(qǐng)求scheme以及CSP規(guī)則。

3.2 如果開(kāi)啟ATS

因?yàn)椋阂坏┐蜷_(kāi)ATS開(kāi)關(guān):Allow Arbitrary Loads選項(xiàng)設(shè)置為NO亭螟,同時(shí)通過(guò) registerSchemeForCustomProtocol 注冊(cè)了http(s) scheme挡鞍,WKWebView發(fā)起的所有非https網(wǎng)絡(luò)請(qǐng)求將被阻塞(即便將Allow Arbitrary Loads in Web Content選項(xiàng)設(shè)置為YES)。
可通過(guò)hook所有的post請(qǐng)求的方式解決:

  • 對(duì)于Ajax post請(qǐng)求预烙,思路是通過(guò)XMLHttpRequest send及open方法墨微,將http body內(nèi)容拼裝在http header中并正常請(qǐng)求,App進(jìn)程 NSURLProtocol 攔截這個(gè)請(qǐng)求默伍,將header中的BODY內(nèi)容取出置于body欢嘿,發(fā)送請(qǐng)求衰琐,并將結(jié)果返回WKWebView(可借助 WebViewProxy 完成)也糊。
    JS文件:
var s_ajaxListener = new Object();
s_ajaxListener.tempOpen = XMLHttpRequest.prototype.open;
s_ajaxListener.tempSend = XMLHttpRequest.prototype.send;
s_ajaxListener.tempSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;

XMLHttpRequest.prototype.open = function(a,b) {
  this._method = a;
  s_ajaxListener.tempOpen.apply(this, arguments);
}

XMLHttpRequest.prototype.send = function(a,b) { 
  if (this._method && this._method.toLowerCase() == 'post') {
    a = encodeURIComponent(a)
    s_ajaxListener.tempSetRequestHeader.apply(this, ['BODY', a])
  }
  return s_ajaxListener.tempSend.apply(this, arguments);
}

APP攔截請(qǐng)求:

[WebViewProxy handleRequestsWithHttpHeader:@"BODY" handlerHash:[self hash] handler:^(NSURLRequest *req, WVPResponse *res) {

    NSURLSession *session = [NSURLSession sharedSession];
    NSMutableURLRequest *request = [req mutableCopy];
    request.HTTPMethod = @"POST";
    NSString *postData = request.allHTTPHeaderFields[@"BODY"];
    NSString *decodePostData = (__bridge_transfer NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, (__bridge CFStringRef)postData, CFSTR(""), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));
    NSString *httpBody = decodePostData;
    request.HTTPBody = [httpBody dataUsingEncoding:NSUTF8StringEncoding];
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
        [res respondWithNSData:data mimeType:response.MIMEType header:headers statusCode:((NSHTTPURLResponse *)response).statusCode];

    }];
    [dataTask resume];
}];
  • 對(duì)于form請(qǐng)求,解決方式類似Ajax羡宙,只是將BODY轉(zhuǎn)編碼后拼接到URL中狸剃,APP進(jìn)程處理方式一致。

JS文件:

var s_formListener = window.onsubmit;
window.onsubmit = function (e) {
var node = e.srcElement;
if (node && node.tagName && node.tagName.toLowerCase() === 'form') {
    if (node.method && node.method.toLowerCase() === 'post') {
        var elements = [].slice.call(node.elements);
        var tempData = [], entryName, entryValue;
        for (var i = 0, l = elements.length; i < l; ++i) {
            entryName = elements[i].name;
            entryValue = elements[i].value;
            if (entryValue.toString() === '[object File]') {
                entryValue = entryValue.name;
            }
            tempData.push(encodeURIComponent(entryName) + '=' + encodeURIComponent(entryValue));
        }
        tempData = tempData.join('&');

        var action = node.action || location.href;
        var hashIndex = action.indexOf('#');
        if (hashIndex >= 0) {
            action = action.substring(0, hashIndex);
        }
        var queryIndex = action.indexOf('?');
        if (queryIndex >= 0) {
            action = action + '&POST_DATA=' + encodeURIComponent(tempData);
        } else {
            action = action + '?POST_DATA=' + encodeURIComponent(tempData);
        }

        node.action = action;
    }
}
if (s_formListener && s_formListener.apply) {
    s_formListener.apply(this, arguments);
}
}

APP攔截請(qǐng)求:

[WebViewProxy handleRequestsWithHttpHeader:@"POST_DATA" handler:^(NSURLRequest *req, WVPResponse *res) {

    NSURLSession *session = [NSURLSession sharedSession];
    NSMutableURLRequest *request = [req mutableCopy];
    request.HTTPMethod = @"POST";
    NSString *postData = request.allHTTPHeaderFields[@"POST_DATA"];
    NSString *decodePostData = (__bridge_transfer NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, (__bridge CFStringRef)postData, CFSTR(""), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));
    NSString *httpBody = decodePostData;
    request.HTTPBody = [httpBody dataUsingEncoding:NSUTF8StringEncoding];
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

        NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
        [res respondWithNSData:data mimeType:response.MIMEType header:headers statusCode:((NSHTTPURLResponse *)response).statusCode];

    }];
    [dataTask resume];
}];

缺陷:Http header value及url字符有長(zhǎng)度限制狗热。在WWDC 2017上钞馁,提到iOS 11將開(kāi)放一個(gè) WKURLSchemeHandler 注冊(cè),提供custom response的能力匿刮,拭目以待僧凰。

4.JavaScript交互

4.1 WKWebView調(diào)用JavaScript

[_wkWebView evaluateJavaScript:@"Hello" completionHandler:^(NSString result, NSError * _Nullable error) {
    if([result isEqualToString:@"Hi"]) {
    }
}];

4.2 JavaScript調(diào)用WKWebView

 WKWebViewConfiguration * Configuration = [[WKWebViewConfiguration alloc] init];
 WKUserContentController *userContentController = [[WKUserContentController alloc] init];

//注冊(cè)一個(gè)name為HelloNative的js方法
[userContentController addScriptMessageHandler:self  name:@"HelloNative"];

Configuration.userContentController = userContentController;
_wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, 300,500) configuration:Configuration];
#pragma mark WKScriptMessageHandler
//設(shè)置WKWebView的WKScriptMessageHandler代理方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message {
    if ([message.name isEqualToString:@"HelloNative"]) {
        // 打印所傳過(guò)來(lái)的參數(shù),只支持NSNumber, NSString, NSDate, NSArray, NSDictionary, NSNull類型
        NSLog(@"%@", message.body);
    }
}

JS調(diào)用:

window.webkit.messageHandlers.HelloNative.postMessage(message);

注意:關(guān)閉web頁(yè)時(shí)熟丸,需要調(diào)用removeScriptMessageHandlerForName以防止內(nèi)存泄漏训措。

- (void)dealloc {
    _wkWebView.UIDelegate = nil;
    _wkWebView.navigationDelegate = nil;
    [[_wkWebView configuration].userContentController removeScriptMessageHandlerForName:@"HelloNative"];
}

5.Crash問(wèn)題

5.1 JS調(diào)用window.alert()函數(shù)引起的crash

當(dāng)JS調(diào)用alert函數(shù)時(shí),WKWebView使用如下方法回調(diào):

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

主要原因是上述 completionHandler 沒(méi)有被調(diào)用導(dǎo)致的。在適配WKWebView的時(shí)候绩鸣,我們需要自己實(shí)現(xiàn)該回調(diào)函數(shù)怀大,window.alert()才能調(diào)起alert框。
解決方案:

- (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();
}

5.2 WKWebView 退出前調(diào)用evaluateJavaScript:completionHandler:引起的crash

主要原因WKWebView退出并被釋放后導(dǎo)致 completionHandler 變成野指針呀闻,而此時(shí) JavaScriptCore還在執(zhí)行JS代碼化借,待JavaScriptCore執(zhí)行完畢后會(huì)調(diào)用 completionHandler() ,導(dǎo)致crash捡多。
這個(gè)crash只發(fā)生在iOS 8系統(tǒng)上蓖康,iOS9以上主要是對(duì)completionHandler block做了copy。

解決方案:
通過(guò)在completionHandlerretain``WKWebView防止completionHandler被過(guò)早釋放垒手。

+ (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);
        }
    }];
}

6.進(jìn)度條問(wèn)題

在UIWebView中钓瞭,進(jìn)度條一直是個(gè)缺陷問(wèn)題,盡管有 NJKWebViewProgress 一類開(kāi)源組件淫奔,但在精確處理加載完成上還有些許不足(部分頁(yè)面可見(jiàn)webViewDidStartLoad:與webViewDidFinishLoad:不成對(duì)回調(diào)山涡,導(dǎo)致進(jìn)度條加載結(jié)果計(jì)算有誤)。而WKWebView上增加了一個(gè)estimatedProgress屬性唆迁,通過(guò)KVO可實(shí)現(xiàn)精準(zhǔn)進(jìn)度條控制鸭丛。iOS WKWebView添加類似微信的進(jìn)度條
通過(guò)對(duì)微信的觀察,進(jìn)度條的精確結(jié)果并不是首要的唐责,良好的用戶心態(tài)預(yù)期才是其重點(diǎn)鳞溉。所以,可以考慮以如下方式實(shí)現(xiàn)“虛擬”的進(jìn)度條鼠哥。

- (void)startProgress {
    if (_hideProgress) {
        return;
    }

    if (_progress == 0) {
        _progress = 0.9;

        [_progressLayer removeAllAnimations];//清除所有的動(dòng)畫(huà)

        CGRect frame = _progressView.frame;
        CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
        //設(shè)置關(guān)鍵幀數(shù)組
        animation.values=@[[NSValue valueWithCGPoint:CGPointMake(0, 0)],
                           [NSValue valueWithCGPoint:CGPointMake(0.7 * frame.size.width, .0)],
                           [NSValue valueWithCGPoint:CGPointMake(0.9 * frame.size.width, .0)],
                           [NSValue valueWithCGPoint:CGPointMake(1.0 * frame.size.width, .0)]];
        //設(shè)置每個(gè)關(guān)鍵幀對(duì)應(yīng)的時(shí)間點(diǎn)熟菲,取值為0~1
        animation.keyTimes = @[[NSNumber numberWithFloat:.0],
                               [NSNumber numberWithFloat:.3],
                               [NSNumber numberWithFloat:.7],
                               [NSNumber numberWithFloat:1.]];
        animation.removedOnCompletion = YES;
        animation.fillMode = kCAFillModeForwards;
        animation.duration = 20;
        animation.delegate = self;
        [_progressLayer addAnimation:animation forKey:@"startProgress"];
    }
}

- (void)completeProgress {
    if (_hideProgress) {
        return;
    }

    CGPoint point = _progressLayer.presentationLayer.position;//當(dāng)前動(dòng)畫(huà)的position
    if (round(point.x) == 0 && !_progress) {
        return;
    }
    [_progressLayer removeAnimationForKey:@"startProgress"];

    _progress = 1.0;

    CGRect frame = _progressView.frame;
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    //設(shè)置關(guān)鍵幀數(shù)組
    animation.values=@[[NSValue valueWithCGPoint:point],
                       [NSValue valueWithCGPoint:CGPointMake(1.0 * frame.size.width, .0)]];
    //設(shè)置每個(gè)關(guān)鍵幀對(duì)應(yīng)的時(shí)間點(diǎn)
    animation.keyTimes = @[[NSNumber numberWithFloat:.0],
                           [NSNumber numberWithFloat:1.]];
    animation.duration = .27;
    animation.removedOnCompletion = YES;
    animation.fillMode = kCAFillModeForwards;
    [_progressLayer addAnimation:animation forKey:@"completeProgress"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    // 當(dāng)WKWebView回調(diào)webView:didFinishNavigation:時(shí),頁(yè)面實(shí)際上渲染并未完成
    // 監(jiān)聽(tīng)loading屬性變化朴恳,可精確判斷請(qǐng)求完成+渲染完成
    if ([keyPath isEqualToString:@"loading"]) {

        BOOL oldLoading = [[change objectForKey:NSKeyValueChangeOldKey] boolValue];
        BOOL newLoading = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];

        if (newLoading) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [self startProgress];
            });
        } else if (!newLoading) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [self completeProgress];
                _progress = 0;
            });
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (void)dealloc {
    _wkWebView.UIDelegate = nil;
    _wkWebView.navigationDelegate = nil;

    //記得在銷毀時(shí)釋放監(jiān)聽(tīng)
    [_wkWebView removeObserver:self forKeyPath:@"loading"];
}

7.截屏問(wèn)題

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

使用以上方法對(duì)webGL頁(yè)面的截屏結(jié)果不是空白就是純黑圖片抄罕。

解決方案:約定一個(gè)JS接口,讓H5實(shí)現(xiàn)該接口于颖,具體是通過(guò) canvas getImageData() 方法取得圖片數(shù)據(jù)后返回 base64 格式的數(shù)據(jù)呆贿,客戶端在需要截圖的時(shí)候,調(diào)用這個(gè)JS接口獲取 base64 String 并轉(zhuǎn)換成 UIImage 森渐。

8.其他問(wèn)題

8.1 iOS8.2以下音頻無(wú)法停止

解決方案多種:

//第一種:返回時(shí)請(qǐng)求一個(gè)空頁(yè)面
 [NSURL URLWithString:@"about:blank"]

//第二種:返回時(shí)加載一個(gè)空的HTML String
[_wkWebView loadHTMLString:@"<html/>" baseURL:nil]

//第三種:在viewDidDisappear方法里播放無(wú)聲的音頻再暫停

8.2 視頻沒(méi)有自動(dòng)播放

解決方案:
WKWebView需要通過(guò) WKWebViewConfiguration.mediaPlaybackRequiresUserAction 設(shè)置是否允許自動(dòng)播放做入,但一定要在WKWebView初始化之前設(shè)置,在WKWebView初始化之后設(shè)置無(wú)效同衣。

8.3 頁(yè)面回退問(wèn)題

  • 業(yè)務(wù)上的需求竟块,當(dāng)最后只有一條歷史,直接 pop 回去耐齐,需要如下改寫浪秘。
- (BOOL)canGoBack {
    if (self.backForwardList.backList.count <= 1) {
        return NO;
    }
    return YES;
}
  • WKWebView上調(diào)用 -[WKWebView goBack] , 回退到上一個(gè)頁(yè)面后不會(huì)觸發(fā) window.onload() 函數(shù)前弯、不會(huì)執(zhí)行JS。

8.4 頁(yè)面回退時(shí)秫逝,字體變大

解決方案:
在頁(yè)面 webView:didFinishNavigation: 中執(zhí)行以下JavaScript將webkit中字體還原到100%

[self evaluateJavaScript:@"document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust= '100%'" completionHandler:nil];

參考文獻(xiàn):
1. 騰訊Bugly團(tuán)隊(duì)【W(wǎng)KWebView 那些坑】

歡迎關(guān)注我的簡(jiǎn)書(shū)恕出,我是程序猿小風(fēng)扇,請(qǐng)多多指教
Github:Smallfan

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末违帆,一起剝皮案震驚了整個(gè)濱河市浙巫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌刷后,老刑警劉巖的畴,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異尝胆,居然都是意外死亡丧裁,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門含衔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)煎娇,“玉大人,你說(shuō)我怎么就攤上這事贪染』呵海” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵杭隙,是天一觀的道長(zhǎng)哟绊。 經(jīng)常有香客問(wèn)我,道長(zhǎng)痰憎,這世上最難降的妖魔是什么票髓? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮铣耘,結(jié)果婚禮上洽沟,老公的妹妹穿的比我還像新娘。我一直安慰自己涡拘,他們只是感情好玲躯,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著鳄乏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棘利。 梳的紋絲不亂的頭發(fā)上橱野,一...
    開(kāi)封第一講書(shū)人閱讀 49,760評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音善玫,去河邊找鬼水援。 笑死密强,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蜗元。 我是一名探鬼主播或渤,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼奕扣!你這毒婦竟也來(lái)了薪鹦?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤惯豆,失蹤者是張志新(化名)和其女友劉穎池磁,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體楷兽,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡地熄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了芯杀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片端考。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖揭厚,靈堂內(nèi)的尸體忽然破棺而出跛梗,到底是詐尸還是另有隱情,我是刑警寧澤棋弥,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布核偿,位于F島的核電站,受9級(jí)特大地震影響顽染,放射性物質(zhì)發(fā)生泄漏漾岳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一粉寞、第九天 我趴在偏房一處隱蔽的房頂上張望尼荆。 院中可真熱鬧,春花似錦唧垦、人聲如沸捅儒。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)巧还。三九已至,卻和暖如春坊秸,著一層夾襖步出監(jiān)牢的瞬間麸祷,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工褒搔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留阶牍,地道東北人喷面。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像走孽,于是被迫代替她去往敵國(guó)和親惧辈。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容