再學Android之WebView

騎著蝸牛闖紅燈發表於2019-06-11

WebView

最近一直在做web前端開發,做了預定酒店系統,後臺管理系統,小程式等,正好趁機複習一下Android的WebView

先簡單介紹一下,Android在4.4之後採用了Chrome核心,所以我們在開發web頁面的時候,es6的語法,css3的樣式等大可放心使用

我將分下面幾個模組去介紹Android上面WebView

WebView自身的一些方法

  //方式1. 載入一個網頁:
  webView.loadUrl("http://www.google.com/");

  //方式2:載入apk包中的html頁面
  webView.loadUrl("file:///android_asset/test.html");

  //方式3:載入手機本地的html頁面
   webView.loadUrl("content://com.android.htmlfileprovider/sdcard/test.html");
複製程式碼

正常情況下,在WebView介面,使用者點選返回鍵是直接退出該頁面的,著當然不是我們想要的,我們想要的是網頁自己的前進和後退,所以下面介紹網頁前進和後退的一些API

//判斷是否可以後退
Webview.canGoBack() 
//後退網頁
Webview.goBack()

//判斷是否可以前進                     
Webview.canGoForward()
//前進網頁
Webview.goForward()

// 引數傳負的話表示後退,傳正值的話表示的是前進
Webview.goBackOrForward(int steps) 
複製程式碼
對返回鍵的監聽,來實現網頁的後退
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if ((keyCode == KEYCODE_BACK) && mWebView.canGoBack()) { 
        mWebView.goBack();
        return true;
    }
    return super.onKeyDown(keyCode, event);
}
複製程式碼

如何防止WebView記憶體洩漏

防止記憶體洩漏的一個原則就是:生命週期長的不要跟生命週期短的玩。為了防止WebView不造成記憶體洩漏,

  • 不要在xml裡面定義WebView,而是在Activity選中使用程式碼去構建,並且Context使用ApplicationContext
  • 在Activity銷燬的時候,先讓WebView載入空內容,然後重rootView中移除WebView,再銷燬WebView,最後置空
override fun onDestroy() {
        if (webView != null) {
            webView!!.loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
            webView!!.clearHistory()
            (webView!!.parent as ViewGroup).removeView(webView)
            webView!!.destroy()
            webView = null
        }
        super.onDestroy()

    }
複製程式碼

WebSetting和WebViewClient,WebChromeClient

  • WebSetting

作用:對WebView進行配置和管理

WebSettings webSettings = webView.getSettings();
// 設定可以與js互動,為了防止資源浪費,我們可以在Activity
// 的onResume中設定為true,在onStop中設定為false
webSettings.setJavaScriptEnabled(true); 

//設定自適應螢幕,兩者合用
//將圖片調整到適合webview的大小 
webSettings.setUseWideViewPort(true); 
 // 縮放至螢幕的大小
webSettings.setLoadWithOverviewMode(true);

//設定編碼格式
webSettings.setDefaultTextEncodingName("utf-8");

// 設定允許JS彈窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

//設定快取的模式
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);

複製程式碼

關於快取的設定:

當載入 html 頁面時,WebView會在/data/data/包名目錄下生成 database 與 cache 兩個資料夾,請求的 URL記錄儲存在 WebViewCache.db,而 URL的內容是儲存在 WebViewCache 資料夾下

快取模式如下:
 //LOAD_CACHE_ONLY: 不使用網路,只讀取本地快取資料
 //LOAD_DEFAULT: (預設)根據cache-control決定是否從網路上取據。
 //LOAD_NO_CACHE: 不使用快取,只從網路獲取資料.
 //LOAD_CACHE_ELSE_NETWORK,只要本地有,無論是否過期,或no-cache,都使用快取中的資料。


複製程式碼

離線載入

if (NetStatusUtil.isConnected(getApplicationContext())) {
    webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);//根據cache-control決定是否從網路上取資料。
} else {
    webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);//沒網,則從本地獲取,即離線載入
}

webSettings.setDomStorageEnabled(true); // 開啟 DOM storage API 功能
webSettings.setDatabaseEnabled(true);   //開啟 database storage API 功能
webSettings.setAppCacheEnabled(true);//開啟 Application Caches 功能

String cacheDirPath = getFilesDir().getAbsolutePath() + APP_CACAHE_DIRNAME;
webSettings.setAppCachePath(cacheDirPath); //設定  Application Caches 快取目錄
複製程式碼
  • WebViewClient 作用:處理各種通知,請求事件,主要有,網頁開始載入,記載結束,載入錯誤(如404),處理https請求,具體使用請看下面程式碼,註釋清晰
webView!!.webViewClient = object : WebViewClient() {
    // 啟用WebView,而不是系統自帶的瀏覽器
            override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
                view.loadUrl(url)
                return true
            }

// 頁面開始載入,我們可以在這裡設定loading
            override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                super.onPageStarted(view, url, favicon)
                tv_start.text = "開始載入了..."
            }
// 頁面載入結束,關閉loading
            override fun onPageFinished(view: WebView?, url: String?) {
                super.onPageFinished(view, url)
                tv_end.text = "載入結束了..."
            }

            // 只要載入html,js,css的資源,每次都會回撥到這裡
            override fun onLoadResource(view: WebView?, url: String?) {
                loge("onLoadResource invoked")
            }

// 在這裡我們可以載入我們自己的404頁面
            override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
                loge("載入錯誤:${error.toString()}")
            }

// webview預設設計是不開啟https的,下面的設定是允許使用https
            override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
                handler?.proceed()
            }

            // js呼叫Android的方法,在這裡可以,該方法不存在通過註解的方式的記憶體洩漏,但是想拿到Android的返回值的話很難,
            // 可以通過Android呼叫js的程式碼的形式來傳遞返回值,例如下面的方式
            // Android:MainActivity.java
            //  mWebView.loadUrl("javascript:returnResult(" + result + ")");
            // JS:javascript.html
            //  function returnResult(result){
            //    alert("result is" + result);
            //   }

            override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
                val uri = Uri.parse(request?.url.toString())
                // 一般根據scheme(協議格式) & authority(協議名)判斷(前兩個引數)
                //假定傳入進來的 url = "js://webview?arg1=111&arg2=222"(同時也是約定好的需要攔截的)
                if (uri.scheme == "js") {
                    if (uri.authority == "webview") {
                        toast_custom("js呼叫了Android的方法")
                        val queryParameterNames = uri.queryParameterNames
                        queryParameterNames.forEach {
                            loge(it + ":" + uri.getQueryParameter(it))
                        }
                    }
                    return true
                }
                return super.shouldOverrideUrlLoading(view, request)
            }

            // 攔截資源 通常用於h5的首頁頁面,將常用的一些資源,放到本地
            override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
                if(request?.url.toString().contains("logo.gif")){
                    var inputStream: InputStream? = null
                    inputStream = applicationContext.assets.open("images/test.png")
                    return WebResourceResponse("image/png","utf-8", inputStream)
                }
                return super.shouldInterceptRequest(view, request)
            }
        }
複製程式碼

注意:5.1以上預設禁止了https和http的混用,下面的設定是開啟

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
複製程式碼
  • WebChromeClient 作用:輔助webview的一下回撥方法,可以得到網頁載入的進度,網頁的標題,網頁的icon,js的一些彈框,直接看程式碼,註釋清晰:
webView!!.webChromeClient = object : WebChromeClient() {

            // 網頁載入的進度
            override fun onProgressChanged(view: WebView?, newProgress: Int) {
                tv_progress.text = "$newProgress%"
            }

// 獲得網頁的標題
            override fun onReceivedTitle(view: WebView?, title: String?) {
                tv_title.text = title
            }

//js Alert
            override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {

                AlertDialog.Builder(this@WebActivity)
                    .setTitle("JsAlert")
                    .setMessage(message)
                    .setPositiveButton("OK") { _, _ -> result?.confirm() }
                    .setCancelable(false)
                    .show()
                return true
            }

// js Confirm
            override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
                return super.onJsConfirm(view, url, message, result)
            }

//js Prompt
            override fun onJsPrompt(
                view: WebView?,
                url: String?,
                message: String?,
                defaultValue: String?,
                result: JsPromptResult?
            ): Boolean {
                return super.onJsPrompt(view, url, message, defaultValue, result)
            }


        }
複製程式碼

Android和js的互動

  • Android呼叫js

    1.通過webview的loadUrl

    注意:該方式必須在webview載入完畢之後才能呼叫,也就是webviewClient的onPageFinished()方法回撥之後,而且該方法的執行 會重新整理介面,效率較低

    js程式碼:
    function callJs(){
        alert("Android 呼叫了 js程式碼)
    }
    kotlin程式碼:
    webView?.loadUrl("javascript:callJs()")
    複製程式碼

    2.通過webview的evaluateJavaScript

    比起第一種方法,效率更高,但是要在4.4之後才能使用

    js程式碼:
    function callJs(){
       //  alert("Android 呼叫了 js程式碼)
        return {name:'wfq',age:25}
    }
    kotlin程式碼:
    webView?.evaluateJavascript("javascript:callJs()") {
        // 這裡直接拿到的是js程式碼的返回值
                toast(it) // {name:'wfq',age:25}
            }
    複製程式碼
  • js呼叫Android

    1.通過webview的addJavaScriptInterface進行物件對映

    我們可以單獨定義一個類,所有需要互動的方法可以全部寫在這個類裡面,當然也可以直接寫在Activity裡面,下面以直接定義在Activity裡面為例,優點:使用方便,缺點:存在漏洞(4.2之前),請看下面的“WebView的一些漏洞以及如何防止”

    kotlin中定義被js呼叫的方法
     @JavascriptInterface
    fun hello(name: String) {
        toast("你好,我是來自js的訊息:$msg")
    }
    js程式碼
    function callAndroid(){
        android.hello("我是js的,我來呼叫你了")
    }
    kotlin中們在webview裡面設定Android與js的程式碼的對映
    webView?.addJavascriptInterface(this, "android")
    複製程式碼

    2.通過webviewClient的shouldOverrideUrlLoading的回撥來攔截url

    具體使用:解析該url的協議,如果監測到是預先約定好的協議,那麼就呼叫相應的方法。比較安全,但是使用麻煩,js獲取Android的返回值的話很麻煩,只能通過上面介紹的通過loadurl()去執行js程式碼把返回值通過引數傳遞回去

    首先在js中約定號協議
    function callAndroid(){
            // 約定的url協議為:js://webview?name=wfq&age=24
            document.location = "js://webview?name=wfq&age=24"
         }
    在kotlin裡面,當loadurl的時候就會回撥到shouldOverrideUrlLoading()裡面
    
    override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
                val uri = Uri.parse(request?.url.toString())
                // 一般根據scheme(協議格式) & authority(協議名)判斷(前兩個引數)
                //假定傳入進來的 js://webview?name=wfq&age=24
                if (uri.scheme == "js") {
                    if (uri.authority == "webview") {
                        toast_custom("js呼叫了Android的方法")
                        val queryParameterNames = uri.queryParameterNames
                        queryParameterNames.forEach {
                            loge(it + ":" + uri.getQueryParameter(it))
                        }
                    }
                    return true
                }
                return super.shouldOverrideUrlLoading(view, request)
            }
    
    複製程式碼

    3.通過webChromeClient的onJsAlert,onJsConfirm,onJsPrompt回撥來攔截對話方塊

    通過攔截js對話方塊,得到他們的訊息,然後解析即可,為了安全,建議內容採用上面介紹的url協議, 常用的攔截的話就是攔截prompt,因為它可以返回任意值,alert沒有返回值,confirm只能返回兩種型別,確定和取消

    js程式碼
    function clickprompt(){
        var result=prompt("wfq://demo?arg1=111&arg2=222");
        alert("demo " + result);
    }
    kotlin程式碼
    override fun onJsPrompt(
                view: WebView?,
                url: String?,
                message: String?,
                defaultValue: String?,
                result: JsPromptResult?
            ): Boolean {
                val uri = Uri.parse(message)
                if (uri.scheme == "wfq") {
                    if (uri.authority == "demo") {
                        toast_custom("js呼叫了Android的方法")
                        val queryParameterNames = uri.queryParameterNames
                        queryParameterNames.forEach {
                            loge(it + ":" + uri.getQueryParameter(it))
                        }
                        // 將需要返回的值通過該方式返回
                        result?.confirm("js呼叫了Android的方法成功啦啦啦啦啦")
                    }
                    return true
                }
                return super.onJsPrompt(view, url, message, defaultValue, result)
            }
    
    由於攔截了彈框,所以js程式碼的alert需要處理 這裡的message便是上面程式碼的返回值通過alert顯示出來的資訊
    override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
                AlertDialog.Builder(this@WebActivity)
                    .setTitle("JsAlert")
                    .setMessage(message)
                    .setPositiveButton("OK") { _, _ -> result?.confirm() }
                    .setCancelable(false)
                    .show()
                return true
            }
    
    複製程式碼

    上面三種方式的區別: addJavascriptInterface() 方便簡潔,4.0以下存在漏洞,4.0以上通過@JavascriptInterface註解修復漏洞 WebViewClient.shouldOverrideUrlLoading()回撥,不存在漏洞,使用複雜,需要定義協議的約束,但是返回值的話有些麻煩,在不需要返回值的情況下可以使用這個方式 通過WebChromeClient的onJsAlerta,onJsConfirm,onJsPrompt,不存在漏洞問題,使用複雜,需要進行協議的約束,可以返回值,能滿足大多數情況下的互調通訊

WebView的一些漏洞以及如何防止

參考騰訊大神Carson_Ho的簡書 www.jianshu.com/p/3a345d27c…

密碼明文儲存漏洞

webview預設開啟了密碼儲存功能,在使用者輸入密碼後會彈出提示框詢問使用者是否儲存密碼,儲存後密碼會被明文儲存在 /data/data/com.package.name/databases/webview.db 下面,手機root後可以檢視,那麼如何解決?

WebSettings.setSavePassword(false) // 關閉密碼儲存提醒功能
複製程式碼

WebView 任意程式碼執行漏洞

addJavascriptInterface漏洞,首先先明白一點,js呼叫Android程式碼的時候,我們經常使用的是addJavascriptInterface, JS呼叫Android的其中一個方式是通過addJavascriptInterface介面進行物件對映,那麼Android4.2之前,既然拿到了這個物件,那麼這個物件中的所有方法都是可以呼叫的,4.2之後,需要被js呼叫的函式加上@JavascriptInterface註解後來避免該漏洞

所以怎麼解決

對於Android 4.2以前,需要採用攔截prompt()的方式進行漏洞修復
對於Android 4.2以後,則只需要對被呼叫的函式以 @JavascriptInterface進行註解
複製程式碼

域控制不嚴格漏洞

  • 原因分析 當我們在Applilcation裡面,android:exported="true"的時候,A 應用可以通過 B 應用匯出的 Activity 讓 B 應用載入一個惡意的 file 協議的 url,從而可以獲取 B 應用的內部私有檔案,從而帶來資料洩露威脅,

下面來看下WebView中getSettings類的方法對 WebView 安全性的影響 setAllowFileAccess()

// 設定是否允許 WebView 使用 File 協議
// 預設設定為true,即允許在 File 域下執行任意 JavaScript 程式碼
webView.getSettings().setAllowFileAccess(true);     
如果設定為false的話,便不會存在威脅,但是,webview也無法使用本地的html檔案


複製程式碼

setAllowFileAccessFromFileURLs()

// 設定是否允許通過 file url 載入的 Js程式碼讀取其他的本地檔案
// 在Android 4.1前預設允許
// 在Android 4.1後預設禁止
webView.getSettings().setAllowFileAccessFromFileURLs(true);

我們應該明確的設定為false,禁止讀取其他檔案

複製程式碼

setAllowUniversalAccessFromFileURLs()

// 設定是否允許通過 file url 載入的 Javascript 可以訪問其他的源(包括http、https等源)
// 在Android 4.1前預設允許(setAllowFileAccessFromFileURLs()不起作用)
// 在Android 4.1後預設禁止
webView.getSettings().setAllowUniversalAccessFromFileURLs(true);
複製程式碼

WebView預載入以及資源預載入

為什麼需要預載入

h5頁面載入慢,慢的原因:頁面渲染慢,資源載入慢

如何優化?

h5的快取,資源預載入,資源攔截

  • h5的快取 Android WebView自帶的快取 1.瀏覽器快取

    根據 HTTP 協議頭裡的 Cache-Control(或 Expires)和 Last-Modified(或 Etag)等欄位來控制檔案快取的機制
    瀏覽器自己實現,我需我們處理
    複製程式碼

    2.App Cache

    方便構建Web App的快取,儲存靜態檔案(如JS、CSS、字型檔案)
     WebSettings settings = getSettings();
     String cacheDirPath = context.getFilesDir().getAbsolutePath()+"cache/";
     settings.setAppCachePath(cacheDirPath);
     settings.setAppCacheMaxSize(20*1024*1024);
     settings.setAppCacheEnabled(true);
    複製程式碼

    3.Dom Storage

        WebSettings settings = getSettings();
        settings.setDomStorageEnabled(true);
    複製程式碼

    4.Indexed Database

     // 只需設定支援JS就自動開啟IndexedDB儲存機制
     // Android 在4.4開始加入對 IndexedDB 的支援,只需開啟允許 JS 執行的開關就好了。
    WebSettings settings = getSettings();
    settings.setJavaScriptEnabled(true);
       
    複製程式碼
  • 資源預載入 預載入webview物件,首次初始化WebView會比第二次慢很多的原因:初始化後,即使webview已經釋放,但是WebView的一些共享的物件依然是存在的,我們可以在Application裡面提前初始化一個Webview的物件,然後可以直接loadurl載入資源

  • 資源攔截 可以將跟新頻率低的一些資源靜態檔案放在本地,攔截h5的資源網路請求並進行檢測,如果檢測到,就直接拿本地的資源進行替換即可

// 攔截資源 通常用於h5的首頁頁面,將常用的一些資源,放到本地
            override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {

                if(request?.url.toString().contains("logo.jpg")){
                    var inputStream: InputStream? = null
                    inputStream = applicationContext.assets.open("images/test.jpg")
                    return WebResourceResponse("image/png","utf-8", inputStream)
                }

                return super.shouldInterceptRequest(view, request)
            }
複製程式碼

常見的使用注意事項:

Android9.0,已經禁止了webview使用http,怎麼解決?

在manifest的Application標籤下面使用:android:usesCleartextTraffic="true"

開啟混淆之後,Android無法與h5互動?

#保留annotation, 例如 @JavascriptInterface 等 annotation
-keepattributes *Annotation*

#保留跟 javascript相關的屬性
-keepattributes JavascriptInterface

#保留JavascriptInterface中的方法
-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}
#這個類是用來與js互動,所以這個類中的 欄位 ,方法, 不能被混淆、全路徑名稱.類名
-keepclassmembers public class com.youpackgename.xxx.H5CallBackAndroid{
   <fields>;
   <methods>;
   public *;
   private *;
}
複製程式碼

如何除錯?

1.在WebViewActivity裡面,開啟除錯

// 開啟除錯
WebView.setWebContentsDebuggingEnabled(true)
複製程式碼

2.chrome瀏覽器位址列輸入 chrome://inspect

3.手機開啟USB除錯,開啟webview頁面,點選chrome頁面的最下面的inspect,這樣,便可以進入了web開發,看控制檯,網路請求等

相關文章