背景
在iOS应用开发中,内嵌WebView一直占有一定的页面数量比例。它能以较低的开发成本实现iOS、Android和Web的复用,也可以一定程度的的规避苹果对热更新的封锁。然而UIWebView
的CPU资源消耗和内存占用一直被嫌弃,导致很多客户端中需要动态更新等页面时不得不采用其他方案。长远来看,功能的动态加载以及三端的融合将会是大趋势。怎么解决WebView固有的问题呢?我们将通过全面的对比来分析使用UIWebView
的问题。
全面对比
-
UIWebView使用UIKit框架,而WKWebView使用WebKit.framework。WKWebView采用与Safari 相同的 Nitro JavaScript 引擎,在cpu资源消耗方面,远低于UIWebView。在使用WKWebView后,应用在打开类似商城首页这种加载内容较多的网页时,CPU占用下降非常明显
-
WKWebView为多进程组件,会从App内存中分离内存到单独的进程(Network Process and Rendring Process)中。当内存超过了系统分配给WKWebView的内存时候,会导致WKWebView浏览器崩溃白屏,但是App不会Crash(app会收到系统通知,并且尝试去重新加载页面)。相反UIWebView是和app同一个进程,UIWebView加载页面占用的内存被计算为app内存占用的一部分,当app超过了系统分配的内存,则会被操作系统crash。在整个过程中,会经常收到iOS系统的通知用来防止app被系统kill,很多时候,这些通知并不及时,或者根本没有返回通知。
-
WKWebView是异步处理native与JavaScript之间的通信,所以执行速度会更快。
-
WKWevbView内存消耗较UIWebView大幅下降。
-
WKWebView有着高达60fps的滚动刷新率以及内置手势
-
WKWevView更多的支持 HTML5 的特性
-
WKWevView将 UIWebViewDelegate 与 UIWebView 拆分成了14类与3个协议,包含更细节功能的实现,详解如下:
WKBackForwardList: 之前访问过的 web 页面的列表,可以通过后退和前进动作来访问到。WKBackForwardListItem: webview 中后退列表里的某一个网页。
WKFrameInfo: 包含一个网页的布局信息。
WKNavigation: 包含一个网页的加载进度信息。
WKNavigationAction: 包含可能让网页导航变化的信息,用于判断是否做出导航变化。
WKNavigationResponse: 包含可能让网页导航变化的返回内容信息,用于判断是否做出导航变化。
WKPreferences: 概括一个 webview 的偏好设置。
WKProcessPool: 表示一个 web 内容加载池。
WKUserContentController: 提供使用 JavaScript post 信息和注射 script 的方法。
WKScriptMessage: 包含网页发出的信息。
WKUserScript: 表示可以被网页接受的用户脚本。
WKWebViewConfiguration: 初始化 webview 的设置。
WKWindowFeatures: 指定加载新网页时的窗口属性。
WKNavigationDelegate: 提供了追踪主窗口网页加载过程和判断主窗口和子窗口是否进行页面加载新页面的相关方法。
WKUIDelegate: 提供用原生控件显示网页的方法回调。
WKScriptMessageHandler: 提供从网页中收消息的回调方法。
-
在使用cookie方面,在使用 UIWebVIew 的时候我们并不关注 Cookie,因为在调用登录接口的时候无论是AFNetworking,还是其他,登录成功之后都会自动保存在[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies 中。但 WKWebView 的存储体系与 UIWebVIew 完全不一样,只能手动给它添加 Cookie。这也是很多同行所诟病的地方,甚至因为这个原因而迟迟不愿意更新,下面贴出代码供参考
private func configWebView() -> WKWebView {
let webConfig = WKWebViewConfiguration()
// 1.删除沙盒中 之前旧版本的cookies以及现在的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 机制不同:加载过程中所有的请求都不经过 NSURLProtocol,也就是WKWebView无法拦截响应数据
- 对于 WKWebView ,有三个属性支持KVO,因此我们可以轻松监听其值的变化,分别是:loading、title、estimatedProgress,对应功能表示为:是否正在加载中、页面的标题、页面内容加载进度(值为0.0~1.0),下面贴出我们实际在项目中kvo监听的运用
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
// 监听 WKWebView 对象的 estimatedProgress 属性,就是当前网页加载的进度
webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
// 监听 WKWebView 对象的 title 属性,就是当前网页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)")
}
}
}
业务场景
native与JS的相互调用
WKWebView对于HTML5的操作已经很便捷了,但是还没有Android的WebView那样简单。WebView能够直接注入JavaScript对象,交互过程中Java 与 JavaScript甚至可以直接调用对方的方法,不用拦截,不用分发,这样的Java 与 JavaScript的交互非常清晰明了。在iOS上,还达不到这样的便捷。
在使用WKWebView时,H5调用Native 的过程是:1、Native注入JavaScript函数;2、Native实现桥接方法:通过系统方法拦截JavaScript事件,匹配OC/Swift注册列表,分发调用不同的原生方法。附上代码:
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
}
// 现有业务协议,与H5约定命名,可携带参数
guard urlString.contains("JS协议"),
let preRange = urlString.range(of: "JS协议") 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调用JavaScript的过程是:使用WKWebView的接口调用JavaScript函数。附上代码
self.webView.evaluateJavaScript(javaScript, completionHandler: { (_, error) in
if let error = error {
FCLog("webJS——\(javaScript)执行失败: \(error.localizedDescription)")
}
})
动态加载并运行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');}}"
// 根据JS字符串初始化WKUserScript对象
let testScript = WKUserScript(source: jsStr, injectionTime:.atDocumentEnd, forMainFrameOnly: true)
let testController = WKUserContentController()
testController.addUserScript(testScript)
// 根据生成的WKUserScript对象,初始化WKWebViewConfiguration
let webConfiguration = WKWebViewConfiguration()
webConfiguration.userContentController = testController
let testWebview = WKWebView(frame: CGRect.zero, configuration: webConfiguration)
view.addSubview(testWebview)
踩坑实录
- 对于JS有异步接口回调的数据情况,可能导致页面加载数据异常,除了延时机制外,还没找到好的解决办法。有高招的大神,求指导。
- 创建WKWebViewConfiguration的实例,这个实例可以给网页进行一些配置。注意这个实例只能在 webView第一次创建的时候才能使用。
- WKWebView 点击H5内链接无反应,多半是因为网页中有target="_blank" 在新窗口打开连接,此时我们需要设置WKWebView的另外一个代理WKUIDelegate,并实现代理方法如下:
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
webView.load(navigationAction.request)
return nil
}
- App之间的数据交互是通过URL Scheme来实现的。在UIWebView时代如果加载的URL是“customURLScheme://”这种形式的,UIWebView会执行openUrl函数,从而和其他App进行交互,然而WKWebView就需要自己支持了,我们要对非http://和https://的做相应处理,附上代码:
if !(urlString.hasPrefix("http://") || urlString.hasPrefix("https://")) {
if UIApplication.shared.canOpenURL(appUrl) {
UIApplication.shared.openURL(appUrl)
}
}
- WKWebView不会像UIWebView把Content-Type标头设置为POST请求的application / x-www-formurlencoded,都要手动添加,否则会造成所加载H5页面出错
let request = NSMutableURLRequest(url: url)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
网友评论