背景
在iOS應(yīng)用開(kāi)發(fā)中,內(nèi)嵌WebView一直占有一定的頁(yè)面數(shù)量比例。它能以較低的開(kāi)發(fā)成本實(shí)現(xiàn)iOS、Android和Web的復(fù)用,也可以一定程度的的規(guī)避蘋果對(duì)熱更新的封鎖。然而UIWebView
的CPU資源消耗和內(nèi)存占用一直被嫌棄,導(dǎo)致很多客戶端中需要?jiǎng)討B(tài)更新等頁(yè)面時(shí)不得不采用其他方案。長(zhǎng)遠(yuǎn)來(lái)看,功能的動(dòng)態(tài)加載以及三端的融合將會(huì)是大趨勢(shì)。怎么解決WebView固有的問(wèn)題呢?我們將通過(guò)全面的對(duì)比來(lái)分析使用UIWebView
的問(wèn)題。
全面對(duì)比
UIWebView使用UIKit框架,而WKWebView使用WebKit.framework。WKWebView采用與Safari 相同的 Nitro JavaScript 引擎,在cpu資源消耗方面,遠(yuǎn)低于UIWebView。在使用WKWebView后,應(yīng)用在打開(kāi)類似商城首頁(yè)這種加載內(nèi)容較多的網(wǎng)頁(yè)時(shí),CPU占用下降非常明顯
WKWebView為多進(jìn)程組件,會(huì)從App內(nèi)存中分離內(nèi)存到單獨(dú)的進(jìn)程(Network Process and Rendring Process)中。當(dāng)內(nèi)存超過(guò)了系統(tǒng)分配給WKWebView的內(nèi)存時(shí)候,會(huì)導(dǎo)致WKWebView瀏覽器崩潰白屏,但是App不會(huì)Crash(app會(huì)收到系統(tǒng)通知,并且嘗試去重新加載頁(yè)面)。相反UIWebView是和app同一個(gè)進(jìn)程,UIWebView加載頁(yè)面占用的內(nèi)存被計(jì)算為app內(nèi)存占用的一部分,當(dāng)app超過(guò)了系統(tǒng)分配的內(nèi)存,則會(huì)被操作系統(tǒng)crash。在整個(gè)過(guò)程中,會(huì)經(jīng)常收到iOS系統(tǒng)的通知用來(lái)防止app被系統(tǒng)kill,很多時(shí)候,這些通知并不及時(shí),或者根本沒(méi)有返回通知。
WKWebView是異步處理native與JavaScript之間的通信,所以執(zhí)行速度會(huì)更快。
WKWevbView內(nèi)存消耗較UIWebView大幅下降。
WKWebView有著高達(dá)60fps的滾動(dòng)刷新率以及內(nèi)置手勢(shì)
WKWevView更多的支持 HTML5 的特性
-
WKWevView將 UIWebViewDelegate 與 UIWebView 拆分成了14類與3個(gè)協(xié)議,包含更細(xì)節(jié)功能的實(shí)現(xiàn),詳解如下:
WKBackForwardList: 之前訪問(wèn)過(guò)的 web 頁(yè)面的列表,可以通過(guò)后退和前進(jìn)動(dòng)作來(lái)訪問(wèn)到。WKBackForwardListItem: webview 中后退列表里的某一個(gè)網(wǎng)頁(yè)。
WKFrameInfo: 包含一個(gè)網(wǎng)頁(yè)的布局信息。
WKNavigation: 包含一個(gè)網(wǎng)頁(yè)的加載進(jìn)度信息。
WKNavigationAction: 包含可能讓網(wǎng)頁(yè)導(dǎo)航變化的信息,用于判斷是否做出導(dǎo)航變化。
WKNavigationResponse: 包含可能讓網(wǎng)頁(yè)導(dǎo)航變化的返回內(nèi)容信息,用于判斷是否做出導(dǎo)航變化。
WKPreferences: 概括一個(gè) webview 的偏好設(shè)置。
WKProcessPool: 表示一個(gè) web 內(nèi)容加載池。
WKUserContentController: 提供使用 JavaScript post 信息和注射 script 的方法。
WKScriptMessage: 包含網(wǎng)頁(yè)發(fā)出的信息。
WKUserScript: 表示可以被網(wǎng)頁(yè)接受的用戶腳本。
WKWebViewConfiguration: 初始化 webview 的設(shè)置。
WKWindowFeatures: 指定加載新網(wǎng)頁(yè)時(shí)的窗口屬性。
WKNavigationDelegate: 提供了追蹤主窗口網(wǎng)頁(yè)加載過(guò)程和判斷主窗口和子窗口是否進(jìn)行頁(yè)面加載新頁(yè)面的相關(guān)方法。
WKUIDelegate: 提供用原生控件顯示網(wǎng)頁(yè)的方法回調(diào)。
WKScriptMessageHandler: 提供從網(wǎng)頁(yè)中收消息的回調(diào)方法。
在使用cookie方面,在使用 UIWebVIew 的時(shí)候我們并不關(guān)注 Cookie,因?yàn)樵谡{(diào)用登錄接口的時(shí)候無(wú)論是AFNetworking,還是其他,登錄成功之后都會(huì)自動(dòng)保存在[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies 中。但 WKWebView 的存儲(chǔ)體系與 UIWebVIew 完全不一樣,只能手動(dòng)給它添加 Cookie。這也是很多同行所詬病的地方,甚至因?yàn)檫@個(gè)原因而遲遲不愿意更新,下面貼出代碼供參考
private func configWebView() -> WKWebView {
let webConfig = WKWebViewConfiguration()
// 1.刪除沙盒中 之前舊版本的cookies以及現(xiàn)在的cookie 2.實(shí)時(shí)從登錄信息中組合cookies,不以沙盒中的cookie為準(zhǔn)
if #available(iOS 11.0, *) {
/// iOS 11以上
let webView = WKWebView.init(frame: .zero, configuration: webConfig)
let store = webConfig.websiteDataStore.httpCookieStore
store.getAllCookies { (items) in
for item in items {
if let comment = item.comment, comment.contains("fc_") {
FCLog("開(kāi)始刪除:\(item.domain)____\(item.name)")
store.delete(item, completionHandler: {
FCLog("\(item.domain) 刪除了")
})
}
}
for item in HttpCookieManager.cookies {
store.setCookie(item, completionHandler: nil)
}
}
return webView
} else {
// iOS 11以下
let userContentController = WKUserContentController()
webConfig.userContentController = userContentController
for str in HttpCookieManager.getAllCookies() {
let setCookie = "document.cookie='\(str)';"
let cookieScript = WKUserScript.init(source: setCookie, injectionTime: .atDocumentStart, forMainFrameOnly: false)
userContentController.addUserScript(cookieScript)
}
let webView = WKWebView.init(frame: .zero, configuration: webConfig)
return webView
}
}
- WKWebView與 UIWebView 機(jī)制不同:加載過(guò)程中所有的請(qǐng)求都不經(jīng)過(guò) NSURLProtocol,也就是WKWebView無(wú)法攔截響應(yīng)數(shù)據(jù)
- 對(duì)于 WKWebView ,有三個(gè)屬性支持KVO,因此我們可以輕松監(jiān)聽(tīng)其值的變化,分別是:loading、title、estimatedProgress,對(duì)應(yīng)功能表示為:是否正在加載中、頁(yè)面的標(biāo)題、頁(yè)面內(nèi)容加載進(jìn)度(值為0.0~1.0),下面貼出我們實(shí)際在項(xiàng)目中kvo監(jiān)聽(tīng)的運(yùn)用
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
// 監(jiān)聽(tīng) WKWebView 對(duì)象的 estimatedProgress 屬性,就是當(dāng)前網(wǎng)頁(yè)加載的進(jìn)度
webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
// 監(jiān)聽(tīng) WKWebView 對(duì)象的 title 屬性,就是當(dāng)前網(wǎng)頁(yè)title
webView.addObserver(self, forKeyPath: "title", options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
if let x = change?[.newKey], let keyPath = keyPath {
switch keyPath {
case "estimatedProgress":
if let progress = x as? Float {
print("progress is \(progress)")
progressView.setProgress(progress, animated: true)
}
case "title":
if let title = x as? String, let action = didGetTitleAction {
action(title)
}
default:
print("observeValue:\(x)")
}
}
}
業(yè)務(wù)場(chǎng)景
native與JS的相互調(diào)用
WKWebView對(duì)于HTML5的操作已經(jīng)很便捷了,但是還沒(méi)有Android的WebView那樣簡(jiǎn)單。WebView能夠直接注入JavaScript對(duì)象,交互過(guò)程中Java 與 JavaScript甚至可以直接調(diào)用對(duì)方的方法,不用攔截,不用分發(fā),這樣的Java 與 JavaScript的交互非常清晰明了。在iOS上,還達(dá)不到這樣的便捷。
在使用WKWebView時(shí),H5調(diào)用Native 的過(guò)程是:1、Native注入JavaScript函數(shù);2、Native實(shí)現(xiàn)橋接方法:通過(guò)系統(tǒng)方法攔截JavaScript事件,匹配OC/Swift注冊(cè)列表,分發(fā)調(diào)用不同的原生方法。附上代碼:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
guard let urlString = url.absoluteString.removingPercentEncoding else {
decisionHandler(.allow)
return
}
// 現(xiàn)有業(yè)務(wù)協(xié)議,與H5約定命名,可攜帶參數(shù)
guard urlString.contains("JS協(xié)議"),
let preRange = urlString.range(of: "JS協(xié)議") else {
decisionHandler(.allow)
return
}
let jsonStr = urlString[preRange.upperBound..<urlString.endIndex]
guard let jsonData = jsonStr.data(using: .utf8) else {
decisionHandler(.allow)
return
}
let json = JSON(jsonData)
guard !json.isEmpty else {
decisionHandler(.allow)
return
}
decisionHandler(.allow)
}
而OC/Swift調(diào)用JavaScript的過(guò)程是:使用WKWebView的接口調(diào)用JavaScript函數(shù)。附上代碼
self.webView.evaluateJavaScript(javaScript, completionHandler: { (_, error) in
if let error = error {
FCLog("webJS——\(javaScript)執(zhí)行失敗: \(error.localizedDescription)")
}
})
動(dòng)態(tài)加載并運(yùn)行JS代碼
附上示例代碼
// js代碼片段
let jsStr = "var clickBtn = document.getElementsByClassName('click_fcbox');for(var j = 0;j < clickBtn.length; j++){clickBtn[j].onclick = function(){this.removeAttribute('clicked');}}"
// 根據(jù)JS字符串初始化WKUserScript對(duì)象
let testScript = WKUserScript(source: jsStr, injectionTime:.atDocumentEnd, forMainFrameOnly: true)
let testController = WKUserContentController()
testController.addUserScript(testScript)
// 根據(jù)生成的WKUserScript對(duì)象,初始化WKWebViewConfiguration
let webConfiguration = WKWebViewConfiguration()
webConfiguration.userContentController = testController
let testWebview = WKWebView(frame: CGRect.zero, configuration: webConfiguration)
view.addSubview(testWebview)
踩坑實(shí)錄
- 對(duì)于JS有異步接口回調(diào)的數(shù)據(jù)情況,可能導(dǎo)致頁(yè)面加載數(shù)據(jù)異常,除了延時(shí)機(jī)制外,還沒(méi)找到好的解決辦法。有高招的大神,求指導(dǎo)。
- 創(chuàng)建WKWebViewConfiguration的實(shí)例,這個(gè)實(shí)例可以給網(wǎng)頁(yè)進(jìn)行一些配置。注意這個(gè)實(shí)例只能在 webView第一次創(chuàng)建的時(shí)候才能使用。
- WKWebView 點(diǎn)擊H5內(nèi)鏈接無(wú)反應(yīng),多半是因?yàn)榫W(wǎng)頁(yè)中有target="_blank" 在新窗口打開(kāi)連接,此時(shí)我們需要設(shè)置WKWebView的另外一個(gè)代理WKUIDelegate,并實(shí)現(xiàn)代理方法如下:
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
webView.load(navigationAction.request)
return nil
}
- App之間的數(shù)據(jù)交互是通過(guò)URL Scheme來(lái)實(shí)現(xiàn)的。在UIWebView時(shí)代如果加載的URL是“customURLScheme://”這種形式的,UIWebView會(huì)執(zhí)行openUrl函數(shù),從而和其他App進(jìn)行交互,然而WKWebView就需要自己支持了,我們要對(duì)非http://和https://的做相應(yīng)處理,附上代碼:
if !(urlString.hasPrefix("http://") || urlString.hasPrefix("https://")) {
if UIApplication.shared.canOpenURL(appUrl) {
UIApplication.shared.openURL(appUrl)
}
}
- WKWebView不會(huì)像UIWebView把Content-Type標(biāo)頭設(shè)置為POST請(qǐng)求的application / x-www-formurlencoded,都要手動(dòng)添加,否則會(huì)造成所加載H5頁(yè)面出錯(cuò)
let request = NSMutableURLRequest(url: url)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")