一逗爹、WKWebView初識
- 原生提供的一種用于加載H5網(wǎng)頁的控件飒硅,運用Safari瀏覽器相同的JavaScript引擎徒蟆,相同的WebKit內(nèi)核胎挎,大大提高頁面JS執(zhí)行速度沟启,相當(dāng)于UIWebView的封裝。
二犹菇、WKWebView相比于UIWebView的優(yōu)缺點
- 優(yōu)點
-
更少的內(nèi)存占用德迹,優(yōu)化性能管理。如下圖示揭芍,這是加載相同網(wǎng)頁時兩種WebView的內(nèi)存占用表現(xiàn)胳搞。
UIWebView內(nèi)存占用
WKWebView內(nèi)存占用 - 增加新的代理方法,可控性更高称杨。
- JS交互上更加方便:WKWebView支持直接注入JS方法名肌毅,不需要通過JavaScriptCore作為中間橋梁。
Swift實現(xiàn):userContentController.add(self, name: "openUrl")
H5調(diào)用:window.webkit.messageHandlers.openUrl.postMessage("XXX");
Tips:JS只支持單個參數(shù)傳遞姑原,如果需要傳遞多個數(shù)據(jù)悬而,建議使用JSON字符串傳值。
- 缺點
- 問題1:承載當(dāng)前WebView的控制器無法正常釋放锭汛。
原因:WKUserContentController對self有個引用笨奠,而WKWebConfiguration對WKUserContentController有引用袭蝗,WebView初始化時對WKWebConfiguration有引用,而WebView本身又是self的一個變量般婆,這就相當(dāng)于self引用了self呻袭,形成循環(huán)引用,導(dǎo)致控制器無法被正常釋放腺兴。
解決方案:在WKUserContentController里初始化一個新的NSObject對象左电,弱引用WKScriptMessageHandle,在WKUserContentController實現(xiàn)代理页响,然后設(shè)置一個新協(xié)議篓足,將WKUserContentController的代理實現(xiàn)掛載出去,用于controller實現(xiàn)闰蚕,這樣就不會引起循環(huán)引用栈拖,從而解決controller無法正常釋放問題。步驟如下:
(1) 聲明一個新的繼承于NSObject的class對象WKScriptMessageHandleDelegate没陡,實現(xiàn)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里面初始化新對象涩哟,并實現(xiàn)代理:
class BaseWKUCController: WKUserContentController {
weak var messageHandleDelegate: WKJSImplementDelegate?
override init() {
super.init()
let contentHandleDelegate = WKScriptMessageHandleDelegate()
contentHandleDelegate.messageHandleDelegate = self
// 成對出現(xiàn)
add(contentHandleDelegate, name: "JS方法名")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
debugPrintOnly("\(self) is deinit ......")
}
}
(3) 聲明一個新的代理,用于JS的具體實現(xiàn):
@objc
protocol WKJSImplementDelegate: NSObjectProtocol {
@objc func openUrl(_ param: String)
}
(4) WKUserContentController里實現(xiàn)代理:
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時盼玄,在當(dāng)前的Controller里實現(xiàn)WKJSImplementDelegate即可贴彼。
- 問題2:斷點調(diào)試發(fā)現(xiàn)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
// 成對出現(xiàn)
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文件內(nèi)容:
<!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()">點擊響應(yīng)Swift</button>
<script type="text/javascript">
function sayHello(name) {
document.getElementById("name").innerHTML = name
return "Swift成功喚起H5!"
}
document.title = "WKWebView標(biāo)題獲取成功"
function responseSwift() {
window.webkit.messageHandlers.JS方法名.postMessage("參數(shù)值");
}
</script>
</body>
</html>
四幼东、WKWebView與JS交互
- JS調(diào)用Swift:
- 在WKUserContentController里注入商定的JS方法臂容,這里以"openUrl"為例:
add(contentHandleDelegate, name: "openUrl")
- 在WKUserContentController實現(xiàn)contentHandleDelegate代理:
// 這里我聲明的自定義協(xié)議方法名為: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
}
}
- 在當(dāng)前控制器實現(xiàn)自定義協(xié)議方法:
extension WebViewController: WKJSImplementDelegate {
func openUrl(_ param: String) {
debugPrint("JS調(diào)用Swift成功啦!=畲帧策橘!參數(shù)值為======== \(param)")
}
}
-
Swift調(diào)用JS:
在WKNavigationDelegate里的didFinish navigation方法里調(diào)用evaluateJavaScript:
// 頁面加載完成之后調(diào)用,與UIWebView的代理:webViewDidFinishLoad對應(yīng) 第二步
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)
}
}
// 控制臺打印結(jié)果為:
"WKWebView標(biāo)題獲取成功"
Optional(Swift你好丽已!)
//成功獲取H5的值,代表Swift調(diào)用JS成功买决。
五沛婴、亮點記錄:
- 如果是從UIWebView切換過來的吼畏,在盡量不改動H5端代碼的前提下,那么原生WKWebView就需要將原有調(diào)用JS的方式做一層轉(zhuǎn)換嘁灯。演示如下:
- 這里以JS方法名為:openUrl 舉例泻蚊,參數(shù)值為:http://www.reibang.com
// UIWebView調(diào)用JS方式:
window.openUrl("http://www.reibang.com")
// WKWebView調(diào)用JS方式:
window.webkit.messageHandlers.openUrl.postMessage("http://www.reibang.com")
- WKUserContentController類里進(jìn)行方法轉(zhuǎn)換:
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是以何種方式調(diào)用JS丑婿,WKWebView均可以響應(yīng)該方法性雄。
- 如果H5端是通過注入對象的形式調(diào)用JS,比如注入對象名為:WKTest(這個可以根據(jù)自己項目自定義)
那么羹奉,上述的轉(zhuǎn)換方法就需要更改為:
//
// 腳本里聲明該JS對象秒旋,用分號隔開,然后通過WKTest.function的形式調(diào)用:
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調(diào)用原生有返回值方法:
原理:將js的方法轉(zhuǎn)換成prompt函數(shù)诀拭,APP再將返回值給prompt函數(shù)迁筛,再將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實現(xiàn)WKUIDelegate方法:
WKWeb.uiDelegate = self
// 代理方法實現(xiàn):
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
}
參考鏈接: