iOS 的 Cookie 存取 ?https://juejin.im/entry/58d4c4cc44d90400692b9991
項(xiàng)目鏈接地址:https://github.com/jzfjay/JZFMagazineWebView
一個(gè)不錯(cuò)的網(wǎng)頁(yè)圖片瀏覽器
http://www.reibang.com/p/97a3ee9ee517
1.取消長(zhǎng)按webView上的鏈接彈出actionSheet
[webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.webkitTouchCallout = 'none';"];
2.根據(jù)內(nèi)容獲取webview高度
NSString *fitHeight = [webview stringByEvaluatingJavaScriptFromString:@"document.body.scrollHeight;"];
3.獲取webview頁(yè)面內(nèi)容
NSString *docStr=[webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.textContent"];
4.去除某個(gè)標(biāo)簽內(nèi)容
[webView stringByEvaluatingJavaScriptFromString:@"document.getElementById('some-tag-id’).remove();"];
5.獲取當(dāng)前頁(yè)面的title:
NSString *title= [webview ?stringByEvaluatingJavaScriptFromString:@"document.title"];
6.獲取當(dāng)前頁(yè)面的url
NSString *url = [webview stringByEvaluatingJavaScriptFromString:@"document.location.href"];
7.獲取環(huán)境變量
[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
8.禁止彈出菜單/ 禁止選中
[_webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.webkitUserSelect='none';" ];
[_webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.webkitTouchCallout='none';" ];
=======================================================================
當(dāng)你的App中有 WebView 或者有 Text 文本的時(shí)候篱蝇,毫無(wú)疑問(wèn)屑咳,系統(tǒng)默認(rèn)地會(huì)在你進(jìn)行長(zhǎng)按的時(shí)候鹦倚,彈出一個(gè)框拿霉,來(lái)讓你拷貝、粘貼渐白、剪切文本等,亦或是彈出一個(gè)放大鏡里伯。
而當(dāng)你進(jìn)行開(kāi)發(fā)的時(shí)候含思,往往很明確的知道自己的 WebView 希望展示的是什么東西崎弃,這個(gè)時(shí)候,你可能會(huì)希望自定義長(zhǎng)按手勢(shì)(LongPressGesture)的功能含潘,而不是自帶的彈出框饲做。
然而當(dāng)你做了一系列工作自定義了長(zhǎng)按手勢(shì)之后,卻發(fā)現(xiàn)它的優(yōu)先級(jí)是低于系統(tǒng)默認(rèn)的彈出框的
ios低版本的可以用下面的方法
[_webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.webkitUserSelect='none';" ];
[_webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.webkitTouchCallout='none';" ];
在Xcode7IOS9上面這個(gè)方法就不適用了遏弱。
最后發(fā)現(xiàn)長(zhǎng)按手勢(shì)的默認(rèn)屬性中盆均,有一個(gè) minimumPressDuration 默認(rèn)值是 0.5,也就是說(shuō)漱逸,在長(zhǎng)按了0.5秒時(shí)泪姨,我們自定義的長(zhǎng)按手勢(shì)執(zhí)行方法和系統(tǒng)的沖突了,于是解決方法顯而易見(jiàn):把自定義長(zhǎng)按手勢(shì)的反應(yīng)時(shí)間縮短到0.5s以內(nèi)饰抒,這樣就可以在系統(tǒng)方法執(zhí)行之前完成操作肮砾,而事實(shí)上,你甚至可以簡(jiǎn)單粗暴地把它改為0.1s循集,依然不會(huì)和點(diǎn)按的 Tap 手勢(shì)有沖突唇敞。
self.longPressPan.minimumPressDuration = 0.1
WkWebView的一些坑
導(dǎo)語(yǔ)
WKWebView 是蘋(píng)果在 WWDC 2014 上推出的新一代 webView 組件,用以替代 UIKit 中笨重難用咒彤、內(nèi)存泄漏的 UIWebView疆柔。WKWebView?擁有60fps滾動(dòng)刷新率、和 safari 相同的 JavaScript 引擎等優(yōu)勢(shì)镶柱。
簡(jiǎn)單的適配方法本文不再贅述旷档,主要來(lái)說(shuō)說(shuō)適配 WKWebView 過(guò)程中填過(guò)的坑以及善待解決的技術(shù)難題。
1歇拆、WKWebView 白屏問(wèn)題
WKWebView 自詡擁有更快的加載速度鞋屈,更低的內(nèi)存占用范咨,但實(shí)際上 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)象鄙麦。在 WKWebView 中加載下面的測(cè)試鏈接可以穩(wěn)定重現(xiàn)白屏現(xiàn)象:
http://people.mozilla.org/~rnewman/fennec/mem.html
這個(gè)時(shí)候 WKWebView.URL 會(huì)變?yōu)?nil, 簡(jiǎn)單的 reload 刷新操作已經(jīng)失效典唇,對(duì)于一些長(zhǎng)駐的H5頁(yè)面影響比較大。
我們最后的解決方案是:
A黔衡、借助 WKNavigtionDelegate
iOS 9以后 WKNavigtionDelegate 新增了一個(gè)回調(diào)函數(shù):
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webViewAPI_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)的適配操作侣签。
B塘装、檢測(cè) 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.titile 會(huì)被置空, 因此矛双,可以在 viewWillAppear 的時(shí)候檢測(cè) webView.title 是否為空來(lái) reload 頁(yè)面。
綜合以上兩種方法可以解決絕大多數(shù)的白屏問(wèn)題蟆豫。
2议忽、WKWebView Cookie 問(wèn)題
Cookie 問(wèn)題是目前 WKWebView 的一大短板
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ì)寫(xiě)入 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 May201923: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
蘋(píng)果開(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(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、Workaround
由于許多 H5 業(yè)務(wù)都依賴于 Cookie 作登錄態(tài)校驗(yàn)糖赔,而 WKWebView 上請(qǐng)求不會(huì)自動(dòng)攜帶 Cookie, 目前的主要解決方案是:
a萍丐、WKWebView loadRequest 前,在 request header 中設(shè)置 Cookie, 解決首個(gè)請(qǐng)求 Cookie 帶不上的問(wèn)題放典;
WKWebView * webView = [WKWebViewnew];
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://h5.qzone.qq.com/mqzone/index"]];
[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"];
[webView loadRequest:request];
b逝变、通過(guò) document.cookie 設(shè)置 Cookie 解決后續(xù)頁(yè)面(同域)Ajax、iframe 請(qǐng)求的 Cookie 問(wèn)題奋构;
注意:document.cookie()無(wú)法跨域設(shè)置 cookie
WKUserContentController* userContentController = [WKUserContentControllernew];
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)求是 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:]只適合加載 mainFrame 請(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 上直接使用 NSURLProtocol 無(wú)法攔截請(qǐng)求熏瞄。蘋(píng)果開(kāi)源的 webKit2 源碼暴露了私有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"];
}
但是這種方案目前存在兩個(gè)嚴(yán)重缺陷:
a、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è)字段被丟棄掉了
參考蘋(píng)果源碼:
https://github.com/WebKit/webkit/blob/fe39539b83d28751e86077b173abd5b7872ce3f9/Source/WebKit2/Shared/mac/WebCoreArgumentCodersMac.mm#L61-L88(復(fù)制鏈接到瀏覽器中打開(kāi))
及bug report:
https://bugs.webkit.org/show_bug.cgi?id=138169(復(fù)制鏈接到瀏覽器中打開(kāi))
因此剪廉,如果通過(guò) registerSchemeForCustomProtocol 注冊(cè)了 http(s) scheme, 那么由 WKWebView 發(fā)起的所有 http(s)請(qǐng)求都會(huì)通過(guò) IPC 傳給主進(jìn)程 NSURLProtocol 處理,導(dǎo)致 post 請(qǐng)求 body 被清空炕檩;
b斗蒋、對(duì)ATS支持不足
測(cè)試發(fā)現(xiàn)一旦打開(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)求捎稚,比如 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];
workaround:
假如想通過(guò)-[WKWebView loadRequest:]加載 post 請(qǐng)求 request1:http://h5.qzone.qq.com/mqzone/index,可以通過(guò)以下步驟實(shí)現(xiàn):
替換請(qǐng)求 scheme色冀,生成新的 post 請(qǐng)求 request2:post://h5.qzone.qq.com/mqzone/index, 同時(shí)將 request1 的 body 字段復(fù)制到 request2 的 header 中(WebKit 不會(huì)丟棄 header 字段);
通過(guò)-[WKWebView loadRequest:]加載新的 post 請(qǐng)求 request2;
通過(guò) +[WKBrowsingContextController registerSchemeForCustomProtocol:]注冊(cè) scheme:post://;
注冊(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)題
在 WKWebView 適配過(guò)程中,我們發(fā)現(xiàn)部分H5頁(yè)面元素位置向下偏移或被拉伸變形,追蹤后發(fā)現(xiàn)主要是H5頁(yè)面高度值異常導(dǎo)致:
a. 空間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);
b. 在接入 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)谴忧,蘋(píng)果后來(lái)fix了這個(gè)bug。我們最后的解決方案是:延遲調(diào)用window.innerHeight
setTimeout(function(){height = window.innerHeight},0);
or
Use shrink-to-fit meta-tag
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)翻遍蘋(pí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 UIKit0x0000000190513360 UIApplicationMain +208
29 Qzone0x0000000101380570 main (main.m:181)30 libdyld.dylib0x00000001895205b8 _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:(nonnullvoid(^)())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)畫(huà)尚未結(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:^{}];elsecompletionHandler();
}
確保上面兩種情況下 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)蘋(pí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、結(jié)語(yǔ)
本文總結(jié)了在 WKWebView 上踩過(guò)的一些坑胳岂。雖然 WKWebView 坑比較多编整,但是相對(duì) UIWebView 在內(nèi)存消耗、穩(wěn)定性方面還是有很大的優(yōu)勢(shì)乳丰。盡管蘋(píng)果對(duì) WKWebView 的開(kāi)發(fā)進(jìn)度過(guò)于緩慢掌测,但相信 WKWebView 才是未來(lái)。