有了UIWebView,為什么還需要WKWebView?
UIWebVieW的缺點(diǎn): 笨重難用、內(nèi)存泄露、內(nèi)存消耗大,性能差 —— WKWebView提高性能
WKWebView 擁有60fps滾動(dòng)刷新率和safari相同的js引擎等優(yōu)勢(shì)。
原生和Web的交互
JS調(diào)用OC
方法一:
1、動(dòng)態(tài)注入JS方法
//在OC中添加一個(gè)scriptMessageHandler,添加處理消息,
//self指代的對(duì)象需要遵守WKScripteMessageHandler協(xié)議,結(jié)束時(shí)候需要移除
[userContentController addScriptMessageHandler:self name:@"share"];
2、當(dāng)JS中調(diào)用share方法時(shí)候
windnow.webkit.messageHandlers.share.postMessage("參數(shù)")
在OC中會(huì)收到WKScriptMessageHandler的回調(diào)
#pragma mark -- WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.name isEqualToString:@"share"]) {
NSLog(@"message.body=%@", message.body);
}
}
方法二:
通過(guò)url的scheme來(lái)進(jìn)行判斷 ,然后再下面的代理方法中處理
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
// 通過(guò)定義的scheme來(lái)進(jìn)行處理
decisionHandler(WKNavigationActionPolicyCancel);
}
方法三
/**
* JS 調(diào)用 OC ,關(guān)閉當(dāng)前 H5 控制器
*/
[_bridge registerHandler:@"_app_closeWebView" handler:^(id data, WVJBResponseCallback responseCallback) {
[weakSelf closeTheView];
responseCallback(@"OK,已關(guān)閉當(dāng)前 WebView ");
}];
/**
* OC 調(diào)用 JS ,獲取 OC 的值
*/
[_bridge callHandler:@"_app_getToken" data:@"userToken"];
js寫的橋接文件
/**
* 使用 WebViewJavascriptBridge 實(shí)現(xiàn) OC 與 JS 交互
*/
- (void)setUpWebViewJavascriptBridge {
/**
JS 調(diào)用 OC ,設(shè)置導(dǎo)航條 title
@param data 后臺(tái) JS 頁(yè)面?zhèn)鬟^(guò)來(lái)的參數(shù)
@param registerHandler 要注冊(cè)的事件名稱(這里我們?yōu)?locationAlertViewWithMessage)
@param handler 回調(diào) block 函數(shù) 當(dāng)后臺(tái)觸發(fā)這個(gè)事件的時(shí)候會(huì)執(zhí)行 block 里面的代碼
*/
[_bridge registerHandler:@"_app_setTitle" handler:^(id data, WVJBResponseCallback responseCallback) {
if (data) {
NSDictionary *dic = (NSDictionary *)data;
weakSelf.title = dic[@"title"];
}
// responseCallback 給后臺(tái) JS 的回復(fù)
responseCallback(@"OK,已收到標(biāo)題信息!");
}];
/**
* JS 調(diào)用 OC ,關(guān)閉當(dāng)前 H5 控制器
*/
[_bridge registerHandler:@"_app_closeWebView" handler:^(id data, WVJBResponseCallback responseCallback) {
[weakSelf closeTheView];
responseCallback(@"OK,已關(guān)閉當(dāng)前 WebView ");
}];
/**
* OC 調(diào)用 JS ,獲取 OC 的值
*/
[_bridge callHandler:@"_app_getToken" data:@"userToken"];
}
OC調(diào)用異步j(luò)s方法,并獲取js的返回值,在呢么實(shí)現(xiàn)?
主要的是阻塞主線程
- (void)nativeCallJS:(NSString *)func para:(NSString *)para block:(void (^)(id))block{
//在主線程調(diào)用
__block BOOL end = NO;
[self.bridge callHandler:func data:para responseCallback:^(id responseData) {
NSLog ( @"from js: %@" , responseData ) ;
block(responseData);
end = YES;
}];
while (!end) {
//阻塞主線程
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
官方提供的異步調(diào)用js的方法
- (void)callAsyncJavaScript:(NSString *)functionBody arguments:(nullable NSDictionary<NSString *, id> *)arguments inFrame:(nullable WKFrameInfo *)frame inContentWorld:(WKContentWorld *)contentWorld completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler NS_REFINED_FOR_SWIFT API_AVAILABLE(macos(11.0), ios(14.0));
js調(diào)用OC,并需要異步回調(diào)結(jié)果怎么處理的?
//別忘了,在configuration中的userContentController中添加scriptMessageHandler
//[controller addScriptMessageHandler:self name:@"share"]; //記得適當(dāng)時(shí)候remove哦
//JS調(diào)用share方法時(shí),則會(huì)調(diào)用下面的方法
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"share"]) {
NSDictionary *shareData = message.body;
//模擬異步回調(diào)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//讀取js function的字符串
NSString *jsFunctionString = shareData[@"result"];
//拼接調(diào)用該方法的js字符串
NSString *callbackJs = [NSString stringWithFormat:@"(%@)(%d);", jsFunctionString, NO]; //后面的參數(shù)NO為模擬分享失敗
[self.webView evaluateJavaScript:callbackJs completionHandler:^(id _Nullable result, NSError * _Nullable error) {
if (!error) {
NSLog(@"模擬回調(diào),分享失敗");
}
}];
});
}
}
對(duì)應(yīng)的JS代碼
/**
* 分享方法,并且會(huì)異步回調(diào)分享結(jié)果
* @param {對(duì)象類型} shareData 一個(gè)分享數(shù)據(jù)的對(duì)象,包含title,imgUrl,link以及一個(gè)回調(diào)function
* @return {void} 無(wú)同步返回值
*/
function shareNew(shareData) {
//這是該方法的默認(rèn)實(shí)現(xiàn),上篇文章中有所提及
var title = shareData.title;
var imgUrl = shareData.imgUrl;
var link = shareData.link;
var result = shareData.result;
//do something
//這里模擬異步操作
setTimeout(function() {
//2s之后,回調(diào)true分享成功
result(true);
}, 2000);
//用于WKWebView,因?yàn)閃KWebView并沒(méi)有辦法把js function傳遞過(guò)去,因此需要特殊處理一下
//把js function轉(zhuǎn)換為字符串,oc端調(diào)用時(shí) (<js function string>)(true); 即可
shareData.result = result.toString();
window.webkit.messageHandlers.shareNew.postMessage(shareData);
}
function test() {
//清空分享結(jié)果
shareResult.innerHTML = "";
//調(diào)用時(shí),應(yīng)該
share({
title: "title",
imgUrl: "http://img.dd.com/xxx.png",
link: location.href,
result: function(res) {
//這里shareResult 等同于 document.getElementById("shareResult")
shareResult.innerHTML = res ? "success" : "failure";
}
});
}
關(guān)鍵點(diǎn):
- 可以采用異步回調(diào)的方式,將返回值返回給js
- 一般js的參數(shù)中包含function是為了異步回調(diào),這里我們可以把js的function轉(zhuǎn)換為字符串,再傳遞給OC。
1、WKWebView 白屏問(wèn)題
WKWebView是一個(gè)多進(jìn)程的組件,Network Loading以及UI Rendering在其他進(jìn)程中執(zhí)行。初次適配WKWebView的時(shí)候,我們也驚訝于打開(kāi)WKWebView后, App進(jìn)程內(nèi)存消耗反而大幅度下降,但仔細(xì)觀察會(huì)發(fā)現(xiàn),Other Process的內(nèi)存占用會(huì)增加。在一些用webGL渲染的復(fù)雜頁(yè)面,使用WKWebView總體的內(nèi)存占用【App process Memory + other Process Memory】,不見(jiàn)得比UIWebView少很多。
UIWebView上當(dāng)內(nèi)存占用太大的時(shí)候, App Process會(huì)crash;而在WKWebView上當(dāng)總體的內(nèi)存占用比較大的時(shí)候,WebContent Process會(huì)crash, 從而出現(xiàn)白屏現(xiàn)象
這個(gè)時(shí)候WKWebView.URL會(huì)變成nil,簡(jiǎn)單的reload刷新操作已經(jīng)失效,對(duì)于一些長(zhǎng)駐的H5頁(yè)面影響比較大。
解決方案:
《1》借助WKNavigationDelegate
iOS9之后增加的回調(diào)函數(shù)
/*! @abstract Invoked when the web view's web content process is terminated.
@param webView The web view whose underlying web content process was terminated.
*/
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macos(10.11), ios(9.0));
在WKWebView總體內(nèi)存占用過(guò)大的時(shí)候,頁(yè)面即將出現(xiàn)白屏,在上面這個(gè)系統(tǒng)回調(diào)方法中執(zhí)行[webview reload]來(lái)解決白屏問(wèn)題。
《2》檢測(cè)webView.title是否為空
并不是所有的H5頁(yè)面白屏的時(shí)候都會(huì)調(diào)用上面的回調(diào)函數(shù);
場(chǎng)景:最近遇到的一個(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è)面。
2、WKWebView Cookie問(wèn)題
2.1、WKWebView Cookie存儲(chǔ)
業(yè)界普遍認(rèn)為 WKWebView 擁有自己的私有存儲(chǔ),不會(huì)將 Cookie 存入到標(biāo)準(zhǔn)的 Cookie 容器 NSHTTPCookieStorage 中。
實(shí)踐發(fā)現(xiàn) WKWebView 實(shí)例其實(shí)也會(huì)將 Cookie 存儲(chǔ)于 NSHTTPCookieStorage 中,但存儲(chǔ)時(shí)機(jī)有延遲,在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 中,F(xiàn)ireFox 工程師曾建議通過(guò) reset WKProcessPool 來(lái)觸發(fā) Cookie 同步到 NSHTTPCookieStorage 中,實(shí)踐發(fā)現(xiàn)不起作用,并可能會(huì)引發(fā)當(dāng)前頁(yè)面 session cookie 丟失等問(wèn)題。
WKWebView Cookie 問(wèn)題在于 WKWebView 發(fā)起的請(qǐng)求不會(huì)自動(dòng)帶上存儲(chǔ)于 NSHTTPCookieStorage 容器中的 Cookie。
比如,NSHTTPCookieStorage 中存儲(chǔ)了一個(gè) Cookie:
name=Nicholas;value=test;domain=y.qq.com;expires=Sat, 02 May 2019 23:38:25 GMT;
通過(guò) UIWebView 發(fā)起請(qǐng)求http://y.qq.com, 則請(qǐng)求頭會(huì)自動(dòng)帶上 cookie: Nicholas=test;
而通過(guò) WKWebView發(fā)起請(qǐng)求http://y.qq.com, 請(qǐng)求頭不會(huì)自動(dòng)帶上 cookie: Nicholas=test。
2.2、WKProcessPool
*WKProcessPool定義:A WKProcessPool object represents a pool of Web Content process。
通過(guò)讓所有 WKWebView 共享同一個(gè) WKProcessPool 實(shí)例,可以實(shí)現(xiàn)多個(gè) WKWebView 之間共享 Cookie(session Cookie and persistent Cookie)數(shù)據(jù)。 不過(guò)WKWebView WkProcessPool實(shí)例在app殺進(jìn)程重啟后會(huì)被重置,導(dǎo)致WKProcessPool 中的cookie/session Cookie數(shù)據(jù)丟失,目前也無(wú)法實(shí)現(xiàn)WKProcessPool實(shí)例本地化保存。
2.3 Workround
H5的業(yè)務(wù)都是依賴于Cookie作登陸態(tài)校驗(yàn),而WKWebView上請(qǐng)求不會(huì)自動(dòng)攜帶Cookie,目前的主要解決方案是:
《1》WKWebView loadRequest前,在request header中設(shè)置Cookie,解決首個(gè)請(qǐng)求Cookie帶不上的問(wèn)題
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];
《2》通過(guò)document.cookie設(shè)置Cookie解決后續(xù)頁(yè)面(同域)Ajax, iframe 請(qǐng)求的cookie問(wèn)題
注意:document.cookie() 無(wú)法跨域設(shè)置cookie
WKUserContentController* userContentController = [WKUserContentController new];
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie = 'skey=skeyValue';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
這種方案無(wú)法解決302請(qǐng)求的Cookie問(wèn)題,比如:第一個(gè)請(qǐng)求時(shí)www.a.com,我們通過(guò)在request header里帶上Cookie解決該請(qǐng)求的Cookie問(wèn)題,接著頁(yè)面302跳轉(zhuǎn)到www.b.com, 這個(gè)時(shí)候www.b.com 這個(gè)請(qǐng)求就可能因?yàn)闆](méi)有攜帶cookie而無(wú)法訪問(wèn)。當(dāng)然,由于每一次頁(yè)面跳轉(zhuǎn)都會(huì)調(diào)用回調(diào)函數(shù):
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
可以在該回調(diào)函數(shù)里攔截302請(qǐng)求,copy request, 在request header中帶上cookie并重新loadRequest。不過(guò)這種方法依然解決不了頁(yè)面的iframe跨域請(qǐng)求的cookie問(wèn)題,畢竟-[WKWebView loadRequest]只適合加載mainiFrame請(qǐng)求。
3、WKWebView 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上直接使用NSURLProcol無(wú)法攔截請(qǐng)求。
蘋果開(kāi)源的WebKit源碼暴露了私有API
+ [WKBrowsingContextController registerSchemeForCustomProtocol:]
通過(guò)注冊(cè)http(s) scheme 后, WKWebView將可以使用NSURLProtocol攔截http(s)請(qǐng)求:
Class cls = NSClassFromString(@"WKBrowsingContextController”);
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([(id)cls respondsToSelector:sel]) {
// 注冊(cè)http(s) scheme, 把 http和https請(qǐng)求交給 NSURLProtocol處理
[(id)cls performSelector:sel withObject:@"http"];
[(id)cls performSelector:sel withObject:@"https"];
}
缺點(diǎn):
《1》post請(qǐng)求body數(shù)據(jù)被清空
由于WKWebView在獨(dú)立進(jìn)程里網(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ò)registerSchemeForCustomProcol注冊(cè)了http(s) scheme,那么由WKWebView發(fā)起的所有http(s)請(qǐng)求都會(huì)通過(guò)IPC傳給進(jìn)程N(yùn)SURLProtocol處理,導(dǎo)致post請(qǐng)求body被清空;
《2》對(duì)ATS支持不足
打開(kāi)ATS開(kāi)關(guān): Allow Arbitrary Loads選項(xiàng)設(shè)置為NO,同時(shí)通過(guò)registerSchemeForCustomProtocol注冊(cè)了http(s) scheme;WKWebView發(fā)起的所有http網(wǎng)絡(luò)請(qǐng)求將被阻塞(即便將Allow Arbitrary Loads in Web Content 選項(xiàng)設(shè)置為YES)
WKWebView可以注冊(cè)customScheme,比如dynamic://,因此希望使用離線功能,又不使用post方式的請(qǐng)求可以通過(guò)customScheme發(fā)起請(qǐng)求,eg:dynamic://www.dynamicalbumlocalimage.com/,然后在 app 進(jìn)程 NSURLProtocol 攔截這個(gè)請(qǐng)求并加載離線數(shù)據(jù)。不足: 使用post方式的請(qǐng)求該方案依然不適用,同時(shí)需要H5側(cè)修改請(qǐng)求scheme以及CSP規(guī)則。
4、WKWebView loadRequest問(wèn)題
在WKWebView上通過(guò)loadRequest發(fā)起的post請(qǐng)求body數(shù)據(jù)會(huì)丟失;
//同樣是由于進(jìn)程間通信性能問(wèn)題,HTTPBody字段被丟棄
[request setHTTPMethod:@"POST"];
[request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]];
[wkwebview loadRequest: request];
workround:
假設(shè)想通過(guò)-[WKWebView loadRequest:]加載post請(qǐng)求,request1: http://h5.qzone.qq.com/mqzone/index,可以通過(guò)以下步驟實(shí)現(xiàn):
1、替換請(qǐng)求scheme,生成新的post請(qǐng)求request2:post://h5.qzone.qq.com/mqzone/index,同時(shí)將request1的body字段復(fù)制到request2的header中(WebKit不會(huì)丟棄header字段)
2、通過(guò)-[WKWebView loadRequest:]加載新的post請(qǐng)求request2;
3、通過(guò) +[WKbrowsingContextController registerSchemeForCustom Protocol:]注冊(cè)scheme:post://;
4、注冊(cè) NSURLProtocol 攔截請(qǐng)求post://h5.qzone.qq.com/mqzone/index ,替換請(qǐng)求 scheme, 生成新的請(qǐng)求 request3: http://h5.qzone.qq.com/mqzone/index,將 request2 header的body 字段復(fù)制到 request3 的 body 中,并使用 NSURLConnection 加載 request3,最后通過(guò) NSURLProtocolClient 將加載結(jié)果返回 WKWebView;
5、WKWebView頁(yè)面樣式問(wèn)題
適配過(guò)程中,發(fā)現(xiàn)h5頁(yè)面元素位置向下偏移或被拉伸變形,追蹤后發(fā)現(xiàn)主要是h5頁(yè)面高度值異常導(dǎo)致:
1.問(wèn)題: 空間H5頁(yè)面有透明導(dǎo)航、透明導(dǎo)航下拉刷新、全屏等需求,因此之前 webView 整個(gè)是從(0, 0)開(kāi)始布局,通過(guò)調(diào)整webView.scrollView.contentInset 來(lái)適配特殊導(dǎo)航欄需求。而在 WKWebView 上對(duì) contentInset 的調(diào)整會(huì)反饋到webView.scrollView.contentSize.height的變化上,比如設(shè)置 webView.scrollView.contentInset.top = a,那么contentSize.height的值會(huì)增加a,導(dǎo)致H5頁(yè)面長(zhǎng)度增加,頁(yè)面元素位置向下偏移;
解決方案是:調(diào)整WKWebView布局方式,避免調(diào)整webView.scrollView.contentInset。實(shí)際上,即便在 UIWebView 上也不建議直接調(diào)整webView.scrollView.contentInset的值,這確實(shí)會(huì)帶來(lái)一些奇怪的問(wèn)題。如果某些特殊情況下非得調(diào)整 contentInset 不可的話,可以通過(guò)下面方式讓H5頁(yè)面恢復(fù)正常顯示:
/**設(shè)置contentInset值后通過(guò)調(diào)整webView.frame讓頁(yè)面恢復(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);
2、接入now直播問(wèn)題: 在接入 now 直播的時(shí)候,我們發(fā)現(xiàn)在 iOS 9 上 WKWebView 會(huì)出現(xiàn)頁(yè)面被拉伸變形的情況,最后發(fā)現(xiàn)是window.innerHeight值不準(zhǔn)確導(dǎo)致(在WKWebView上返回了一個(gè)非常大的值),而H5同學(xué)通過(guò)獲取window.innerHeight來(lái)設(shè)置頁(yè)面高度,導(dǎo)致頁(yè)面整體被拉伸。通過(guò)查閱相關(guān)資料發(fā)現(xiàn),這個(gè)bug只在 iOS 9 的幾個(gè)系統(tǒng)版本上出現(xiàn),蘋果后來(lái)fix了這個(gè)bug。我們最后的解決方案是:延遲調(diào)用window.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截屏問(wèn)題
空間玩吧H5小游戲有截屏分享的功能,WKWebView 下通過(guò) -[CALayer renderInContext:]實(shí)現(xiàn)截屏的方式失效,需要通過(guò)以下方式實(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 頁(yè)面的截屏問(wèn)題,筆者已經(jīng)翻遍蘋果文檔,研究過(guò) webKit2 源碼里的截屏私有API,依然沒(méi)有找到合適的解決方案,同時(shí)發(fā)現(xiàn) Safari 以及 Chrome 這兩個(gè)全量切換到 WKWebView 的瀏覽器也存在同樣的問(wèn)題:對(duì)webGL 頁(yè)面的截屏結(jié)果不是空白就是純黑圖片。無(wú)奈之下,我們只能約定一個(gè)JS接口,讓游戲開(kāi)發(fā)商實(shí)現(xiàn)該接口,具體是通過(guò) canvas getImageData()方法取得圖片數(shù)據(jù)后返回 base64 格式的數(shù)據(jù),客戶端在需要截圖的時(shí)候,調(diào)用這個(gè)JS接口獲取 base64 String 并轉(zhuǎn)換成 UIImage。
7、WKWebView crash問(wèn)題
WKWebView 放量后,外網(wǎng)新增了一些 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調(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 沒(méi)有被調(diào)用導(dǎo)致的。在適配 WKWebView 的時(shí)候,我們需要自己實(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 退出的時(shí)候,JS剛好執(zhí)行了window.alert(), alert 框可能彈不出來(lái),completionHandler 最后沒(méi)有被執(zhí)行,導(dǎo)致 crash;另一種情況是在 WKWebView 一打開(kāi),JS就執(zhí)行window.alert(),這個(gè)時(shí)候由于 WKWebView 所在的 UIViewController 出現(xiàn)(push或present)的動(dòng)畫尚未結(jié)束,alert 框可能彈不出來(lái),completionHandler 最后沒(méi)有被執(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 下彈 alert 框的 crash,WKWebView 下彈 confirm 框的 crash 的原因與解決方式與 alert 類似。
另一個(gè) crash 發(fā)生在 WKWebView 退出前調(diào)用:
-[WKWebView evaluateJavaScript: completionHandler:]
執(zhí)行JS代碼的情況下。WKWebView 退出并被釋放后導(dǎo)致completionHandler變成野指針,而此時(shí) javaScript Core 還在執(zhí)行JS代碼,待 javaScript Core 執(zhí)行完畢后會(huì)調(diào)用completionHandler(),導(dǎo)致 crash。這個(gè) crash 只發(fā)生在 iOS 8 系統(tǒng)上,參考Apple Open Source,在iOS9及以后系統(tǒng)蘋果已經(jīng)修復(fù)了這個(gè)bug,主要是對(duì)completionHandler block做了copy(refer: https://trac.webkit.org/changeset/179160);對(duì)于iOS 8系統(tǒng),可以通過(guò)在 completionHandler 里 retain WKWebView 防止 completionHandler 被過(guò)早釋放。我們最后用 methodSwizzle hook 了這個(gè)系統(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、其它問(wèn)題
8.1、視頻自動(dòng)播放
WKWebView 需要通過(guò)WKWebViewConfiguration.mediaPlaybackRequiresUserAction設(shè)置是否允許自動(dòng)播放,但一定要在 WKWebView 初始化之前設(shè)置,在 WKWebView 初始化之后設(shè)置無(wú)效。
8.2、goBack API問(wèn)題
WKWebView 上調(diào)用 -[WKWebView goBack], 回退到上一個(gè)頁(yè)面后不會(huì)觸發(fā)window.onload()函數(shù)、不會(huì)執(zhí)行JS。
8.3、頁(yè)面滾動(dòng)速率
WKWebView 需要通過(guò)scrollView delegate調(diào)整滾動(dòng)速率:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
}
9、App嵌入小段html代碼 —— CoreText

1、富文本復(fù)雜的排版
2、圖片
3、連接
優(yōu)點(diǎn):
1)比webView消耗少
2)后臺(tái)渲染【非常適用于內(nèi)容排版工作】快
3)精確
缺點(diǎn):
1)不能夠像webView那樣支持復(fù)制
2)需要自己處理很多邏輯
小結(jié):
坑多, 相對(duì) UIWebView 在內(nèi)存消耗、穩(wěn)定性方面還是有很大的優(yōu)勢(shì)。