一、WKWebView初识
- 原生提供的一种用于加载H5网页的控件,运用Safari浏览器相同的JavaScript引擎,相同的WebKit内核,大大提高页面JS执行速度,相当于UIWebView的封装。
二、WKWebView相比于UIWebView的优缺点
- 优点
-
更少的内存占用,优化性能管理。如下图示,这是加载相同网页时两种WebView的内存占用表现。
- 增加新的代理方法,可控性更高。
- JS交互上更加方便:WKWebView支持直接注入JS方法名,不需要通过JavaScriptCore作为中间桥梁。
Swift实现:userContentController.add(self, name: "openUrl")
H5调用:window.webkit.messageHandlers.openUrl.postMessage("XXX");
Tips:JS只支持单个参数传递,如果需要传递多个数据,建议使用JSON字符串传值。
- 缺点
- 问题1:承载当前WebView的控制器无法正常释放。
原因:WKUserContentController对self有个引用,而WKWebConfiguration对WKUserContentController有引用,WebView初始化时对WKWebConfiguration有引用,而WebView本身又是self的一个变量,这就相当于self引用了self,形成循环引用,导致控制器无法被正常释放。
解决方案:在WKUserContentController里初始化一个新的NSObject对象,弱引用WKScriptMessageHandle,在WKUserContentController实现代理,然后设置一个新协议,将WKUserContentController的代理实现挂载出去,用于controller实现,这样就不会引起循环引用,从而解决controller无法正常释放问题。步骤如下:
(1) 声明一个新的继承于NSObject的class对象WKScriptMessageHandleDelegate,实现WKScriptMessageHandle代理
class WKScriptMessageHandleDelegate: NSObject {
weak var messageHandleDelegate: WKScriptMessageHandler?
}
extension WKScriptMessageHandleDelegate: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let delegate = messageHandleDelegate else {
return
}
if delegate.responds(to: #selector(userContentController(_:didReceive:))) {
delegate.userContentController(userContentController, didReceive: message)
}
}
}
(2) 在WKUserContentCOntroller里面初始化新对象,并实现代理:
class BaseWKUCController: WKUserContentController {
weak var messageHandleDelegate: WKJSImplementDelegate?
override init() {
super.init()
let contentHandleDelegate = WKScriptMessageHandleDelegate()
contentHandleDelegate.messageHandleDelegate = self
// 成对出现
add(contentHandleDelegate, name: "JS方法名")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
debugPrintOnly("\(self) is deinit ......")
}
}
(3) 声明一个新的代理,用于JS的具体实现:
@objc
protocol WKJSImplementDelegate: NSObjectProtocol {
@objc func openUrl(_ param: String)
}
(4) WKUserContentController里实现代理:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let delegate = messageHandleDelegate else {
return
}
switch message.name {
case "JS方法名":
if delegate.responds(to: #selector(delegate.openUrl(_:))) {
DispatchQueue.main.async {
delegate.openUrl(message.body as? String ?? "")
}
}
default:
return
}
}
(5) 在初始化WebView时,在当前的Controller里实现WKJSImplementDelegate即可。
- 问题2:断点调试发现WKUserContentController无法正常释放。
原因:成对添加JS方法,需要在WebViewController释放时成对remove:
//注入JS方法
userContentController.add(contentHandleDelegate, name: "JS方法名")
// 移除
userContentController.removeScriptMessageHandler(forName: "JS方法名")
三、WKWebView初始化
- WKUserContentController的初始化。我这里是声明一个Base。
/*
父类 WKUserContentController
*/
class BaseWKUCController: WKUserContentController {
weak var messageHandleDelegate: WKJSImplementDelegate?
override init() {
super.init()
let contentHandleDelegate = WKScriptMessageHandleDelegate()
contentHandleDelegate.messageHandleDelegate = self
// 成对出现
add(contentHandleDelegate, name: "JS方法名")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
debugPrintOnly("\(self) is deinit ......")
}
}
- WKWebViewConfiguration初始化。依然是声明一个Base。
/*
父类 WKWebViewConfiguration
*/
class BaseWKConfiguration: WKWebViewConfiguration {
weak var contentController: BaseWKUCController!
convenience init(_ delegate: WKJSImplementDelegate?) {
self.init()
contentController = BaseWKUCController().then({ (c) in
c.messageHandleDelegate = delegate
userContentController = c
})
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
debugPrintOnly("\(self) is deinit ......")
}
}
- WKWebView初始化。
WKWeb = WKWebView(frame: view.bounds, configuration: wkConfig).then({ (v) in
view.addSubview(v)
v.scrollView.bounces = true
v.scrollView.alwaysBounceVertical = true
v.navigationDelegate = self
v.loadHTMLString(HTML, baseURL: nil)
v.allowsBackForwardNavigationGestures = true
v.snp.makeConstraints({ (make) in
if BasicTool.isIPhoneXSeries {
make.top.equalTo(BasicTool.iphoneXSafeAreaInsets().top + NavigationBarDefaultHeight)
}else{
make.top.equalTo(TopLayoutDefaultHeight)
}
make.bottom.left.right.equalTo(self.view)
})
})
- 其中,wkConfig和HTML分别为:
// wkConfig
lazy var wkConfig: BaseWKConfiguration! = BaseWKConfiguration(self)
// HTML
let HTML = try! String(contentsOfFile: Bundle.main.path(forResource: "index", ofType: "html")!, encoding: String.Encoding.utf8)
- 附上本地测试HTML文件内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0,user-scalable=no"/>
</head>
<body>
名字:<span id="name"></span>
<br/>
<button onclick="responseSwift()">点击响应Swift</button>
<script type="text/javascript">
function sayHello(name) {
document.getElementById("name").innerHTML = name
return "Swift成功唤起H5!"
}
document.title = "WKWebView标题获取成功"
function responseSwift() {
window.webkit.messageHandlers.JS方法名.postMessage("参数值");
}
</script>
</body>
</html>
四、WKWebView与JS交互
- JS调用Swift:
- 在WKUserContentController里注入商定的JS方法,这里以"openUrl"为例:
add(contentHandleDelegate, name: "openUrl")
- 在WKUserContentController实现contentHandleDelegate代理:
// 这里我声明的自定义协议方法名为:openUrl
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
debugPrint(message.name)
debugPrint(message.body)
debugPrint(message.frameInfo)
guard let delegate = messageHandleDelegate else {
return
}
switch message.name {
case "openUrl":
if delegate.responds(to: #selector(delegate.openUrl(_:))) {
DispatchQueue.main.async {
delegate.openUrl(message.body as? String ?? "")
}
}
default:
return
}
}
- 在当前控制器实现自定义协议方法:
extension WebViewController: WKJSImplementDelegate {
func openUrl(_ param: String) {
debugPrint("JS调用Swift成功啦!!!参数值为======== \(param)")
}
}
-
Swift调用JS:
在WKNavigationDelegate里的didFinish navigation方法里调用evaluateJavaScript:
// 页面加载完成之后调用,与UIWebView的代理:webViewDidFinishLoad对应 第二步
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
debugPrint("didFinish navigation ======")
WKWeb.evaluateJavaScript("document.title") { (title, error) in
let titleStr = title as? String ?? ""
navigationItem.title = titleStr
debugPrint(titleStr)
}
webView.evaluateJavaScript("sayHello('WKWebView你好!')") { (result, err) in
debugPrint(result)
}
}
// 控制台打印结果为:
"WKWebView标题获取成功"
Optional(Swift你好!)
//成功获取H5的值,代表Swift调用JS成功。
五、亮点记录:
- 如果是从UIWebView切换过来的,在尽量不改动H5端代码的前提下,那么原生WKWebView就需要将原有调用JS的方式做一层转换。演示如下:
- 这里以JS方法名为:openUrl 举例,参数值为:https://www.jianshu.com
// UIWebView调用JS方式:
window.openUrl("https://www.jianshu.com")
// WKWebView调用JS方式:
window.webkit.messageHandlers.openUrl.postMessage("https://www.jianshu.com")
- WKUserContentController类里进行方法转换:
let scriptSource = "setTimeout(function(){window.openUrl=function(str){window.webkit.messageHandlers.openUrl.postMessage(str)};}, 1)"
let userScript = WKUserScript(source: scriptSource, injectionTime: .atDocumentStart, forMainFrameOnly: true)
userContentController.addUserScript(userScript)
- 如此,不管H5是以何种方式调用JS,WKWebView均可以响应该方法。
- 如果H5端是通过注入对象的形式调用JS,比如注入对象名为:WKTest(这个可以根据自己项目自定义)
那么,上述的转换方法就需要更改为:
//
// 脚本里声明该JS对象,用分号隔开,然后通过WKTest.function的形式调用:
let scriptSource = "var WKTest = new String();setTimeout(function(){WKTest.openUrl=function(str){window.webkit.messageHandlers.openUrl.postMessage(str)};}, 1)"
let userScript = WKUserScript(source: scriptSource, injectionTime: .atDocumentStart, forMainFrameOnly: true)
userContentController.addUserScript(userScript)
- JS调用原生有返回值方法:
原理:将js的方法转换成prompt函数,APP再将返回值给prompt函数,再将prompt接收到的值返回给原始的js方法。
// 注入脚本,这里js对象名为WKTest(自定义),方法名为getDeviceInfo
let jsSourceStr = "setTimeout(function(){WKTest.getDeviceInfo=function(){return window.prompt('getDeviceInfo');};},1);"
let userScript = WKUserScript(source: jsSourceStr, injectionTime: .atDocumentStart, forMainFrameOnly: true)
userContentController.addUserScript(userScript)
// WKWebView实现WKUIDelegate方法:
WKWeb.uiDelegate = self
// 代理方法实现:
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
switch prompt {
case "getDeviceInfo":
completionHandler(getDeviceInfo())
default:
completionHandler(defaultText)
}
}
// 自定义原生需要回传H5返回值方法:
func getDeviceInfo() -> String {
return xxx
}
参考链接: