本文系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ò)封裝繼承 UIView
的 SFWebView
,實(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ò)在completionHandler
里retain``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