再學Android之WebView

WebView

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

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

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

WebView自身的一些方法html

//方式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內存泄漏java

防止內存泄漏的一個原則就是:生命週期長的不要跟生命週期短的玩。爲了防止WebView不形成內存泄漏,android

  • 不要在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進行配置和管理css3

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);

複製代碼

關於緩存的設置:es6

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

緩存模式以下:
 //LOAD_CACHE_ONLY: 不使用網絡,只讀取本地緩存數據
 //LOAD_DEFAULT: (默認)根據cache-control決定是否從網絡上取據。
 //LOAD_NO_CACHE: 不使用緩存,只從網絡獲取數據.
 //LOAD_CACHE_ELSE_NETWORK,只要本地有,不管是否過時,或no-cache,都使用緩存中的數據。


複製代碼

離線加載chrome

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開發,看控制檯,網絡請求等

相關文章
相關標籤/搜索