踩坑WKWebView

背景

在iOS應(yīng)用開(kāi)發(fā)中,內(nèi)嵌WebView一直占有一定的頁(yè)面數(shù)量比例。它能以較低的開(kāi)發(fā)成本實(shí)現(xiàn)iOS、Android和Web的復(fù)用,也可以一定程度的的規(guī)避蘋果對(duì)熱更新的封鎖。然而UIWebView的CPU資源消耗和內(nèi)存占用一直被嫌棄,導(dǎo)致很多客戶端中需要?jiǎng)討B(tài)更新等頁(yè)面時(shí)不得不采用其他方案。長(zhǎng)遠(yuǎn)來(lái)看,功能的動(dòng)態(tài)加載以及三端的融合將會(huì)是大趨勢(shì)。怎么解決WebView固有的問(wèn)題呢?我們將通過(guò)全面的對(duì)比來(lái)分析使用UIWebView的問(wèn)題。

全面對(duì)比

  • UIWebView使用UIKit框架,而WKWebView使用WebKit.framework。WKWebView采用與Safari 相同的 Nitro JavaScript 引擎,在cpu資源消耗方面,遠(yuǎn)低于UIWebView。在使用WKWebView后,應(yīng)用在打開(kāi)類似商城首頁(yè)這種加載內(nèi)容較多的網(wǎng)頁(yè)時(shí),CPU占用下降非常明顯

  • WKWebView為多進(jìn)程組件,會(huì)從App內(nèi)存中分離內(nèi)存到單獨(dú)的進(jìn)程(Network Process and Rendring Process)中。當(dāng)內(nèi)存超過(guò)了系統(tǒng)分配給WKWebView的內(nèi)存時(shí)候,會(huì)導(dǎo)致WKWebView瀏覽器崩潰白屏,但是App不會(huì)Crash(app會(huì)收到系統(tǒng)通知,并且嘗試去重新加載頁(yè)面)。相反UIWebView是和app同一個(gè)進(jìn)程,UIWebView加載頁(yè)面占用的內(nèi)存被計(jì)算為app內(nèi)存占用的一部分,當(dāng)app超過(guò)了系統(tǒng)分配的內(nèi)存,則會(huì)被操作系統(tǒng)crash。在整個(gè)過(guò)程中,會(huì)經(jīng)常收到iOS系統(tǒng)的通知用來(lái)防止app被系統(tǒng)kill,很多時(shí)候,這些通知并不及時(shí),或者根本沒(méi)有返回通知。

  • WKWebView是異步處理native與JavaScript之間的通信,所以執(zhí)行速度會(huì)更快。

  • WKWevbView內(nèi)存消耗較UIWebView大幅下降。

  • WKWebView有著高達(dá)60fps的滾動(dòng)刷新率以及內(nèi)置手勢(shì)

  • WKWevView更多的支持 HTML5 的特性

  • WKWevView將 UIWebViewDelegate 與 UIWebView 拆分成了14類與3個(gè)協(xié)議,包含更細(xì)節(jié)功能的實(shí)現(xiàn),詳解如下:
    WKBackForwardList: 之前訪問(wèn)過(guò)的 web 頁(yè)面的列表,可以通過(guò)后退和前進(jìn)動(dòng)作來(lái)訪問(wèn)到。

    WKBackForwardListItem: webview 中后退列表里的某一個(gè)網(wǎng)頁(yè)。

    WKFrameInfo: 包含一個(gè)網(wǎng)頁(yè)的布局信息。

    WKNavigation: 包含一個(gè)網(wǎng)頁(yè)的加載進(jìn)度信息。

    WKNavigationAction: 包含可能讓網(wǎng)頁(yè)導(dǎo)航變化的信息,用于判斷是否做出導(dǎo)航變化。

    WKNavigationResponse: 包含可能讓網(wǎng)頁(yè)導(dǎo)航變化的返回內(nèi)容信息,用于判斷是否做出導(dǎo)航變化。

    WKPreferences: 概括一個(gè) webview 的偏好設(shè)置。

    WKProcessPool: 表示一個(gè) web 內(nèi)容加載池。

    WKUserContentController: 提供使用 JavaScript post 信息和注射 script 的方法。

    WKScriptMessage: 包含網(wǎng)頁(yè)發(fā)出的信息。

    WKUserScript: 表示可以被網(wǎng)頁(yè)接受的用戶腳本。

    WKWebViewConfiguration: 初始化 webview 的設(shè)置。

    WKWindowFeatures: 指定加載新網(wǎng)頁(yè)時(shí)的窗口屬性。

    WKNavigationDelegate: 提供了追蹤主窗口網(wǎng)頁(yè)加載過(guò)程和判斷主窗口和子窗口是否進(jìn)行頁(yè)面加載新頁(yè)面的相關(guān)方法。

    WKUIDelegate: 提供用原生控件顯示網(wǎng)頁(yè)的方法回調(diào)。

    WKScriptMessageHandler: 提供從網(wǎng)頁(yè)中收消息的回調(diào)方法。

  • 在使用cookie方面,在使用 UIWebVIew 的時(shí)候我們并不關(guān)注 Cookie,因?yàn)樵谡{(diào)用登錄接口的時(shí)候無(wú)論是AFNetworking,還是其他,登錄成功之后都會(huì)自動(dòng)保存在[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies 中。但 WKWebView 的存儲(chǔ)體系與 UIWebVIew 完全不一樣,只能手動(dòng)給它添加 Cookie。這也是很多同行所詬病的地方,甚至因?yàn)檫@個(gè)原因而遲遲不愿意更新,下面貼出代碼供參考

private func configWebView() -> WKWebView {
    let webConfig = WKWebViewConfiguration()
    
    // 1.刪除沙盒中 之前舊版本的cookies以及現(xiàn)在的cookie  2.實(shí)時(shí)從登錄信息中組合cookies,不以沙盒中的cookie為準(zhǔn)
    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("開(kāi)始刪除:\(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ī)制不同:加載過(guò)程中所有的請(qǐng)求都不經(jīng)過(guò) NSURLProtocol,也就是WKWebView無(wú)法攔截響應(yīng)數(shù)據(jù)
  • 對(duì)于 WKWebView ,有三個(gè)屬性支持KVO,因此我們可以輕松監(jiān)聽(tīng)其值的變化,分別是:loading、title、estimatedProgress,對(duì)應(yīng)功能表示為:是否正在加載中、頁(yè)面的標(biāo)題、頁(yè)面內(nèi)容加載進(jìn)度(值為0.0~1.0),下面貼出我們實(shí)際在項(xiàng)目中kvo監(jiān)聽(tīng)的運(yùn)用
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    
    // 監(jiān)聽(tīng) WKWebView 對(duì)象的 estimatedProgress 屬性,就是當(dāng)前網(wǎng)頁(yè)加載的進(jìn)度
    webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
    
    // 監(jiān)聽(tīng) WKWebView 對(duì)象的 title 屬性,就是當(dāng)前網(wǎng)頁(yè)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ù)場(chǎng)景

native與JS的相互調(diào)用

WKWebView對(duì)于HTML5的操作已經(jīng)很便捷了,但是還沒(méi)有Android的WebView那樣簡(jiǎn)單。WebView能夠直接注入JavaScript對(duì)象,交互過(guò)程中Java 與 JavaScript甚至可以直接調(diào)用對(duì)方的方法,不用攔截,不用分發(fā),這樣的Java 與 JavaScript的交互非常清晰明了。在iOS上,還達(dá)不到這樣的便捷。
在使用WKWebView時(shí),H5調(diào)用Native 的過(guò)程是:1、Native注入JavaScript函數(shù);2、Native實(shí)現(xiàn)橋接方法:通過(guò)系統(tǒng)方法攔截JavaScript事件,匹配OC/Swift注冊(cè)列表,分發(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的過(guò)程是:使用WKWebView的接口調(diào)用JavaScript函數(shù)。附上代碼

self.webView.evaluateJavaScript(javaScript, completionHandler: { (_, error) in
    if let error = error {
        FCLog("webJS——\(javaScript)執(zhí)行失敗: \(error.localizedDescription)")
    }
})

動(dòng)態(tài)加載并運(yùn)行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對(duì)象
let testScript = WKUserScript(source: jsStr, injectionTime:.atDocumentEnd, forMainFrameOnly: true)
let testController = WKUserContentController()
testController.addUserScript(testScript)

// 根據(jù)生成的WKUserScript對(duì)象,初始化WKWebViewConfiguration
let webConfiguration = WKWebViewConfiguration()
webConfiguration.userContentController = testController
let testWebview = WKWebView(frame: CGRect.zero, configuration: webConfiguration)
view.addSubview(testWebview)

踩坑實(shí)錄

  • 對(duì)于JS有異步接口回調(diào)的數(shù)據(jù)情況,可能導(dǎo)致頁(yè)面加載數(shù)據(jù)異常,除了延時(shí)機(jī)制外,還沒(méi)找到好的解決辦法。有高招的大神,求指導(dǎo)。
  • 創(chuàng)建WKWebViewConfiguration的實(shí)例,這個(gè)實(shí)例可以給網(wǎng)頁(yè)進(jìn)行一些配置。注意這個(gè)實(shí)例只能在 webView第一次創(chuàng)建的時(shí)候才能使用。
  • WKWebView 點(diǎn)擊H5內(nèi)鏈接無(wú)反應(yīng),多半是因?yàn)榫W(wǎng)頁(yè)中有target="_blank" 在新窗口打開(kāi)連接,此時(shí)我們需要設(shè)置WKWebView的另外一個(gè)代理WKUIDelegate,并實(shí)現(xiàn)代理方法如下:
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
       webView.load(navigationAction.request)
       return nil
   }
  • App之間的數(shù)據(jù)交互是通過(guò)URL Scheme來(lái)實(shí)現(xiàn)的。在UIWebView時(shí)代如果加載的URL是“customURLScheme://”這種形式的,UIWebView會(huì)執(zhí)行openUrl函數(shù),從而和其他App進(jìn)行交互,然而WKWebView就需要自己支持了,我們要對(duì)非http://和https://的做相應(yīng)處理,附上代碼:
if !(urlString.hasPrefix("http://") || urlString.hasPrefix("https://")) {
    if UIApplication.shared.canOpenURL(appUrl) {
        UIApplication.shared.openURL(appUrl)
    }
}
  • WKWebView不會(huì)像UIWebView把Content-Type標(biāo)頭設(shè)置為POST請(qǐng)求的application / x-www-formurlencoded,都要手動(dòng)添加,否則會(huì)造成所加載H5頁(yè)面出錯(cuò)
let request = NSMutableURLRequest(url: url)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評(píng)論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,559評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,442評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,835評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,581評(píng)論 6 412
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,922評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評(píng)論 3 447
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 43,096評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,639評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,374評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,591評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,789評(píng)論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,196評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,524評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,322評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,554評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容