背景
在iOS應(yīng)用開發(fā)中枪眉,內(nèi)嵌WebView一直占有一定的頁面數(shù)量比例狸驳。它能以較低的開發(fā)成本實現(xiàn)iOS、Android和Web的復(fù)用役耕,也可以一定程度的的規(guī)避蘋果對熱更新的封鎖采转。然而UIWebView
的CPU資源消耗和內(nèi)存占用一直被嫌棄,導(dǎo)致很多客戶端中需要動態(tài)更新等頁面時不得不采用其他方案。長遠來看故慈,功能的動態(tài)加載以及三端的融合將會是大趨勢板熊。怎么解決WebView固有的問題呢?我們將通過全面的對比來分析使用UIWebView
的問題察绷。
全面對比
UIWebView使用UIKit框架干签,而WKWebView使用WebKit.framework。WKWebView采用與Safari 相同的 Nitro JavaScript 引擎拆撼,在cpu資源消耗方面容劳,遠低于UIWebView。在使用WKWebView后闸度,應(yīng)用在打開類似商城首頁這種加載內(nèi)容較多的網(wǎng)頁時竭贩,CPU占用下降非常明顯
WKWebView為多進程組件,會從App內(nèi)存中分離內(nèi)存到單獨的進程(Network Process and Rendring Process)中筋岛。當(dāng)內(nèi)存超過了系統(tǒng)分配給WKWebView的內(nèi)存時候娶视,會導(dǎo)致WKWebView瀏覽器崩潰白屏,但是App不會Crash(app會收到系統(tǒng)通知睁宰,并且嘗試去重新加載頁面)肪获。相反UIWebView是和app同一個進程,UIWebView加載頁面占用的內(nèi)存被計算為app內(nèi)存占用的一部分柒傻,當(dāng)app超過了系統(tǒng)分配的內(nèi)存孝赫,則會被操作系統(tǒng)crash。在整個過程中红符,會經(jīng)常收到iOS系統(tǒng)的通知用來防止app被系統(tǒng)kill青柄,很多時候,這些通知并不及時预侯,或者根本沒有返回通知致开。
WKWebView是異步處理native與JavaScript之間的通信,所以執(zhí)行速度會更快萎馅。
WKWevbView內(nèi)存消耗較UIWebView大幅下降双戳。
WKWebView有著高達60fps的滾動刷新率以及內(nèi)置手勢
WKWevView更多的支持 HTML5 的特性
-
WKWevView將 UIWebViewDelegate 與 UIWebView 拆分成了14類與3個協(xié)議,包含更細節(jié)功能的實現(xiàn)糜芳,詳解如下:
WKBackForwardList: 之前訪問過的 web 頁面的列表飒货,可以通過后退和前進動作來訪問到。WKBackForwardListItem: webview 中后退列表里的某一個網(wǎng)頁峭竣。
WKFrameInfo: 包含一個網(wǎng)頁的布局信息塘辅。
WKNavigation: 包含一個網(wǎng)頁的加載進度信息。
WKNavigationAction: 包含可能讓網(wǎng)頁導(dǎo)航變化的信息皆撩,用于判斷是否做出導(dǎo)航變化扣墩。
WKNavigationResponse: 包含可能讓網(wǎng)頁導(dǎo)航變化的返回內(nèi)容信息,用于判斷是否做出導(dǎo)航變化。
WKPreferences: 概括一個 webview 的偏好設(shè)置呻惕。
WKProcessPool: 表示一個 web 內(nèi)容加載池盘榨。
WKUserContentController: 提供使用 JavaScript post 信息和注射 script 的方法。
WKScriptMessage: 包含網(wǎng)頁發(fā)出的信息蟆融。
WKUserScript: 表示可以被網(wǎng)頁接受的用戶腳本草巡。
WKWebViewConfiguration: 初始化 webview 的設(shè)置。
WKWindowFeatures: 指定加載新網(wǎng)頁時的窗口屬性型酥。
WKNavigationDelegate: 提供了追蹤主窗口網(wǎng)頁加載過程和判斷主窗口和子窗口是否進行頁面加載新頁面的相關(guān)方法山憨。
WKUIDelegate: 提供用原生控件顯示網(wǎng)頁的方法回調(diào)。
WKScriptMessageHandler: 提供從網(wǎng)頁中收消息的回調(diào)方法弥喉。
在使用cookie方面郁竟,在使用 UIWebVIew 的時候我們并不關(guān)注 Cookie,因為在調(diào)用登錄接口的時候無論是AFNetworking由境,還是其他棚亩,登錄成功之后都會自動保存在[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies 中。但 WKWebView 的存儲體系與 UIWebVIew 完全不一樣虏杰,只能手動給它添加 Cookie讥蟆。這也是很多同行所詬病的地方,甚至因為這個原因而遲遲不愿意更新纺阔,下面貼出代碼供參考
private func configWebView() -> WKWebView {
let webConfig = WKWebViewConfiguration()
// 1.刪除沙盒中 之前舊版本的cookies以及現(xiàn)在的cookie 2.實時從登錄信息中組合cookies瘸彤,不以沙盒中的cookie為準
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("開始刪除:\(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īng)過 NSURLProtocol,也就是WKWebView無法攔截響應(yīng)數(shù)據(jù)
- 對于 WKWebView 笛钝,有三個屬性支持KVO质况,因此我們可以輕松監(jiān)聽其值的變化,分別是:loading玻靡、title结榄、estimatedProgress,對應(yīng)功能表示為:是否正在加載中、頁面的標題囤捻、頁面內(nèi)容加載進度(值為0.0~1.0)臼朗,下面貼出我們實際在項目中kvo監(jiān)聽的運用
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
// 監(jiān)聽 WKWebView 對象的 estimatedProgress 屬性,就是當(dāng)前網(wǎng)頁加載的進度
webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
// 監(jiān)聽 WKWebView 對象的 title 屬性最蕾,就是當(dāng)前網(wǎng)頁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ù)場景
native與JS的相互調(diào)用
WKWebView對于HTML5的操作已經(jīng)很便捷了依溯,但是還沒有Android的WebView那樣簡單老厌。WebView能夠直接注入JavaScript對象瘟则,交互過程中Java 與 JavaScript甚至可以直接調(diào)用對方的方法,不用攔截枝秤,不用分發(fā)醋拧,這樣的Java 與 JavaScript的交互非常清晰明了。在iOS上,還達不到這樣的便捷丹壕。
在使用WKWebView時庆械,H5調(diào)用Native 的過程是:1、Native注入JavaScript函數(shù)菌赖;2缭乘、Native實現(xiàn)橋接方法:通過系統(tǒng)方法攔截JavaScript事件,匹配OC/Swift注冊列表琉用,分發(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的過程是:使用WKWebView的接口調(diào)用JavaScript函數(shù)奴紧。附上代碼
self.webView.evaluateJavaScript(javaScript, completionHandler: { (_, error) in
if let error = error {
FCLog("webJS——\(javaScript)執(zhí)行失敗: \(error.localizedDescription)")
}
})
動態(tài)加載并運行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對象
let testScript = WKUserScript(source: jsStr, injectionTime:.atDocumentEnd, forMainFrameOnly: true)
let testController = WKUserContentController()
testController.addUserScript(testScript)
// 根據(jù)生成的WKUserScript對象晶丘,初始化WKWebViewConfiguration
let webConfiguration = WKWebViewConfiguration()
webConfiguration.userContentController = testController
let testWebview = WKWebView(frame: CGRect.zero, configuration: webConfiguration)
view.addSubview(testWebview)
踩坑實錄
- 對于JS有異步接口回調(diào)的數(shù)據(jù)情況黍氮,可能導(dǎo)致頁面加載數(shù)據(jù)異常,除了延時機制外浅浮,還沒找到好的解決辦法沫浆。有高招的大神,求指導(dǎo)滚秩。
- 創(chuàng)建WKWebViewConfiguration的實例递惋,這個實例可以給網(wǎng)頁進行一些配置。注意這個實例只能在 webView第一次創(chuàng)建的時候才能使用带射。
- WKWebView 點擊H5內(nèi)鏈接無反應(yīng)霎苗,多半是因為網(wǎng)頁中有target="_blank" 在新窗口打開連接,此時我們需要設(shè)置WKWebView的另外一個代理WKUIDelegate已艰,并實現(xiàn)代理方法如下:
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
webView.load(navigationAction.request)
return nil
}
- App之間的數(shù)據(jù)交互是通過URL Scheme來實現(xiàn)的痊末。在UIWebView時代如果加載的URL是“customURLScheme://”這種形式的,UIWebView會執(zhí)行openUrl函數(shù)哩掺,從而和其他App進行交互凿叠,然而WKWebView就需要自己支持了,我們要對非http://和https://的做相應(yīng)處理嚼吞,附上代碼:
if !(urlString.hasPrefix("http://") || urlString.hasPrefix("https://")) {
if UIApplication.shared.canOpenURL(appUrl) {
UIApplication.shared.openURL(appUrl)
}
}
- WKWebView不會像UIWebView把Content-Type標頭設(shè)置為POST請求的application / x-www-formurlencoded盒件,都要手動添加,否則會造成所加載H5頁面出錯
let request = NSMutableURLRequest(url: url)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")