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