iOS WKWebView探究

一逗爹、WKWebView初識

  • 原生提供的一種用于加載H5網(wǎng)頁的控件飒硅,運用Safari瀏覽器相同的JavaScript引擎徒蟆,相同的WebKit內(nèi)核胎挎,大大提高頁面JS執(zhí)行速度沟启,相當(dāng)于UIWebView的封裝。

二犹菇、WKWebView相比于UIWebView的優(yōu)缺點

  1. 優(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. 缺點
  • 問題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初始化

  1. 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 ......")
    }
}
  1. 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 ......")
    }
}
  1. 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:
  1. 在WKUserContentController里注入商定的JS方法臂容,這里以"openUrl"為例:
    add(contentHandleDelegate, name: "openUrl")
  2. 在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
        }
    }
  1. 在當(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成功买决。

五沛婴、亮點記錄:

  1. 如果是從UIWebView切換過來的吼畏,在盡量不改動H5端代碼的前提下,那么原生WKWebView就需要將原有調(diào)用JS的方式做一層轉(zhuǎn)換嘁灯。演示如下:
// 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)該方法性雄。
  1. 如果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)
  1. 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
}

參考鏈接:

  1. https://blog.csdn.net/qq_15509071/article/details/86501951
  2. http://www.reibang.com/p/b93d52f61673
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者筒占。
  • 序言:七十年代末贪庙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子赋铝,更是在濱河造成了極大的恐慌插勤,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件革骨,死亡現(xiàn)場離奇詭異,居然都是意外死亡析恋,警方通過查閱死者的電腦和手機(jī)良哲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來助隧,“玉大人筑凫,你說我怎么就攤上這事〔⒋澹” “怎么了巍实?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長哩牍。 經(jīng)常有香客問我棚潦,道長,這世上最難降的妖魔是什么膝昆? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任丸边,我火速辦了婚禮叠必,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘妹窖。我一直安慰自己纬朝,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布骄呼。 她就那樣靜靜地躺著共苛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蜓萄。 梳的紋絲不亂的頭發(fā)上俄讹,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音绕德,去河邊找鬼患膛。 笑死,一個胖子當(dāng)著我的面吹牛耻蛇,可吹牛的內(nèi)容都是我干的踪蹬。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼臣咖,長吁一口氣:“原來是場噩夢啊……” “哼跃捣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起夺蛇,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤疚漆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后刁赦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體娶聘,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年甚脉,在試婚紗的時候發(fā)現(xiàn)自己被綠了丸升。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡牺氨,死狀恐怖狡耻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情猴凹,我是刑警寧澤夷狰,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站郊霎,受9級特大地震影響沼头,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜歹篓,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一瘫证、第九天 我趴在偏房一處隱蔽的房頂上張望揉阎。 院中可真熱鬧,春花似錦背捌、人聲如沸毙籽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坑赡。三九已至,卻和暖如春么抗,著一層夾襖步出監(jiān)牢的瞬間毅否,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工蝇刀, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留螟加,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓吞琐,卻偏偏與公主長得像捆探,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子站粟,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355