雖然你們是扮演路人甲乙丙丁,但是一樣是有生命,有靈魂的领追。”——《喜劇之王》
前言
在開(kāi)發(fā)原生Android應(yīng)用過(guò)程中惦银,某些情況下(比如活動(dòng)詳情頁(yè))為了賦予應(yīng)用適當(dāng)?shù)膭?dòng)態(tài)性我們經(jīng)常需要使用webview控件在應(yīng)用中內(nèi)置web網(wǎng)頁(yè),對(duì)于混合app(Hybrid App)來(lái)說(shuō)WebView更是必不可少末誓,所以學(xué)習(xí)webview相關(guān)知識(shí)扯俱,對(duì)webview有一個(gè)充分全面的了解對(duì)Android開(kāi)發(fā)者來(lái)說(shuō)非常重要。
(注:Android WebView從實(shí)現(xiàn)的Framework層大致可以分為三段Android 4.0系列喇澡,Android 4.1---4.3系列迅栅,Android 4.4及其以上系列。Android 4.4以下(不包含4.4)系統(tǒng)WebView底層實(shí)現(xiàn)是采用WebKit內(nèi)核晴玖,而在Android4.4及以上Google改用Chromium 作為系統(tǒng)webview的底層內(nèi)核读存。在這一變化中Android 提供的WebView相關(guān)API并沒(méi)有發(fā)生大變化,在4.4上也兼容低版本的API并且引進(jìn)了少部分API窜醉∠芴眩基于Chromium WebView提供更廣的HTML5,CSS3,JavaScript支持,提升了性能榨惰,并增加了一些新的功能支持。4.0及4.1-4.3均基于WebKit內(nèi)核静汤,兩者之間的差異主要集中在Framework上的變化琅催,WebKit內(nèi)核極其在Android上的表現(xiàn)機(jī)制并沒(méi)有發(fā)生很大變化,他們的渲染機(jī)制是相同的虫给√俾眨可以簡(jiǎn)單理解為4.1-4.3主要對(duì)Framework進(jìn)行修改,目地是為了將內(nèi)核與上層API接口分離開(kāi)來(lái)抹估,為之后替換內(nèi)核做準(zhǔn)備缠黍。由于目前Android系統(tǒng)市場(chǎng)占有率4.4以下機(jī)型極少,所以本文主要針對(duì)4.4及以上webview進(jìn)行講解药蜻。)
webview簡(jiǎn)介
WebView是Android系統(tǒng)提供能顯示網(wǎng)頁(yè)的系統(tǒng)控件瓷式,它是一個(gè)特殊的View替饿,同時(shí)它也是一個(gè)ViewGroup可以有很多其他子View。
webview作用
- 顯示和渲染W(wǎng)eb頁(yè)面
- 加載網(wǎng)絡(luò)上或本地assets中的html文件
- 與JavaScript交互調(diào)用
將WebView添加到應(yīng)用
需要在AndroidManifest中添加網(wǎng)絡(luò)權(quán)限
<manifest ... >
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>
- 方式一:將以下代碼添加到Activity的布局XML文件中
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
在Activity中引用控件
WebView myWebView = (WebView) findViewById(R.id.webview);
myWebView.loadUrl("http://www.example.com");
- 方式二:直接在代碼中添加
WebView myWebView = new WebView(activityContext);
setContentView(myWebView);
or
FrameLayout flContainer = (FrameLayout) findViewById(R.id.fl_container);
WebView myWebView = new WebView(this);
flContainer.addView(myWebView );
WebView的基礎(chǔ)方法
- String getUrl():獲取當(dāng)前頁(yè)面的URL贸典。
- String getTitle():獲取當(dāng)前頁(yè)面的標(biāo)題视卢。
- Bitmap getFavicon():獲取當(dāng)前頁(yè)面的favicon
- int getProgress():獲取當(dāng)前頁(yè)面的加載進(jìn)度
- setInitialScale(int scaleInPercent):設(shè)置初始縮放比例
- reload():重新reload當(dāng)前的URL,即刷新廊驼。
WebView加載URL
- 方式1. 加載一個(gè)網(wǎng)頁(yè)
myWebView .loadUrl("https://www.baidu.com/");
- 方式2:加載apk包中的html頁(yè)面
myWebView .loadUrl("file:///android_asset/test.html");
- 方式3:加載手機(jī)本地的html頁(yè)面
myWebView .loadUrl("content://com.android.htmlfileprovider/sdcard/test.html");
- 方式4: 加載 HTML 頁(yè)面的一小段內(nèi)容
// 創(chuàng)建HTML文本
// 轉(zhuǎn)化未編碼HTML文本為字節(jié)碼据过,并進(jìn)行Base64編碼
// 加載編碼后的文本
String unencodedHtml =
"<html><body>'%23' is the percent code for ‘#‘ </body></html>";
String encodedHtml = Base64.encodeToString(unencodedHtml.getBytes(),
Base64.NO_PADDING);
myWebView.loadData(encodedHtml, "text/html", "base64");
其他方法:
- loadUrl(String url, Map<String, String> additionalHttpHeaders):攜帶http headers加載URL指定的網(wǎng)頁(yè)
- postUrl(String url, byte[] postData):使用POST請(qǐng)求加載指定的網(wǎng)頁(yè)
WebView的狀態(tài)
//激活WebView為活躍狀態(tài),能正常執(zhí)行網(wǎng)頁(yè)的響應(yīng)
webView.onResume() 妒挎;
//當(dāng)頁(yè)面被失去焦點(diǎn)被切換到后臺(tái)不可見(jiàn)狀態(tài)绳锅,需要執(zhí)行onPause
//通過(guò)onPause動(dòng)作通知內(nèi)核暫停所有的動(dòng)作,比如DOM的解析酝掩、plugin的執(zhí)行榨呆、JavaScript執(zhí)行。
webView.onPause()庸队;
//當(dāng)應(yīng)用程序(存在webview)被切換到后臺(tái)時(shí)积蜻,這個(gè)方法不僅僅針對(duì)當(dāng)前的webview而是全局的全應(yīng)用程序的webview
//它會(huì)暫停所有webview的layout,parsing彻消,javascripttimer竿拆。降低CPU功耗。
webView.pauseTimers()
//恢復(fù)pauseTimers狀態(tài)
webView.resumeTimers()宾尚;
//銷毀Webview
//在關(guān)閉了Activity時(shí)丙笋,如果Webview的音樂(lè)或視頻,還在播放煌贴。就必須銷毀Webview
//但是注意:webview調(diào)用destory時(shí),webview仍綁定在Activity上
//這是由于自定義webview構(gòu)建時(shí)傳入了該Activity的context對(duì)象
//因此需要先從父容器中移除webview,然后再銷毀webview:
rootLayout.removeView(webView);
webView.destroy();
關(guān)于前進(jìn) / 后退網(wǎng)頁(yè)
//是否可以后退
Webview.canGoBack()
//后退網(wǎng)頁(yè)
Webview.goBack()
//是否可以前進(jìn)
Webview.canGoForward()
//前進(jìn)網(wǎng)頁(yè)
Webview.goForward()
//以當(dāng)前的index為起始點(diǎn)前進(jìn)或者后退到歷史記錄中指定的steps
//如果steps為負(fù)數(shù)則為后退御板,正數(shù)則為前進(jìn)
Webview.goBackOrForward(intsteps)
常見(jiàn)用法:Back鍵控制網(wǎng)頁(yè)后退
- 問(wèn)題:在不做任何處理的情況下 ,瀏覽網(wǎng)頁(yè)時(shí)點(diǎn)擊系統(tǒng)的“Back”鍵,整個(gè) Browser 會(huì)調(diào)用 finish()而結(jié)束自身
- 目標(biāo):點(diǎn)擊“Back”鍵后牛郑,是網(wǎng)頁(yè)回退而不是退出瀏覽器
- 解決方案:在當(dāng)前Activity中處理并消費(fèi)掉該 “Back”事件
public boolean onKeyDown(int keyCode, KeyEvent event) {
if ((keyCode == KEYCODE_BACK) && myWebView .canGoBack()) {
myWebView .goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
清除緩存數(shù)據(jù)
//清除網(wǎng)頁(yè)訪問(wèn)留下的緩存
//由于內(nèi)核緩存是全局的因此這個(gè)方法不僅僅針對(duì)webview而是針對(duì)整個(gè)應(yīng)用程序.
Webview.clearCache(true);
//清空當(dāng)前頁(yè)面之前的所有記錄怠肋,也就是說(shuō)當(dāng)前的頁(yè)面記錄并不
//會(huì)被刪除。比如從頁(yè)面A打開(kāi)頁(yè)面B的同時(shí)調(diào)用該方法淹朋,此時(shí)當(dāng)前頁(yè)面是A,清空的是A的之前的記錄,A的自身
//記錄還在笙各。因此要在當(dāng)前頁(yè)面是B的時(shí)候調(diào)用該方法。
Webview.clearHistory()础芍;
//這個(gè)api僅僅清除自動(dòng)完成填充的表單數(shù)據(jù)杈抢,并不會(huì)清除WebView存儲(chǔ)到本地的數(shù)據(jù)
Webview.clearFormData();
常用工具類
WebSettings
設(shè)置webview屬性需要依賴WebSettings類
//聲明WebSettings子類
WebSettings webSettings = myWebView .getSettings();
//如果訪問(wèn)的頁(yè)面中要與Javascript交互仑性,則webview必須設(shè)置支持Javascript
// 若加載的 html 里有JS 在執(zhí)行動(dòng)畫(huà)等操作惶楼,會(huì)造成資源浪費(fèi)(CPU、電量)
// 在 onStop 和 onResume 里分別把 setJavaScriptEnabled() 給設(shè)置成 false 和 true 即可
webSettings.setJavaScriptEnabled(true);
//支持插件
webSettings.setPluginsEnabled(true);
//設(shè)置自適應(yīng)屏幕,兩者合用
webSettings.setUseWideViewPort(true); //將圖片調(diào)整到適合webview的大小
webSettings.setLoadWithOverviewMode(true); // 縮放至屏幕的大小
//縮放操作
webSettings.setSupportZoom(true); //支持縮放歼捐,默認(rèn)為true何陆。是下面那個(gè)的前提。
webSettings.setBuiltInZoomControls(true); //設(shè)置內(nèi)置的縮放控件窥岩。若為false甲献,則該WebView不可縮放
webSettings.setDisplayZoomControls(false); //隱藏原生的縮放控件
//其他細(xì)節(jié)操作
webSettings.setCacheMode(@CacheMode int mode); //設(shè)置WebView的緩存模式。當(dāng)我們加載頁(yè)面或從上一個(gè)頁(yè)面返回的時(shí)候颂翼,會(huì)按照設(shè)置的緩存模式去檢查并使用(或不使用)緩存晃洒。緩存模式有四種:
//LOAD_DEFAULT:默認(rèn)的緩存使用模式。在進(jìn)行頁(yè)面前進(jìn)或后退的操作時(shí)朦乏,如果緩存可用并未過(guò)期就優(yōu)先加載緩存球及,否則從網(wǎng)絡(luò)上加載數(shù)據(jù)。這樣可以減少頁(yè)面的網(wǎng)絡(luò)請(qǐng)求次數(shù)呻疹。
//LOAD_CACHE_ELSE_NETWORK:只要緩存可用就加載緩存吃引,哪怕它們已經(jīng)過(guò)期失效。如果緩存不可用就從網(wǎng)絡(luò)上加載數(shù)據(jù)刽锤。
//LOAD_NO_CACHE:不加載緩存镊尺,只從網(wǎng)絡(luò)加載數(shù)據(jù)。
//LOAD_CACHE_ONLY:不從網(wǎng)絡(luò)加載數(shù)據(jù)并思,只從緩存加載數(shù)據(jù)庐氮。
//通常我們可以根據(jù)網(wǎng)絡(luò)情況將這幾種模式結(jié)合使用,比如有網(wǎng)的時(shí)候使用LOAD_DEFAULT宋彼,離線時(shí)使用LOAD_CACHE_ONLY弄砍、LOAD_CACHE_ELSE_NETWORK,讓用戶不至于在離線時(shí)啥都看不到
webSettings.setAllowFileAccess(boolean allow):是否可訪問(wèn)本地文件输涕,默認(rèn)值 true
webSettings.setAllowFileAccessFromFileURLs(boolean flag):是否允許通過(guò)file url加載的Javascript讀取本地文件音婶,默認(rèn)值 false
webSettings.setAllowUniversalAccessFromFileURLs(boolean flag):是否允許通過(guò)file url加載的Javascript讀取全部資源(包括文件,http,https),默認(rèn)值 false
webSettings.setJavaScriptCanOpenWindowsAutomatically(boolean allow); //是否可用Javascript(window.open)打開(kāi)新窗口莱坎,默認(rèn)值 false
webSettings.setLoadsImagesAutomatically(boolean allow); //是否自動(dòng)加載圖片
webSettings.setBlockNetworkImage(boolean flag):禁止加載網(wǎng)絡(luò)圖片
webSettings.setDefaultTextEncodingName("utf-8");//設(shè)置編碼格式
webSettings.setDefaultFontSize(int size):設(shè)置默認(rèn)字體大小
webSettings.setDefaultFixedFontSize(int size):默認(rèn)等寬字體尺寸
webSettings.setMinimumFontSize(int size):最小文字尺寸衣式,默認(rèn)值 8
webSettings.setMinimumLogicalFontSize(int size):最小文字邏輯尺寸,默認(rèn)值 8
webSettings.setTextZoom(int textZoom):文字縮放百分比型奥,默認(rèn)值 100
webSettings.setStandardFontFamily(String font):標(biāo)準(zhǔn)字體瞳收,默認(rèn)值 "sans-serif"
webSettings.setSerifFontFamily(String font):襯線字體,默認(rèn)值 "serif"
webSettings.setSansSerifFontFamily(String font):無(wú)襯線字體厢汹,默認(rèn)值 "sans-serif"
webSettings.setFixedFontFamily(String font):等寬字體,默認(rèn)值 "monospace"
webSettings.setCursiveFontFamily(String font):手寫(xiě)體谐宙,默認(rèn)值 "cursive"
webSettings.setFantasyFontFamily(String font):幻想體烫葬,默認(rèn)值 "fantasy"
webSettings.setMediaPlaybackRequiresUserGesture(boolean require):用戶是否需要通過(guò)手勢(shì)播放媒體(不會(huì)自動(dòng)播放),默認(rèn)值 true
webSettings.setBlockNetworkLoads(boolean flag):禁止加載所有網(wǎng)絡(luò)資源
webSettings.setDomStorageEnabled(boolean allow):?jiǎn)⒂肏TML5 DOM storage API插爹,默認(rèn)值 false
webSettings.setDatabaseEnabled(boolean allow):?jiǎn)⒂肳eb SQL Database API特姐,這個(gè)設(shè)置會(huì)影響同一進(jìn)程內(nèi)的所有WebView斤寇,默認(rèn)值 false澎灸,此API已不推薦使用
webSettings.setAppCachePath(String appCachePath):設(shè)值緩存路徑
webSettings.setAppCacheEnabled(boolean flag):?jiǎn)⒂肁pplication Caches API淋肾,必需設(shè)置有效的緩存路徑才能生效骡显,默認(rèn)值 false合搅,此API已廢棄
webSettings.setGeolocationEnabled(boolean flag):?jiǎn)⒂枚ㄎ?webSettings.setSaveFormData(boolean save):是否保存表單數(shù)據(jù)
webSettings.setNeedInitialFocus(boolean flag):是否當(dāng)webview調(diào)用requestFocus時(shí)為頁(yè)面的某個(gè)元素設(shè)置焦點(diǎn)殊霞,默認(rèn)值 true
webSettings.setUseWideViewPort(boolean use):是否支持viewport屬性蒋歌,默認(rèn)值 false帅掘, 頁(yè)面通過(guò)<meta name="viewport" ... />自適應(yīng)手機(jī)屏幕
webSettings.setLoadWithOverviewMode(boolean overview):是否使用overview mode加載頁(yè)面,默認(rèn)值 false堂油,當(dāng)頁(yè)面寬度大于WebView寬度時(shí)修档,縮小使頁(yè)面寬度等于WebView寬度
webSettings.setLayoutAlgorithm(LayoutAlgorithm l):布局算法,默認(rèn)是LayoutAlgorithm#NARROW_COLUMNS
webSettings.setSupportMultipleWindows(boolean support):是否支持多窗口府框,默認(rèn)值false
webSettings.setAllowContentAccess(boolean allow):/是否可訪問(wèn)Content Provider的資源吱窝,默認(rèn)值 true
webSettings.setOffscreenPreRaster(boolean enabled):是否在離開(kāi)屏幕時(shí)光柵化(會(huì)增加內(nèi)存消耗),默認(rèn)值 false
常見(jiàn)用法:設(shè)置WebView緩存實(shí)現(xiàn)離線加載
if (NetStatusUtil.isConnected(getApplicationContext())) {
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);//根據(jù)cache-control決定是否從網(wǎng)絡(luò)上取數(shù)據(jù)迫靖。
} else {
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);//沒(méi)網(wǎng)院峡,則從本地獲取,即離線加載
}
webSettings.setDomStorageEnabled(true); // 開(kāi)啟 DOM storage API 功能
webSettings.setDatabaseEnabled(true); //開(kāi)啟 database storage API 功能
webSettings.setAppCacheEnabled(true);//開(kāi)啟 Application Caches 功能
String cacheDirPath = getFilesDir().getAbsolutePath() + APP_CACAHE_DIRNAME;
webSettings.setAppCachePath(cacheDirPath); //設(shè)置 Application Caches 緩存目錄
注意: 每個(gè) Application 只調(diào)用一次 WebSettings.setAppCachePath()系宜,WebSettings.setAppCacheMaxSize()
WebViewClient類
這個(gè)類就像WebView的委托人一樣照激,是幫助WebView處理各種通知和請(qǐng)求事件的
常見(jiàn)方法1:shouldOverrideUrlLoading()
打開(kāi)網(wǎng)頁(yè)時(shí)不調(diào)用系統(tǒng)瀏覽器, 而是在本W(wǎng)ebView中顯示蜈首;在網(wǎng)頁(yè)上的所有加載都經(jīng)過(guò)這個(gè)方法,這個(gè)函數(shù)我們可以做很多操作实抡。返回true表示宿主app攔截并處理了該url,否則返回false由當(dāng)前WebView處理,不處理POST請(qǐng)求
myWebView .setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
});
此方法在API24被廢棄欢策,API24添加如下方法:
- public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request)
攔截頁(yè)面加載吆寨,返回true表示宿主app攔截并處理了該url,否則返回false由當(dāng)前WebView處理
不處理POST請(qǐng)求踩寇,可攔截處理子frame的非http請(qǐng)求
常見(jiàn)方法2:onPageStarted()
開(kāi)始載入頁(yè)面時(shí)調(diào)用
我們可以設(shè)定一個(gè)loading的頁(yè)面啄清,告訴用戶程序在等待網(wǎng)絡(luò)響應(yīng)。
myWebView.setWebViewClient(new WebViewClient(){
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
//設(shè)定加載開(kāi)始的操作
}
});
常見(jiàn)方法3:onPageFinished()
在頁(yè)面加載結(jié)束時(shí)調(diào)用
我們可以關(guān)閉loading 條俺孙,切換程序動(dòng)作辣卒。
myWebView.setWebViewClient(new WebViewClient(){
@Override
public void onPageFinished(WebView view, String url) {
//設(shè)定加載結(jié)束的操作
}
});
常見(jiàn)方法4:onLoadResource()
在加載頁(yè)面資源時(shí)會(huì)調(diào)用,每一個(gè)資源(比如圖片)的加載都會(huì)調(diào)用一次睛榄。
myWebView.setWebViewClient(new WebViewClient(){
@Override
public boolean onLoadResource(WebView view, String url) {
//設(shè)定加載資源的操作
}
});
常見(jiàn)方法5:onReceivedError()
加載頁(yè)面的服務(wù)器出現(xiàn)錯(cuò)誤時(shí)(如404)調(diào)用荣茫。
App里面使用webview控件的時(shí)候遇到了諸如404這類的錯(cuò)誤的時(shí)候,若也顯示瀏覽器里面的那種錯(cuò)誤提示頁(yè)面就顯得很丑陋了场靴,那么這個(gè)時(shí)候我們的app就需要加載一個(gè)本地的錯(cuò)誤提示頁(yè)面啡莉,即webview如何加載一個(gè)本地的頁(yè)面
//步驟1:寫(xiě)一個(gè)html文件(error_handle.html)港准,用于出錯(cuò)時(shí)展示給用戶看的提示頁(yè)面
//步驟2:將該html文件放置到代碼根目錄的assets文件夾下
//步驟3:復(fù)寫(xiě)WebViewClient的onRecievedError方法
//該方法傳回了錯(cuò)誤碼,根據(jù)錯(cuò)誤類型可以進(jìn)行不同的錯(cuò)誤分類處理
myWebView.setWebViewClient(new WebViewClient() {
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
switch (errorCode) {
case HttpStatus.SC_NOT_FOUND:
view.loadUrl("file:///android_assets/error_handle.html");
break;
}
}
});
此方法廢棄于API23咧欣,并添加如下方法:
- public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)
加載資源時(shí)出錯(cuò)浅缸,通常意味著連接不到服務(wù)器。由于所有資源加載錯(cuò)誤都會(huì)調(diào)用此方法魄咕,所以此方法應(yīng)盡量邏輯簡(jiǎn)單
常見(jiàn)方法6:onReceivedSslError()
處理https請(qǐng)求,加載資源時(shí)發(fā)生了一個(gè)SSL錯(cuò)誤衩椒,應(yīng)用必需響應(yīng)(繼續(xù)請(qǐng)求或取消請(qǐng)求), 處理決策可能被緩存用于后續(xù)的請(qǐng)求哮兰,默認(rèn)行為是取消請(qǐng)求
webView默認(rèn)是不處理https請(qǐng)求的毛萌,頁(yè)面顯示空白,需要進(jìn)行如下設(shè)置:
myWebView.setWebViewClient(new WebViewClient() {
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed(); //表示等待證書(shū)響應(yīng)
// handler.cancel(); //表示掛起連接奠蹬,為默認(rèn)方式
// handler.handleMessage(null); //可做其他處理
}
});
// 特別注意:5.1以上默認(rèn)禁止了https和http混用朝聋,以下方式是開(kāi)啟
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
myWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
其他方法
- public WebResourceResponse shouldInterceptRequest(WebView view, String url)
調(diào)用于非UI線程攔截資源請(qǐng)求并返回響應(yīng)數(shù)據(jù),返回null時(shí)WebView將繼續(xù)加載資源囤躁。注意:API21以下的AJAX請(qǐng)求會(huì)走onLoadResource冀痕,無(wú)法通過(guò)此方法攔截,此方法廢棄于API21 - public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request)
此方法添加于API21狸演,調(diào)用于非UI線程言蛇,攔截資源請(qǐng)求并返回?cái)?shù)據(jù),返回null時(shí)WebView將繼續(xù)加載資源 - public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse)
此方法添加于API23宵距, 在加載資源(iframe,image,js,css,ajax...)時(shí)收到了 HTTP 錯(cuò)誤(狀態(tài)碼>=400) - public void onFormResubmission(WebView view, Message dontResend, Message resend)
是否重新提交表單腊尚,默認(rèn)不重發(fā) - public void onReceivedClientCertRequest(WebView view, ClientCertRequest request)
此方法添加于API21,在UI線程被調(diào)用满哪, 處理SSL客戶端證書(shū)請(qǐng)求婿斥,必要的話可顯示一個(gè)UI來(lái)提供KEY。
有三種響應(yīng)方式:proceed()/cancel()/ignore()哨鸭,默認(rèn)行為是取消請(qǐng)求民宿,如果調(diào)用proceed()或cancel(),Webview 將在內(nèi)存中保存響應(yīng)結(jié)果且對(duì)相同的"host:port"不會(huì)再次調(diào)用 onReceivedClientCertRequest
多數(shù)情況下像鸡,可通過(guò)KeyChain.choosePrivateKeyAlias啟動(dòng)一個(gè)Activity供用戶選擇合適的私鑰 - public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm)
處理HTTP認(rèn)證請(qǐng)求活鹰,默認(rèn)行為是取消請(qǐng)求 - public void onReceivedLoginRequest(WebView view, String realm, String account, String args)
通知應(yīng)用有個(gè)已授權(quán)賬號(hào)自動(dòng)登陸了 - public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event)
給應(yīng)用一個(gè)機(jī)會(huì)處理按鍵事件,如果返回true只估,WebView不處理該事件志群,否則WebView會(huì)一直處理,默認(rèn)返回false - public void onScaleChanged(WebView view, float oldScale, float newScale)
通知應(yīng)用頁(yè)面縮放系數(shù)變化
一些關(guān)鍵方法調(diào)用流程:
- loadUrl()無(wú)重定向時(shí)
onPageStarted()->onPageFinished()
- loadUrl()網(wǎng)頁(yè)A重定向到B時(shí)
onPageStarted()->shouldOverrideUrlLoading()->onPageStarted()->onPageFinished()->onPageFinished()
- 在已加載的頁(yè)面中點(diǎn)擊鏈接蛔钙,加載頁(yè)面A(無(wú)重定向)
shouldOverrideUrlLoading()->onPageStarted()->onPageFinished()
- 在已加載的頁(yè)面中點(diǎn)擊鏈接锌云,加載頁(yè)面A(頁(yè)面A重定向至頁(yè)面B)
shouldOverrideUrlLoading()->onPageStarted()->shouldOverrideUrlLoading()->onPageStarted()->onPageFinished()->onPageFinished()
- 執(zhí)行g(shù)oBack/goForward/reload方法
onPageStarted()->onPageFinished()
- 發(fā)生資源加載
shouldInterceptRequest->onLoadResource
WebChromeClient類
輔助 WebView 處理 Javascript 的對(duì)話框,網(wǎng)站圖標(biāo),網(wǎng)站標(biāo)題等等。
常見(jiàn)方法1: onProgressChanged()
獲得網(wǎng)頁(yè)的加載進(jìn)度并顯示
myWebView.setWebChromeClient(new WebChromeClient(){
@Override
public void onProgressChanged(WebView view, int newProgress) {
if (newProgress < 100) {
String progress = newProgress + "%";
progress.setText(progress);
} else {
}
});
常見(jiàn)方法2: onReceivedTitle()
獲取Web頁(yè)中的標(biāo)題
每個(gè)網(wǎng)頁(yè)的頁(yè)面都有一個(gè)標(biāo)題吁脱,比如www.baidu.com這個(gè)頁(yè)面的標(biāo)題即“百度一下宾抓,你就知道”子漩,那么如何知道當(dāng)前webview正在加載的頁(yè)面的title并進(jìn)行設(shè)置呢豫喧?
myWebView.setWebChromeClient(new WebChromeClient(){
@Override
public void onReceivedTitle(WebView view, String title) {
titleview.setText(title)石洗;
}
常見(jiàn)方法3: onJsAlert()
支持javascript的警告框
myWebView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("JsAlert")
.setMessage(message)
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
})
.setCancelable(false)
.show();
return true;
}
});
常見(jiàn)方法4: onJsConfirm()
支持javascript的確認(rèn)框
myWebView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("JsConfirm")
.setMessage(message)
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
})
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.cancel();
}
})
.setCancelable(false)
.show();
// 返回布爾值:判斷點(diǎn)擊時(shí)確認(rèn)還是取消
// true表示點(diǎn)擊了確認(rèn);false表示點(diǎn)擊了取消紧显;
return true;
}
});
常見(jiàn)方法5: onJsPrompt()
支持javascript輸入框
點(diǎn)擊確認(rèn)返回輸入框中的值讲衫,點(diǎn)擊取消返回 null。
myWebView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
final EditText et = new EditText(MainActivity.this);
et.setText(defaultValue);
new AlertDialog.Builder(MainActivity.this)
.setTitle(message)
.setView(et)
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.confirm(et.getText().toString());
}
})
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.cancel();
}
})
.setCancelable(false)
.show();
return true;
}
});
其他方法
- public void getVisitedHistory(ValueCallback<String[]> callback)
獲得所有訪問(wèn)歷史項(xiàng)目的列表孵班,用于鏈接著色涉兽。 - public Bitmap getDefaultVideoPoster()
<video /> 控件在未播放時(shí),會(huì)展示為一張海報(bào)圖篙程,HTML中可通過(guò)它的'poster'屬性來(lái)指定枷畏。
如果未指定'poster'屬性,則通過(guò)此方法提供一個(gè)默認(rèn)的海報(bào)圖虱饿。 - public View getVideoLoadingProgressView()
當(dāng)全屏的視頻正在緩沖時(shí)拥诡,此方法返回一個(gè)占位視圖(比如旋轉(zhuǎn)的菊花)。 - public void onShowCustomView(View view, CustomViewCallback callback)
通知應(yīng)用當(dāng)前頁(yè)進(jìn)入了全屏模式氮发,此時(shí)應(yīng)用必須顯示一個(gè)包含網(wǎng)頁(yè)內(nèi)容的自定義View - public void onHideCustomView()
通知應(yīng)用當(dāng)前頁(yè)退出了全屏模式渴肉,此時(shí)應(yīng)用必須隱藏之前顯示的自定義View
-public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result)
顯示一個(gè)對(duì)話框讓用戶選擇是否離開(kāi)當(dāng)前頁(yè)面 - public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback)
指定源網(wǎng)頁(yè)內(nèi)容在沒(méi)有設(shè)置權(quán)限狀態(tài)下嘗試使用地理位置API。
從API24開(kāi)始爽冕,此方法只為安全的源(https)調(diào)用仇祭,非安全的源會(huì)被自動(dòng)拒絕 - public void onGeolocationPermissionsHidePrompt()
當(dāng)前一個(gè)調(diào)用 onGeolocationPermissionsShowPrompt() 取消時(shí),隱藏相關(guān)的UI颈畸。 - public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg)
通知應(yīng)用打開(kāi)新窗口 - public void onCloseWindow(WebView window)
通知應(yīng)用關(guān)閉窗口 - public void onRequestFocus(WebView view)
請(qǐng)求獲取取焦點(diǎn) - public void onPermissionRequest(PermissionRequest request)
通知應(yīng)用網(wǎng)頁(yè)內(nèi)容申請(qǐng)?jiān)L問(wèn)指定資源的權(quán)限(該權(quán)限未被授權(quán)或拒絕) - public void onPermissionRequestCanceled(PermissionRequest request)
通知應(yīng)用權(quán)限的申請(qǐng)被取消乌奇,隱藏相關(guān)的UI。 - public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams)
為'<input type="file" />'顯示文件選擇器眯娱,返回false使用默認(rèn)處理 - public boolean onConsoleMessage(ConsoleMessage consoleMessage)
接收J(rèn)avaScript控制臺(tái)消息
另:頁(yè)面加載方法調(diào)用流程
shouldOverrideUrlLoading()
onProgressChanged[10]
shouldInterceptRequest()
onProgressChanged[...]
onPageStarted()
onProgressChanged[...]
onLoadResource()
onProgressChanged[...]
onReceivedTitle()/onPageCommitVisible()
onProgressChanged[100]
onPageFinished()
onReceivedIcon()
總結(jié)
如何避免WebView內(nèi)存泄露礁苗?
- 不在xml中定義 Webview ,而是在需要的時(shí)候在Activity中創(chuàng)建困乒,并且Context使用 getApplicationgContext()
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mWebView = new WebView(getApplicationContext());
mWebView.setLayoutParams(params);
mLayout.addView(mWebView);
- 在 Activity 銷毀( WebView )的時(shí)候寂屏,先讓 WebView 加載null內(nèi)容,然后移除 WebView娜搂,再銷毀 WebView迁霎,最后置空。
@Override
protected void onDestroy() {
if (mWebView != null) {
mWebView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
mWebView.clearHistory();
((ViewGroup) mWebView.getParent()).removeView(mWebView);
mWebView.destroy();
mWebView = null;
}
super.onDestroy();
}
如何避免WebView內(nèi)存泄露百宇?
實(shí)例
- 目標(biāo):實(shí)現(xiàn)顯示“www.baidu.com”考廉、獲取其標(biāo)題键耕、提示加載開(kāi)始 & 結(jié)束和獲取加載進(jìn)度
- 具體實(shí)現(xiàn):
步驟1:添加訪問(wèn)網(wǎng)絡(luò)權(quán)限
<uses-permission android:name="android.permission.INTERNET"/>
步驟2:主布局
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.example.carson_ho.webview_demo.MainActivity">
<!-- 獲取網(wǎng)站的標(biāo)題-->
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""/>
<!--開(kāi)始加載提示-->
<TextView
android:id="@+id/text_beginLoading"
android:layout_below="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""/>
<!--獲取加載進(jìn)度-->
<TextView
android:layout_below="@+id/text_beginLoading"
android:id="@+id/text_Loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""/>
<!--結(jié)束加載提示-->
<TextView
android:layout_below="@+id/text_Loading"
android:id="@+id/text_endLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""/>
<!--顯示網(wǎng)頁(yè)區(qū)域-->
<WebView
android:id="@+id/webView1"
android:layout_below="@+id/text_endLoading"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_marginTop="10dp" />
</RelativeLayout>
步驟3:根據(jù)需要實(shí)現(xiàn)的功能從而使用相應(yīng)的子類及其方法(注釋很清楚了)
MainActivity.java
public class MainActivity extends AppCompatActivity {
WebView mWebview;
WebSettings mWebSettings;
TextView beginLoading, endLoading, loading, mtitle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebview = (WebView) findViewById(R.id.webView1);
beginLoading = (TextView) findViewById(R.id.text_beginLoading);
endLoading = (TextView) findViewById(R.id.text_endLoading);
loading = (TextView) findViewById(R.id.text_Loading);
mtitle = (TextView) findViewById(R.id.title);
mWebSettings = mWebview.getSettings();
mWebview.loadUrl("http://www.baidu.com/");
//設(shè)置不用系統(tǒng)瀏覽器打開(kāi),直接顯示在當(dāng)前Webview
mWebview.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
});
//設(shè)置WebChromeClient類
mWebview.setWebChromeClient(new WebChromeClient() {
//獲取網(wǎng)站標(biāo)題
@Override
public void onReceivedTitle(WebView view, String title) {
System.out.println("標(biāo)題在這里");
mtitle.setText(title);
}
//獲取加載進(jìn)度
@Override
public void onProgressChanged(WebView view, int newProgress) {
if (newProgress < 100) {
String progress = newProgress + "%";
loading.setText(progress);
} else if (newProgress == 100) {
String progress = newProgress + "%";
loading.setText(progress);
}
}
});
//設(shè)置WebViewClient類
mWebview.setWebViewClient(new WebViewClient() {
//設(shè)置加載前的函數(shù)
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
System.out.println("開(kāi)始加載了");
beginLoading.setText("開(kāi)始加載了");
}
//設(shè)置結(jié)束加載函數(shù)
@Override
public void onPageFinished(WebView view, String url) {
endLoading.setText("結(jié)束加載了");
}
});
}
//點(diǎn)擊返回上一頁(yè)面而不是退出瀏覽器
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && mWebview.canGoBack()) {
mWebview.goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
//銷毀Webview
@Override
protected void onDestroy() {
if (mWebview != null) {
mWebview.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
mWebview.clearHistory();
((ViewGroup) mWebview.getParent()).removeView(mWebview);
mWebview.destroy();
mWebview = null;
}
super.onDestroy();
}
}
CookieManager
幫助webview管理Cookie的類
常用方法:
- CookieManager getInstance():獲得一個(gè)CookieManager實(shí)例
- String getCookie(String url):根據(jù)url獲取Cookie锌半,以字符串形式返回Cookie
- void setCookie(String url, String value):為url設(shè)置Cookie
- void setCookie(String url, String value, ValueCallback<Boolean> callback):
callback的onReceiveValue方法獲取的參數(shù)如果是true也殖,代表本次設(shè)置成功缴挖,否則代表設(shè)置失敗。如果并不關(guān)心執(zhí)行結(jié)果涮坐,為callback參數(shù)傳入null即可 - void setAcceptCookie(boolean accept):設(shè)置WebView是否允許使用Cookie凄贩,這個(gè)方法針對(duì)的是當(dāng)前應(yīng)用的所有WebView。
- void setAcceptThirdPartyCookies(WebView webview, boolean accept):設(shè)置WebView是否允許設(shè)置第三方Cookie袱讹,Android 5.0(API 21)以下默認(rèn)為true疲扎,Android 5.0及以上默認(rèn)為false
- void removeSessionCookies(ValueCallback<Boolean> callback):移除所有Session Cookies(異步執(zhí)行),在執(zhí)行完移除操作后捷雕,會(huì)回調(diào)onReceiveValue方法
- void removeAllCookies(ValueCallback<Boolean> callback):移除所有Cookies(異步執(zhí)行)椒丧,在執(zhí)行完移除操作后,會(huì)回調(diào)onReceiveValue方法
- boolean hasCookies():判斷是否存在Cookies
關(guān)于Cookie
Hybrid App(混合式應(yīng)用)的開(kāi)發(fā)過(guò)程中少不了與WebView的交互救巷,在涉及到賬戶體系的產(chǎn)品中壶熏,包含了一種登錄狀態(tài)的傳遞,而Cookie作為網(wǎng)頁(yè)身份信息的載體浦译,便需要將其傳遞到Web里面棒假,從而避免二次身份認(rèn)證,這時(shí)就涉及到WebView加載網(wǎng)頁(yè)時(shí)的Cookie操作了管怠。通常我們?cè)诘卿洉r(shí)獲取到用戶的Cookie信息淆衷,然后將其保存到sdcard的WebView緩存文件當(dāng)中,然后再加載網(wǎng)頁(yè)時(shí)渤弛,WebView會(huì)自動(dòng)將當(dāng)前url的本地Cookie信息放在http請(qǐng)求的request中祝拯,傳遞給服務(wù)器。
Cookie 默認(rèn)保存位置:
data/data/package_name/app_WebView/Cookies.db
保存Cookie
我們通常使用安卓提供的CookieManager類中的setCookie(String url, String value)或void setCookie(String url, String value, ValueCallback<Boolean> callback)方法來(lái)對(duì)url的Cookie進(jìn)行設(shè)置她肯。方法作用見(jiàn)上文佳头。另外API文檔注釋:
Sets a cookie for the given URL. Any existing cookie with the same host, path and name will be replaced with the new cookie. The cookie being set will be ignored if it is expired.
可見(jiàn)具有相同host和path的cookie會(huì)自動(dòng)覆蓋掉舊的cookie信息,當(dāng)存儲(chǔ)的cookie信息過(guò)期時(shí)會(huì)被自動(dòng)忽略晴氨。
關(guān)于host(Domain)和path
- host(Domain)表示cookie所在的域康嘉,默認(rèn)為請(qǐng)求地址,如網(wǎng)址為www.test.com/test/test.aspx,那么domain默認(rèn)為www.test.com籽前。而跨域訪問(wèn)亭珍,如域A為t1.test.com,域B為t2.test.com枝哄,那么在域A生產(chǎn)一個(gè)令域A和域B都能訪問(wèn)的cookie就要將該cookie的domain設(shè)置為.test.com肄梨;如果要在域A生產(chǎn)一個(gè)令域A不能訪問(wèn)而域B能訪問(wèn)的cookie就要將該cookie的domain設(shè)置為t2.test.com。
- path表示cookie所在的目錄挠锥,asp.net默認(rèn)為/众羡,就是根目錄。在同一個(gè)服務(wù)器上有目錄如下:/test/,/test/cd/,/test/dd/蓖租,現(xiàn)設(shè)一個(gè)cookie1的path為/test/粱侣,cookie2的path為/test/cd/,那么test下的所有頁(yè)面都可以訪問(wèn)到cookie1油猫,而/test/和/test/dd/的子頁(yè)面不能訪問(wèn)cookie2。這是因?yàn)閏ookie只能讓其path路徑下的頁(yè)面訪問(wèn)。因此一般如果訪問(wèn)一級(jí)域名共螺,只需要將path設(shè)置為根目錄就可以该肴。
針對(duì)一級(jí)域名同步Cookie
- 針對(duì)項(xiàng)目使用的一級(jí)域名進(jìn)行設(shè)置 Cookie
- 選擇合適的時(shí)機(jī)進(jìn)行刷新
/**
* 同步cookie
* @param context 上下文
* @param url 一級(jí)域名
* @param cookies 需要添加的Cookie值,以鍵值對(duì)的方式:key=value
*/
public void synCookies(Context context,String url, List<HttpCookie> cookies) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(context);
}
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);// 允許接受 Cookie
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
cookieManager.removeSessionCookie();// 移除
} else {
cookieManager.removeSessionCookies(null);// 移除
}
for (int i = 0; i < cookies.size(); i++) {
HttpCookie cookie = cookies.get(i);
String value = cookie.getName() + "=" + cookie.getValue();
cookieManager.setCookie(url, value);
}
cookieManager.setCookie(url, "Domain="+url);
cookieManager.setCookie(url, "Path=/");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.getInstance().sync();
} else {
cookieManager.flush();
}
}
這里簡(jiǎn)單說(shuō)明:
- 參數(shù)中的 URL 在使用過(guò)程中基本是域名。例如 https://www.baidu.com/ 就可以使 www.baidu.com
- 注意使用 for 循環(huán) 進(jìn)行 setCookie(String url, String value) 調(diào)用藐不。使用分號(hào)手動(dòng)拼接的 value 值會(huì)導(dǎo)致 Cookie 不能完整設(shè)置或者無(wú)效
- 注意 value 的值是使用 key=value 的完整形式匀哄。文檔提示:
the cookie as a string, using the format of the 'Set-Cookie' HTTP response header
- CookieSyncManager 是個(gè)過(guò)時(shí)的類,Api21 中 WebView 可以自動(dòng)同步,使用時(shí)可以添加版本判斷
- CookieSyncManager.getInstance().sync(); 方法的替代方法是 cookieManager.flush()
- Cookie 同步方法要在 WebView 的 setting 設(shè)置完之后,WebView的loadUrl(url)之前調(diào)用雏蛮,否則無(wú)效涎嚼。
Cookie的過(guò)期機(jī)制
可以設(shè)置Cookie的生效時(shí)間字段名為: expires 或 max-age。
expires:過(guò)期的時(shí)間點(diǎn)
max-age:生效的持續(xù)時(shí)間挑秉,單位為秒法梯。
- 若將Cookie的 max-age 設(shè)置為負(fù)數(shù),或者 expires 字段設(shè)置為過(guò)期時(shí)間點(diǎn)产喉,數(shù)據(jù)庫(kù)更新后這條Cookie將從數(shù)據(jù)庫(kù)中被刪除疤苹。
- 如果將Cookie的 max-age 和 expires 字段設(shè)置為正常的過(guò)期日期,則到期后再數(shù)據(jù)庫(kù)更新時(shí)會(huì)刪除該條數(shù)據(jù)。
特殊場(chǎng)景介紹
1友酱、視口(viewport)
視口是一個(gè)為網(wǎng)頁(yè)提供繪圖區(qū)域的矩形系羞。
你可以指定數(shù)個(gè)視口屬性澎迎,比如尺寸和初始縮放系數(shù)(initial scale)罩引。其中最重要的是視口寬度,它定義了網(wǎng)頁(yè)水平方向的可用像素總數(shù)(可用的CSS像素?cái)?shù))。
多數(shù) Android 上的網(wǎng)頁(yè)瀏覽器(包括 Chrome)設(shè)置默認(rèn)視口為一個(gè)大尺寸(被稱為"wide viewport mode",寬約 980px)。
也有許多瀏覽器默認(rèn)會(huì)盡可能縮小以顯示完整的視口寬度(被稱為"overview mode")
// 是否支持viewport屬性,默認(rèn)值 false
// 頁(yè)面通過(guò)`<meta name="viewport" ... />`自適應(yīng)手機(jī)屏幕
// 當(dāng)值為true且viewport標(biāo)簽不存在或未指定寬度時(shí)使用 wide viewport mode
settings.setUseWideViewPort(true);
// 是否使用overview mode加載頁(yè)面胸哥,默認(rèn)值 false
// 當(dāng)頁(yè)面寬度大于WebView寬度時(shí),縮小使頁(yè)面寬度等于WebView寬度
settings.setLoadWithOverviewMode(true);
viewport 語(yǔ)法
<meta name="viewport"
content="
height = [pixel_value | "device-height"] ,
width = [pixel_value | "device-width"] ,
initial-scale = float_value ,
minimum-scale = float_value ,
maximum-scale = float_value ,
user-scalable = ["yes" | "no"]
" />
通過(guò)WebView設(shè)置初始縮放(initial-scale)
// 設(shè)置初始縮放百分比
// 0表示依賴于setUseWideViewPort和setLoadWithOverviewMode
// 100表示不縮放
web.setInitialScale(0)
2、長(zhǎng)按保存圖片或者撥打電話
一般瀏覽器都有長(zhǎng)按保存圖片或者撥打圖片的功能嗤攻,實(shí)現(xiàn)這個(gè)功能和WebView.HitTestResult這個(gè)類有關(guān)辛臊,這個(gè)類會(huì)將你觸摸網(wǎng)頁(yè)的地方的類型和其他信息反饋給你隔心。
WebView.HitTestResult的常用方法:
- HitTestResult.getExtra():獲取額外的信息须尚。
- HitTestResult.getType():獲取所選中目標(biāo)的類型撩轰,可以是如下類型。
WebView.HitTestResult.UNKNOWN_TYPE //未知類型
WebView.HitTestResult.PHONE_TYPE //電話類型
WebView.HitTestResult.EMAIL_TYPE //電子郵件類型
WebView.HitTestResult.GEO_TYPE //地圖類型
WebView.HitTestResult.SRC_ANCHOR_TYPE //超鏈接類型
WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE //帶有鏈接的圖片類型
WebView.HitTestResult.IMAGE_TYPE //單純的圖片類型
WebView.HitTestResult.EDIT_TEXT_TYPE //選中的文字類型
實(shí)現(xiàn)步驟:
- 給WebView設(shè)置長(zhǎng)按監(jiān)聽(tīng)事件;
- 獲取WebView長(zhǎng)按時(shí)的WebView.HitTestResult的事件類型,如果是圖片伶授,則做處理诸迟。
webView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
WebView.HitTestResult result = ((WebView) view).getHitTestResult();
if(result != null){
switch (result.getType()){
case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
String imgUrl = result.getExtra();
...
return true;
...
}
}
return false;
}
});
3紊册、全屏(Fullscreen)
- 當(dāng)H5請(qǐng)求全屏?xí)r撞反,會(huì)回調(diào) WebChromeClient.onShowCustomView 方法
- 當(dāng)H5退出全屏?xí)r,會(huì)回調(diào) WebChromeClient.onHideCustomView 方法
- manifest
自己處理屏幕尺寸方向的變化(切換屏幕方向時(shí)不重建activity)
WebView播放視頻需要開(kāi)啟硬件加速
<activity
android:name=".WebViewActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true"
android:screenOrientation="portrait" />
- 處理全屏回調(diào)
CustomViewCallback mCallback;
View vCustom;
@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
setFullscreen(true);
vCustom = view;
mCallback = callback;
if (vCustom != null) {
ViewGroup parent = (ViewGroup) vWeb.getParent();
parent.addView(vCustom);
}
}
@Override
public void onHideCustomView() {
setFullscreen(false);
if (vCustom != null) {
ViewGroup parent = (ViewGroup) vWeb.getParent();
parent.removeView(vCustom);
vCustom = null;
}
if (mCallback != null) {
mCallback.onCustomViewHidden();
mCallback = null;
}
}
- 設(shè)置全屏惑折,切換屏幕方向
void setFullscreen(boolean fullscreen) {
if (fullscreen) {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
vToolbar.setVisibility(View.GONE);
vWeb.setVisibility(View.GONE);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
vToolbar.setVisibility(View.VISIBLE);
vWeb.setVisibility(View.VISIBLE);
}
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
WebView與Javascript交互
JavaScript 和 Android 可以交互,我們可以在網(wǎng)頁(yè)中隨意調(diào)用本地的 Java 代碼,實(shí)現(xiàn)了 WebView 和本地代碼的交互。所以 WebView 的功能非常強(qiáng)大,我們直接在一個(gè) WebView 中就幾乎可以實(shí)現(xiàn) Android 的所有功能,所以現(xiàn)在才會(huì)出現(xiàn)不少純 HTML5 開(kāi)發(fā)的 App,這往往適合創(chuàng)業(yè)型公司剛起步階段。
Android與JS通過(guò)WebView互相調(diào)用方法猫牡,實(shí)際上是:
- Android去調(diào)用JS的代碼
- JS去調(diào)用Android的代碼
二者溝通的橋梁是WebView
對(duì)于Android調(diào)用JS代碼的方法有2種:
- 通過(guò)WebView的loadUrl()
- 通過(guò)WebView的evaluateJavascript()
方式1:通過(guò)WebView的loadUrl()
同步調(diào)用瑰抵,會(huì)導(dǎo)致JS頁(yè)面刷新肴颊。
- 實(shí)例介紹:點(diǎn)擊Android按鈕竟宋,即調(diào)用WebView JS(文本名為javascript)中callJS()
- 具體實(shí)現(xiàn):
步驟1:將需要調(diào)用的JS代碼以.html格式放到src/main/assets文件夾里
為了方便展示婉陷,本文是采用Andorid調(diào)用本地JS代碼說(shuō)明担神;實(shí)際情況時(shí)躬窜,Android更多的是調(diào)用遠(yuǎn)程JS代碼默垄,即將加載的JS代碼路徑改成url即可
需要加載JS代碼:javascript.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>webview</title>
<script>
<!--Android需要調(diào)用的方法-->
function callJS(){
alert("Android調(diào)用了JS的callJS()方法");
}
</script>
</head>
</html>
步驟2:在Android里通過(guò)WebView設(shè)置調(diào)用JS代碼
Android代碼:MainActivity.java
注釋已經(jīng)非常清楚了
public class MainActivity extends AppCompatActivity {
Button mButton;
WebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = findViewById(R.id.btn);
mWebView = findViewById(R.id.webview);
WebSettings webSettings = mWebView.getSettings();
// 設(shè)置與Js交互的權(quán)限
webSettings.setJavaScriptEnabled(true);
// 設(shè)置允許JS彈窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
// 由于設(shè)置了彈窗檢驗(yàn)調(diào)用結(jié)果,所以需要支持js對(duì)話框
// webview只是載體鹃操,內(nèi)容的渲染需要使用webviewChromClient類去實(shí)現(xiàn)
// 通過(guò)設(shè)置WebChromeClient對(duì)象處理JavaScript的對(duì)話框
//設(shè)置響應(yīng)js 的Alert()函數(shù)
mWebView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
AlertDialog.Builder b = new AlertDialog.Builder(MainActivity.this);
b.setTitle("Alert");
b.setMessage(message);
b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
});
b.setCancelable(false);
b.create().show();
return true;
}
});
// 先載入JS代碼
// 格式規(guī)定為:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 通過(guò)Handler發(fā)送消息
mWebView.post(new Runnable() {
@Override
public void run() {
// 注意調(diào)用的JS方法名要對(duì)應(yīng)上
// 調(diào)用javascript的callJS()方法
mWebView.loadUrl("javascript:callJS()");
}
});
}
});
}
}
特別注意:JS代碼調(diào)用一定要在 onPageFinished() 回調(diào)之后才能調(diào)用,否則不會(huì)調(diào)用。
onPageFinished()屬于WebViewClient類的方法,主要在頁(yè)面加載結(jié)束時(shí)調(diào)用
方式2:通過(guò)WebView的evaluateJavascript()
異步執(zhí)行調(diào)用JS往核,并通過(guò)回調(diào)返回值窜护,不會(huì)使JS頁(yè)面刷新
優(yōu)點(diǎn):該方法比第一種方法效率更高、使用更簡(jiǎn)潔概行。
限制:API>=19
- 具體實(shí)現(xiàn):
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此處為 js 返回的結(jié)果
}
});
}
方式對(duì)比
使用建議
兩種方法混合使用腹尖,即Android 4.4以下使用方法1,Android 4.4以上方法2
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此處為 js 返回的結(jié)果
}
});
}else {
mWebView.loadUrl("javascript:callJS()");
}
JS通過(guò)WebView調(diào)用 Android 代碼的方法有3種:
- 通過(guò)WebView的addJavascriptInterface()進(jìn)行對(duì)象映射
- 通過(guò) WebViewClient 的shouldOverrideUrlLoading ()方法回調(diào)攔截 url
- 通過(guò) WebChromeClient 的onJsAlert()、onJsConfirm()如庭、onJsPrompt()方法回調(diào)攔截JS對(duì)話框alert()叹卷、confirm()、prompt() 消息
方式1:通過(guò) WebView的addJavascriptInterface()進(jìn)行對(duì)象映射
步驟1:定義一個(gè)與JS對(duì)象映射關(guān)系的Android類
AndroidtoJs.java
注釋已經(jīng)非常清楚了
public class AndroidtoJs {
// 定義JS需要調(diào)用的方法
// 被JS調(diào)用的方法必須加入@JavascriptInterface注解
@JavascriptInterface
public void hello(String msg) {
System.out.println("JS調(diào)用了Android的hello方法");
}
}
步驟2:將需要調(diào)用的JS代碼以.html格式放到src/main/assets文件夾里
需要加載JS代碼:javascript.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>webview</title>
<script>
function callAndroid(){
<!-- 由于對(duì)象映射坪它,所以調(diào)用test對(duì)象等于調(diào)用Android映射的對(duì)象-->
test.hello("js調(diào)用了android中的hello方法");
}
</script>
</head>
<body>
<!--點(diǎn)擊按鈕則調(diào)用callAndroid函數(shù)-->
<button type="button" id="button1" onclick="callAndroid()"></button>
</body>
</html>
步驟3:在Android里通過(guò)WebView設(shè)置Android類與JS代碼的映射
Android代碼:MainActivity.java
注釋已經(jīng)非常清楚了
public class MainActivity extends AppCompatActivity {
WebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebView = findViewById(R.id.webview);
WebSettings webSettings = mWebView.getSettings();
// 設(shè)置與Js交互的權(quán)限
webSettings.setJavaScriptEnabled(true);
// 通過(guò)addJavascriptInterface()將Java對(duì)象映射到JS對(duì)象
//參數(shù)1:Javascript對(duì)象名
//參數(shù)2:Java對(duì)象名
mWebView.addJavascriptInterface(new AndroidtoJs(), "test");//AndroidtoJS類對(duì)象映射到j(luò)s的test對(duì)象
// 加載JS代碼
// 格式規(guī)定為:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");
}
}
特點(diǎn)
- 優(yōu)點(diǎn):使用簡(jiǎn)單
- 缺點(diǎn):存在嚴(yán)重的漏洞問(wèn)題,后文會(huì)說(shuō)明嗤详。
方式2:通過(guò) WebViewClient 的方法shouldOverrideUrlLoading ()回調(diào)攔截 url
- 具體原理:
- Android通過(guò) WebViewClient 的回調(diào)方法shouldOverrideUrlLoading ()攔截 url
- 解析該 url 的協(xié)議
- 如果檢測(cè)到是預(yù)先約定好的協(xié)議,就調(diào)用相應(yīng)方法
- 具體實(shí)現(xiàn):
步驟1:在JS約定所需要的Url協(xié)議
JS代碼:javascript.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Carson_Ho</title>
<script>
function callAndroid(){
<!--約定的url協(xié)議為:js://webview?arg1=111&arg2=222-->
document.location = "js://webview?arg1=111&arg2=222";
}
</script>
</head>
<!-- 點(diǎn)擊按鈕則調(diào)用callAndroid()方法 -->
<body>
<button type="button" id="button1" onclick="callAndroid()">點(diǎn)擊調(diào)用Android代碼</button>
</body>
</html>
當(dāng)該JS通過(guò)Android的mWebView.loadUrl("file:///android_asset/javascript.html")加載后,就會(huì)回調(diào)shouldOverrideUrlLoading(),接下來(lái)繼續(xù)看步驟2:
步驟2:在Android通過(guò)WebViewClient復(fù)寫(xiě)shouldOverrideUrlLoading ()
MainActivity.java
public class MainActivity extends AppCompatActivity {
WebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebView = findViewById(R.id.webview);
WebSettings webSettings = mWebView.getSettings();
// 設(shè)置與Js交互的權(quán)限
webSettings.setJavaScriptEnabled(true);
// 設(shè)置允許JS彈窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
// 步驟1:加載JS代碼
// 格式規(guī)定為:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");
// 復(fù)寫(xiě)WebViewClient類的shouldOverrideUrlLoading方法
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 步驟2:根據(jù)協(xié)議的參數(shù)亿汞,判斷是否是所需要的url
// 一般根據(jù)scheme(協(xié)議格式) & authority(協(xié)議名)判斷(前兩個(gè)參數(shù))
//假定傳入進(jìn)來(lái)的 url = "js://webview?arg1=111&arg2=222"(同時(shí)也是約定好的需要攔截的)
Uri uri = Uri.parse(url);
// 如果url的協(xié)議 = 預(yù)先約定的 js 協(xié)議
// 就解析往下解析參數(shù)
if (uri.getScheme().equals("js")) {
// 如果 authority = 預(yù)先約定協(xié)議里的 webview琼开,即代表都符合約定的協(xié)議
// 所以攔截url,下面JS開(kāi)始調(diào)用Android需要的方法
if (uri.getAuthority().equals("webview")) {
// 步驟3:
// 執(zhí)行JS所需要調(diào)用的邏輯
System.out.println("js調(diào)用了Android的方法");
// 可以在協(xié)議上帶有參數(shù)并傳遞到Android上
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
for (String s : collection) {
params.put(s, uri.getQueryParameter(s));
}
System.out.println(uri.getQuery());
System.out.println(params.toString());
}
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
}
);
}
}
特點(diǎn)
- 優(yōu)點(diǎn):不存在方式1的漏洞峦耘;
- 缺點(diǎn):JS獲取Android方法的返回值復(fù)雜滤淳。
如果JS想要得到Android方法的返回值,只能通過(guò) WebView 的 loadUrl ()去執(zhí)行 JS 方法把返回值傳遞回去,相關(guān)的代碼如下:
// Android:MainActivity.java
mWebView.loadUrl("javascript:returnResult(" + result + ")");
// JS:javascript.html
function returnResult(result){
alert("result is" + result);
}
方式3:通過(guò) WebChromeClient 的onJsAlert()凉唐、onJsConfirm()勺拣、onJsPrompt()方法回調(diào)攔截JS對(duì)話框alert()、confirm()胆数、prompt() 消息
在JS中焕参,有三個(gè)常用的對(duì)話框方法:
- 具體原理:Android通過(guò) WebChromeClient 的onJsAlert()互妓、onJsConfirm()或衡、onJsPrompt()方法回調(diào)分別攔截JS對(duì)話框(即上述三個(gè)方法)焦影,得到他們的消息內(nèi)容,然后解析即可封断。
- 具體實(shí)現(xiàn):
下面的例子將用攔截 JS的輸入框(即prompt()方法)說(shuō)明 :
常用的攔截是:攔截 JS的輸入框(即prompt()方法),因?yàn)橹挥衟rompt()可以返回任意類型的值斯辰,操作最全面方便、更加靈活坡疼;而alert()對(duì)話框沒(méi)有返回值彬呻;confirm()對(duì)話框只能返回兩種狀態(tài)(確定 / 取消)兩個(gè)值
步驟1:加載JS代碼,如下:
javascript.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>webview</title>
<script>
function clickprompt(){
<!-- 調(diào)用prompt()-->
var result=prompt("js://webview?arg1=111&arg2=222");
alert("demo " + result);
}
</script>
</head>
<!-- 點(diǎn)擊按鈕則調(diào)用clickprompt()-->
<body>
<button type="button" id="button1" onclick="clickprompt()">點(diǎn)擊調(diào)用Android代碼</button>
</body>
</html>
當(dāng)使用mWebView.loadUrl("file:///android_asset/javascript.html")加載了上述JS代碼后柄瑰,就會(huì)觸發(fā)回調(diào)onJsPrompt()闸氮。
如果是攔截警告框(即alert()),則觸發(fā)回調(diào)onJsAlert()教沾;
如果是攔截確認(rèn)框(即confirm())蒲跨,則觸發(fā)回調(diào)onJsConfirm();
步驟2:在Android通過(guò)WebChromeClient復(fù)寫(xiě)onJsPrompt()
public class MainActivity extends AppCompatActivity {
WebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebView = findViewById(R.id.webview);
WebSettings webSettings = mWebView.getSettings();
// 設(shè)置與Js交互的權(quán)限
webSettings.setJavaScriptEnabled(true);
// 設(shè)置允許JS彈窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
// 先加載JS代碼
// 格式規(guī)定為:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");
mWebView.setWebChromeClient(new WebChromeClient() {
// 攔截輸入框(原理同方式2)
// 參數(shù)message:代表promt()的內(nèi)容(不是url)
// 參數(shù)result:代表輸入框的返回值
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
// 根據(jù)協(xié)議的參數(shù)授翻,判斷是否是所需要的url(原理同方式2)
// 一般根據(jù)scheme(協(xié)議格式) & authority(協(xié)議名)判斷(前兩個(gè)參數(shù))
//假定傳入進(jìn)來(lái)的 url = "js://webview?arg1=111&arg2=222"(同時(shí)也是約定好的需要攔截的)
Uri uri = Uri.parse(message);
// 如果url的協(xié)議 = 預(yù)先約定的 js 協(xié)議
// 就解析往下解析參數(shù)
if (uri.getScheme().equals("js")) {
// 如果 authority = 預(yù)先約定協(xié)議里的 webview或悲,即代表都符合約定的協(xié)議
// 所以攔截url,下面JS開(kāi)始調(diào)用Android需要的方法
if (uri.getAuthority().equals("webview")) {
// 執(zhí)行JS所需要調(diào)用的邏輯
System.out.println("js調(diào)用了Android的方法");
// 可以在協(xié)議上帶有參數(shù)并傳遞到Android上
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
for (String s : collection) {
params.put(s, uri.getQueryParameter(s));
}
System.out.println(uri.getQuery());
System.out.println(params.toString());
//參數(shù)result:代表消息框的返回值(輸入值)
result.confirm("js調(diào)用了Android的方法成功啦");
}
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
// 通過(guò)alert()和confirm()攔截的原理相同孙咪,此處不作過(guò)多講述
// 攔截JS的警告框
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
// 攔截JS的確認(rèn)框
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
}
);
}
}
三種方式的對(duì)比 & 使用場(chǎng)景
總結(jié)
WebView的使用漏洞
1. 類型
WebView中,主要漏洞有三類:
- 任意代碼執(zhí)行漏洞
- 密碼明文存儲(chǔ)漏洞
- 域控制不嚴(yán)格漏洞
2. 具體分析
第一類:WebView 任意代碼執(zhí)行漏洞
出現(xiàn)該漏洞的原因有三個(gè):
- WebView 中 addJavascriptInterface() 接口
- WebView 內(nèi)置導(dǎo)出的 searchBoxJavaBridge_對(duì)象
- WebView 內(nèi)置導(dǎo)出的 accessibility 和 accessibilityTraversalObject 對(duì)象
原因一:addJavascriptInterface 接口引起遠(yuǎn)程代碼執(zhí)行漏洞
漏洞產(chǎn)生原因:
JS調(diào)用Android的其中一個(gè)方式是通過(guò)addJavascriptInterface接口進(jìn)行對(duì)象映射:
// 參數(shù)1:Android的本地對(duì)象
// 參數(shù)2:JS的對(duì)象
// 通過(guò)對(duì)象映射將Android中的本地對(duì)象和JS中的對(duì)象進(jìn)行關(guān)聯(lián)巡语,從而實(shí)現(xiàn)JS調(diào)用Android的對(duì)象和方法
webView.addJavascriptInterface(new JSObject(), "myObj");
所以翎蹈,漏洞產(chǎn)生原因是:當(dāng)JS拿到Android這個(gè)對(duì)象后,就可以調(diào)用這個(gè)Android對(duì)象中所有的方法捌臊,包括系統(tǒng)類(java.lang.Runtime 類)杨蛋,從而進(jìn)行任意代碼執(zhí)行。如可以執(zhí)行命令獲取本地設(shè)備的SD卡中的文件等信息從而造成信息泄露理澎。
具體獲取系統(tǒng)類的原理描述:(結(jié)合 Java 反射機(jī)制)
- Android中的對(duì)象有一公共的方法:getClass()
- 該方法可以獲取到當(dāng)前類的類型Class
- 該類有一關(guān)鍵的方法: Class.forName,
- 該方法可以加載一個(gè)類(可加載 java.lang.Runtime 類),而該類是可以執(zhí)行本地命令的逞力。
以下是攻擊的Js核心代碼:
function execute(cmdArgs)
{
// 步驟1:遍歷 window 對(duì)象
// 目的是為了找到包含 getClass()的對(duì)象
// 因?yàn)锳ndroid映射的JS對(duì)象也在window中,所以肯定會(huì)遍歷到
for (var obj in window) {
if ("getClass" in window[obj]) {
// 步驟2:利用反射調(diào)用forName()得到Runtime類對(duì)象
alert(obj);
return window[obj].getClass().forName("java.lang.Runtime")
// 步驟3:以后糠爬,就可以調(diào)用靜態(tài)方法來(lái)執(zhí)行一些命令寇荧,比如訪問(wèn)文件的命令
getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);
// 從執(zhí)行命令后返回的輸入流中得到字符串,有很嚴(yán)重暴露隱私的危險(xiǎn)执隧。
// 如執(zhí)行完訪問(wèn)文件的命令之后揩抡,就可以得到文件名的信息了。
}
}
}
當(dāng)一些 APP 通過(guò)掃描二維碼打開(kāi)一個(gè)外部網(wǎng)頁(yè)時(shí)镀琉,攻擊者就可以執(zhí)行這段 js 代碼進(jìn)行漏洞攻擊峦嗤。
在微信盛行、掃一掃行為普及的情況下屋摔,該漏洞的危險(xiǎn)性非常大烁设。
解決方案
- Android 4.2版本之后
Google 在Android 4.2 版本中規(guī)定對(duì)被調(diào)用的函數(shù)以 @JavascriptInterface進(jìn)行注解從而避免漏洞攻擊 - Android 4.2版本之前
在Android 4.2版本之前采用攔截prompt()進(jìn)行漏洞修復(fù)。具體步驟如下:
- 繼承 WebView 钓试,重寫(xiě) addJavascriptInterface 方法装黑,然后在內(nèi)部自己維護(hù)一個(gè)對(duì)象映射關(guān)系的 Map,將需要添加的 JS 接口放入該Map中;
- 每次當(dāng) WebView 加載頁(yè)面前加載一段本地的 JS 代碼弓熏,原理是:
讓JS調(diào)用一Javascript方法:該方法是通過(guò)調(diào)用prompt()把JS中的信息(含特定標(biāo)識(shí)恋谭,方法名稱等)傳遞到Android端;
在Android的onJsPrompt()中 挽鞠,解析傳遞過(guò)來(lái)的信息疚颊,再通過(guò)反射機(jī)制調(diào)用Java對(duì)象的方法,這樣實(shí)現(xiàn)安全的JS調(diào)用Android代碼信认。
關(guān)于Android返回給JS的值:可通過(guò)prompt()把Java中方法的處理結(jié)果返回到Js中
具體需要加載的JS代碼如下:
javascript:(function JsAddJavascriptInterface_(){
// window.jsInterface 表示在window上聲明了一個(gè)Js對(duì)象
// jsInterface = 注冊(cè)的對(duì)象名
// 它注冊(cè)了兩個(gè)方法串稀,onButtonClick(arg0)和onImageClick(arg0, arg1, arg2)
// 如果有返回值,就添加上return
if (typeof(window.jsInterface)!='undefined') {
console.log('window.jsInterface_js_interface_name is exist!!');}
else {
window.jsInterface = {
// 聲明方法形式:方法名: function(參數(shù))
onButtonClick:function(arg0) {
// prompt()返回約定的字符串
// 該字符串可自己定義
// 包含特定的標(biāo)識(shí)符MyApp和 JSON 字符串(方法名狮杨,參數(shù)母截,對(duì)象名等)
return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onButtonClick',args:[arg0]}));
},
onImageClick:function(arg0,arg1,arg2) {
return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onImageClick',args:[arg0,arg1,arg2]}));
},
};
}
}
)()
// 當(dāng)JS調(diào)用 onButtonClick() 或 onImageClick() 時(shí),就會(huì)回調(diào)到Android中的 onJsPrompt ()
// 我們解析出方法名橄教,參數(shù)清寇,對(duì)象名
// 再通過(guò)反射機(jī)制調(diào)用Java對(duì)象的方法
關(guān)于該方法的其他細(xì)節(jié)
- 加載上述JS代碼的時(shí)機(jī)
由于當(dāng) WebView 跳轉(zhuǎn)到下一個(gè)頁(yè)面時(shí)喘漏,之前加載的 JS 可能已經(jīng)失效,所以华烟,通常需要在以下方法中加載 JS:
onLoadResource()翩迈;
doUpdateVisitedHistory();
onPageStarted()盔夜;
onPageFinished()负饲;
onReceivedTitle();
onProgressChanged()喂链;
- 需要過(guò)濾掉 Object 類的方法
由于最終是通過(guò)反射得到Android指定對(duì)象的方法返十,所以同時(shí)也會(huì)得到基類的其他方法(最頂層的基類是 Object類),為了不把 getClass()等方法注入到 JS 中,我們需要把 Object 的共有方法過(guò)濾掉椭微,需要過(guò)濾的方法列表如下:
getClass()
hashCode()
notify()
notifyAl()
equals()
toString()
wait()
原因二:searchBoxJavaBridge_接口引起遠(yuǎn)程代碼執(zhí)行漏洞
漏洞產(chǎn)生原因:
在Android 3.0以下洞坑,Android系統(tǒng)會(huì)默認(rèn)通過(guò)searchBoxJavaBridge_的Js接口給 WebView 添加一個(gè)JS映射對(duì)象:searchBoxJavaBridge_對(duì)象,該接口可能被利用,實(shí)現(xiàn)遠(yuǎn)程任意代碼蝇率。
解決方案
刪除searchBoxJavaBridge_接口
// 通過(guò)調(diào)用該方法刪除接口
removeJavascriptInterface();
原因三:accessibility和 accessibilityTraversal接口引起遠(yuǎn)程代碼執(zhí)行漏洞
問(wèn)題分析與解決方案同上迟杂,這里不作過(guò)多闡述。
第二類:密碼明文存儲(chǔ)漏洞
漏洞原因:
WebView默認(rèn)開(kāi)啟密碼保存功能
mWebView.setSavePassword(true);
開(kāi)啟后本慕,在用戶輸入密碼時(shí)排拷,會(huì)彈出提示框:詢問(wèn)用戶是否保存密碼;
如果選擇”是”锅尘,密碼會(huì)被明文保到 /data/data/com.package.name/databases/webview.db 中监氢,這樣就有被盜取密碼的危險(xiǎn)
解決方案
關(guān)閉密碼保存提醒
WebSettings.setSavePassword(false)
第三類:域控制不嚴(yán)格漏洞
漏洞原因:
先看Android里的* WebViewActivity.java*:
public class WebViewActivity extends Activity {
private WebView webView;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
webView = (WebView) findViewById(R.id.webView);
//webView.getSettings().setAllowFileAccess(false); (1)
//webView.getSettings().setAllowFileAccessFromFileURLs(true); (2)
//webView.getSettings().setAllowUniversalAccessFromFileURLs(true); (3)
Intent i = getIntent();
String url = i.getData().toString(); //url = file:///data/local/tmp/attack.html
webView.loadUrl(url);
}
}
/**Mainifest.xml**/
// 將該 WebViewActivity 在Mainifest.xml設(shè)置exported屬性
// 表示:當(dāng)前Activity是否可以被另一個(gè)Application的組件啟動(dòng)
android:exported="true"
即 A 應(yīng)用可以通過(guò) B 應(yīng)用導(dǎo)出的 Activity 讓 B 應(yīng)用加載一個(gè)惡意的 file 協(xié)議的 url,從而可以獲取 B 應(yīng)用的內(nèi)部私有文件鉴象,從而帶來(lái)數(shù)據(jù)泄露威脅
具體:當(dāng)其他應(yīng)用啟動(dòng)此 Activity 時(shí), intent 中的 data 直接被當(dāng)作 url 來(lái)加載(假定傳進(jìn)來(lái)的 url 為 file:///data/local/tmp/attack.html )何鸡,其他 APP 通過(guò)使用顯式 ComponentName 或者其他類似方式就可以很輕松的啟動(dòng)該 WebViewActivity 并加載惡意url纺弊。
下面我們著重分析WebView中g(shù)etSettings類的方法對(duì) WebView 安全性的影響:
- setAllowFileAccess()
- setAllowFileAccessFromFileURLs()
- setAllowUniversalAccessFromFileURLs()
1. setAllowFileAccess()
// 設(shè)置是否允許 WebView 使用 File 協(xié)議
// 默認(rèn)設(shè)置為true,即允許在 File 域下執(zhí)行任意 JavaScript 代碼
webView.getSettings().setAllowFileAccess(true);
使用 file 域加載的 js代碼能夠使用同源策略跨域訪問(wèn)骡男,從而導(dǎo)致隱私信息泄露
- 同源策略跨域訪問(wèn):對(duì)私有目錄文件進(jìn)行訪問(wèn)
- 針對(duì) IM 類產(chǎn)品淆游,泄露的是聊天信息、聯(lián)系人等等
- 針對(duì)瀏覽器類軟件隔盛,泄露的是cookie 信息泄露犹菱。
如果不允許使用 file 協(xié)議,則不會(huì)存在上述的威脅吮炕;
webView.getSettings().setAllowFileAccess(false);
但同時(shí)也限制了 WebView 的功能腊脱,使其不能加載本地的 html 文件,如下圖:
移動(dòng)版的 Chrome 默認(rèn)禁止加載 file 協(xié)議的文件
解決方案
- 對(duì)于不需要使用 file 協(xié)議的應(yīng)用龙亲,禁用 file 協(xié)議陕凹;
setAllowFileAccess(false);
- 對(duì)于需要使用 file 協(xié)議的應(yīng)用悍抑,禁止 file 協(xié)議加載 JavaScript。
setAllowFileAccess(true);
// 禁止 file 協(xié)議加載 JavaScript
if (url.startsWith("file://") {
setJavaScriptEnabled(false);
} else {
setJavaScriptEnabled(true);
}
2. setAllowFileAccessFromFileURLs()
// 設(shè)置是否允許通過(guò) file url 加載的 Js代碼讀取其他的本地文件
// 在Android 4.1前默認(rèn)允許
// 在Android 4.1后默認(rèn)禁止
webView.getSettings().setAllowFileAccessFromFileURLs(true);
當(dāng)AllowFileAccessFromFileURLs()設(shè)置為 true 時(shí)杜耙,攻擊者的JS代碼為:
<script>
function loadXMLDoc()
{
var arm = "file:///etc/hosts";
var xmlhttp;
if (window.XMLHttpRequest)
{
xmlhttp=new XMLHttpRequest();
}
xmlhttp.onreadystatechange=function()
{
//alert("status is"+xmlhttp.status);
if (xmlhttp.readyState==4)
{
console.log(xmlhttp.responseText);
}
}
xmlhttp.open("GET",arm);
xmlhttp.send(null);
}
loadXMLDoc();
</script>
// 通過(guò)該代碼可成功讀取 /etc/hosts 的內(nèi)容數(shù)據(jù)
解決方案
禁用 file url 加載的 Js代碼讀取其他的本地文件
setAllowFileAccessFromFileURLs(false);
當(dāng)設(shè)置成為 false 時(shí)搜骡,上述JS的攻擊代碼執(zhí)行會(huì)導(dǎo)致錯(cuò)誤,表示瀏覽器禁止從 file url 中的 javascript 讀取其它本地文件佑女。
3. setAllowUniversalAccessFromFileURLs()
// 設(shè)置是否允許通過(guò) file url 加載的 Javascript 可以訪問(wèn)其他的源(包括http记靡、https等源)
// 在Android 4.1前默認(rèn)允許(setAllowFileAccessFromFileURLs()不起作用)
// 在Android 4.1后默認(rèn)禁止
webView.getSettings().setAllowUniversalAccessFromFileURLs(true);
當(dāng)AllowFileAccessFromFileURLs()被設(shè)置成true時(shí),攻擊者的JS代碼是:
// 通過(guò)該代碼可成功讀取 http://www.so.com 的內(nèi)容
<script>
function loadXMLDoc()
{
var arm = "http://www.so.com";
var xmlhttp;
if (window.XMLHttpRequest)
{
xmlhttp=new XMLHttpRequest();
}
xmlhttp.onreadystatechange=function()
{
//alert("status is"+xmlhttp.status);
if (xmlhttp.readyState==4)
{
console.log(xmlhttp.responseText);
}
}
xmlhttp.open("GET",arm);
xmlhttp.send(null);
}
loadXMLDoc();
</script>
解決方案:設(shè)置setAllowUniversalAccessFromFileURLs(false);
4. setJavaScriptEnabled()
// 設(shè)置是否允許 WebView 使用 JavaScript(默認(rèn)是不允許)
// 但很多應(yīng)用(包括移動(dòng)瀏覽器)為了讓 WebView 執(zhí)行 http 協(xié)議中的 JavaScript团驱,都會(huì)主動(dòng)設(shè)置為true摸吠,不區(qū)別對(duì)待是非常危險(xiǎn)的。
webView.getSettings().setJavaScriptEnabled(true);
即使把setAllowFileAccessFromFileURLs()和setAllowUniversalAccessFromFileURLs()都設(shè)置為 false店茶,通過(guò) file URL 加載的 javascript 仍然有方法訪問(wèn)其他的本地文件:符號(hào)鏈接跨源攻擊
前提是允許 file URL 執(zhí)行 javascript蜕便,即webView.getSettings().setJavaScriptEnabled(true);
這一攻擊能奏效的原因是:通過(guò) javascript 的延時(shí)執(zhí)行和將當(dāng)前文件替換成指向其它文件的軟鏈接就可以讀取到被符號(hào)鏈接所指的文件。具體攻擊步驟:
- 把惡意的 js 代碼輸出到攻擊應(yīng)用的目錄下贩幻,隨機(jī)命名為 xx.html轿腺,修改該目錄的權(quán)限;
- 修改后休眠 1s丛楚,讓文件操作完成族壳;
- 完成后通過(guò)系統(tǒng)的 Chrome 應(yīng)用去打開(kāi)該 xx.html 文件
- 等待 4s 讓 Chrome 加載完成該 html,最后將該 html 刪除趣些,并且使用 ln -s 命令為 Chrome 的 Cookie 文件創(chuàng)建軟連接
注:在該命令執(zhí)行前 xx.html 是不存在的仿荆;執(zhí)行完這條命令之后,就生成了這個(gè)文件坏平,并且將 Cookie 文件鏈接到了 xx.html 上拢操。
于是就可通過(guò)鏈接來(lái)訪問(wèn) Chrome 的 Cookie
Google 沒(méi)有進(jìn)行修復(fù),只是讓Chrome 最新版本默認(rèn)禁用 file 協(xié)議舶替,所以這一漏洞在最新版的 Chrome 中并不存在令境,但是,在日常大量使用 WebView 的App和瀏覽器顾瞪,都有可能受到此漏洞的影響舔庶。通過(guò)利用此漏洞,容易出現(xiàn)數(shù)據(jù)泄露的危險(xiǎn)
如果是 file 協(xié)議陈醒,禁用 javascript 可以很大程度上減小跨源漏洞對(duì) WebView 的威脅惕橙。但并不能完全杜絕跨源文件泄露。
例:應(yīng)用實(shí)現(xiàn)了下載功能钉跷,對(duì)于無(wú)法加載的頁(yè)面弥鹦,會(huì)自動(dòng)下載到 sd 卡中;由于 sd 卡中的文件所有應(yīng)用都可以訪問(wèn)爷辙,于是可以通過(guò)構(gòu)造一個(gè) file URL 指向被攻擊應(yīng)用的私有文件惶凝,然后用此 URL 啟動(dòng)被攻擊應(yīng)用的 WebActivity吼虎,這樣由于該 WebActivity 無(wú)法加載該文件,就會(huì)將該文件下載到 sd 卡下面苍鲜,然后就可以從 sd 卡上讀取這個(gè)文件了
最終解決方案
- 對(duì)于不需要使用 file 協(xié)議的應(yīng)用思灰,禁用 file 協(xié)議;
// 禁用 file 協(xié)議混滔;
setAllowFileAccess(false);
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);
- 對(duì)于需要使用 file 協(xié)議的應(yīng)用洒疚,禁止 file 協(xié)議加載 JavaScript。
// 需要使用 file 協(xié)議
setAllowFileAccess(true);
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);
// 禁止 file 協(xié)議加載 JavaScript
if (url.startsWith("file://") {
setJavaScriptEnabled(false);
} else {
setJavaScriptEnabled(true);
}
WebView及前端H5緩存機(jī)制
前端H5的緩存機(jī)制
- 定義
緩存坯屿,即離線存儲(chǔ)
- 這意味著 H5網(wǎng)頁(yè) 加載后會(huì)存儲(chǔ)在緩存區(qū)域油湖,在無(wú)網(wǎng)絡(luò)連接時(shí)也可訪問(wèn)
- WebView的本質(zhì) = 在 Android中嵌入 H5頁(yè)面,所以领跛,Android WebView自帶的緩存機(jī)制其實(shí)就是 H5頁(yè)面的緩存機(jī)制
- 作用
- 離線瀏覽:用戶可在沒(méi)有網(wǎng)絡(luò)連接時(shí)進(jìn)行H5頁(yè)面訪問(wèn)
- 提高頁(yè)面加載速度 & 減少流量消耗:直接使用已緩存的資源乏德,不需要重新加載
- 具體應(yīng)用
此處講解主要講解 前端H5的緩存機(jī)制 的緩存機(jī)制 & 緩存模式 :
a. 緩存機(jī)制:如何將加載過(guò)的網(wǎng)頁(yè)數(shù)據(jù)保存到本地
b. 緩存模式:加載網(wǎng)頁(yè)時(shí)如何讀取之前保存到本地的網(wǎng)頁(yè)緩存
前者是保存,后者是讀取吠昭,請(qǐng)注意區(qū)別
緩存機(jī)制
瀏覽器緩存機(jī)制
原理
根據(jù) HTTP 協(xié)議頭里的 Cache-Control(或 Expires)和 Last-Modified(或 Etag)等字段來(lái)控制文件緩存的機(jī)制漱牵,下面詳細(xì)介紹Cache-Control肌割、Expires啤覆、Last-Modified & Etag四個(gè)字段:
- Cache-Control:用于控制文件在本地緩存有效時(shí)長(zhǎng)
如服務(wù)器回包:Cache-Control:max-age=600复濒,則表示文件在本地應(yīng)該緩存,且有效時(shí)長(zhǎng)是600秒(從發(fā)出請(qǐng)求算起)蒲肋。在接下來(lái)600秒內(nèi)蘑拯,如果有請(qǐng)求這個(gè)資源,瀏覽器不會(huì)發(fā)出 HTTP 請(qǐng)求兜粘,而是直接使用本地緩存的文件申窘。
- Expires:與Cache-Control功能相同,即控制緩存的有效時(shí)間
- Expires是 HTTP1.0 標(biāo)準(zhǔn)中的字段孔轴,Cache-Control 是 HTTP1.1 標(biāo)準(zhǔn)中新加的字段
- 當(dāng)這兩個(gè)字段同時(shí)出現(xiàn)時(shí)剃法,Cache-Control 優(yōu)先級(jí)較高
- Last-Modified:標(biāo)識(shí)文件在服務(wù)器上的最新更新時(shí)間
下次請(qǐng)求時(shí),如果文件緩存過(guò)期距糖,瀏覽器通過(guò) If-Modified-Since 字段帶上這個(gè)時(shí)間玄窝,發(fā)送給服務(wù)器牵寺,由服務(wù)器比較時(shí)間戳來(lái)判斷文件是否有修改悍引。如果沒(méi)有修改,服務(wù)器返回304告訴瀏覽器繼續(xù)使用緩存帽氓;如果有修改趣斤,則返回200,同時(shí)返回最新的文件黎休。
- Etag:功能同Last-Modified 浓领,即標(biāo)識(shí)文件在服務(wù)器上的最新更新時(shí)間玉凯。不同的是,Etag 的取值是一個(gè)對(duì)文件進(jìn)行標(biāo)識(shí)的特征字串联贩。
- 在向服務(wù)器查詢文件是否有更新時(shí)漫仆,瀏覽器通過(guò)If-None-Match 字段把特征字串發(fā)送給服務(wù)器,由服務(wù)器和文件最新特征字串進(jìn)行匹配泪幌,來(lái)判斷文件是否有更新:沒(méi)有更新回包304盲厌,有更新回包200
- Etag 和 Last-Modified 可根據(jù)需求使用一個(gè)或兩個(gè)同時(shí)使用。兩個(gè)同時(shí)使用時(shí)祸泪,只要滿足基中一個(gè)條件吗浩,就認(rèn)為文件沒(méi)有更新。
常見(jiàn)用法
- Cache-Control與 Last-Modified 一起使用没隘;
-Expires與 Etag一起使用懂扼;
即一個(gè)用于控制緩存有效時(shí)間,一個(gè)用于在緩存失效后右蒲,向服務(wù)查詢是否有更新
特別注意:瀏覽器緩存機(jī)制 是 瀏覽器內(nèi)核的機(jī)制阀湿,一般都是標(biāo)準(zhǔn)的實(shí)現(xiàn),即Cache-Control、 Last-Modified 品嚣、 Expires炕倘、 Etag都是標(biāo)準(zhǔn)實(shí)現(xiàn),你不需要操心
特點(diǎn)
優(yōu)點(diǎn):支持 Http協(xié)議層
不足:緩存文件需要首次加載后才會(huì)產(chǎn)生翰撑;瀏覽器緩存的存儲(chǔ)空間有限罩旋,緩存有被清除的可能;緩存的文件沒(méi)有校驗(yàn)眶诈。
對(duì)于解決以上問(wèn)題涨醋,可以參考手 Q 的離線包
應(yīng)用場(chǎng)景
靜態(tài)資源文件的存儲(chǔ),如JS逝撬、CSS浴骂、字體、圖片等宪潮。
Android Webview會(huì)將緩存的文件記錄及文件內(nèi)容會(huì)存在當(dāng)前 app 的 data 目錄中溯警。
具體實(shí)現(xiàn)
瀏覽器緩存機(jī)制是瀏覽器內(nèi)核的機(jī)制,一般都是標(biāo)準(zhǔn)的實(shí)現(xiàn),Android WebView內(nèi)置自動(dòng)實(shí)現(xiàn)狡相,即不需要設(shè)置即實(shí)現(xiàn).
Android WebView自帶的緩存機(jī)制有5種:
- Application Cache 緩存機(jī)制
- Dom Storage 緩存機(jī)制
- Web SQL Database 緩存機(jī)制
- Indexed Database 緩存機(jī)制
- File System 緩存機(jī)制(H5頁(yè)面新加入的緩存機(jī)制梯轻,雖然Android WebView暫時(shí)不支持,但會(huì)進(jìn)行簡(jiǎn)單介紹)
下面將詳細(xì)介紹每種緩存機(jī)制尽棕。
1. Application Cache 緩存機(jī)制
原理
- 以文件為單位進(jìn)行緩存喳挑,且文件有一定更新機(jī)制(類似于瀏覽器緩存機(jī)制)
- AppCache 原理有兩個(gè)關(guān)鍵點(diǎn):manifest 屬性和 manifest 文件。
<!DOCTYPE html>
<html manifest="demo_html.appcache">
// HTML 在頭中通過(guò) manifest 屬性引用 manifest 文件
// manifest 文件:就是上面以 appcache 結(jié)尾的文件,是一個(gè)普通文件文件伊诵,列出了需要緩存的文件
// 瀏覽器在首次加載 HTML 文件時(shí)单绑,會(huì)解析 manifest 屬性,并讀取 manifest 文件曹宴,獲取 Section:CACHE MANIFEST 下要緩存的文件列表搂橙,再對(duì)文件緩存
<body>
...
</body>
</html>
// 原理說(shuō)明如下:
// AppCache 在首次加載生成后,也有更新機(jī)制笛坦。被緩存的文件如果要更新份氧,需要更新 manifest 文件
// 因?yàn)闉g覽器在下次加載時(shí),除了會(huì)默認(rèn)使用緩存外弯屈,還會(huì)在后臺(tái)檢查 manifest 文件有沒(méi)有修改
//發(fā)現(xiàn)有修改蜗帜,就會(huì)重新獲取 manifest 文件,對(duì) Section:CACHE MANIFEST 下文件列表檢查更新
// manifest 文件與緩存文件的檢查更新也遵守瀏覽器緩存機(jī)制
// 如用戶手動(dòng)清了 AppCache 緩存资厉,下次加載時(shí)厅缺,瀏覽器會(huì)重新生成緩存,也可算是一種緩存的更新
// AppCache 的緩存文件宴偿,與瀏覽器的緩存文件分開(kāi)存儲(chǔ)的湘捎,因?yàn)?AppCache 在本地有 5MB(分 HOST)的空間限制
特點(diǎn)
方便構(gòu)建Web App的緩存
專門為 Web App離線使用而開(kāi)發(fā)的緩存機(jī)制
應(yīng)用場(chǎng)景
存儲(chǔ)靜態(tài)文件(如JS、CSS窄刘、字體文件),應(yīng)用場(chǎng)景同瀏覽器緩存機(jī)制,但AppCache 是對(duì) 瀏覽器緩存機(jī)制 的補(bǔ)充窥妇,不是替代。
具體實(shí)現(xiàn)
// 通過(guò)設(shè)置WebView的settings來(lái)實(shí)現(xiàn)
WebSettings settings = getSettings();
// 1. 設(shè)置緩存路徑
String cacheDirPath = context.getFilesDir().getAbsolutePath()+"cache/";
settings.setAppCachePath(cacheDirPath);
// 2. 設(shè)置緩存大小
settings.setAppCacheMaxSize(20*1024*1024);
// 3. 開(kāi)啟Application Cache存儲(chǔ)機(jī)制
settings.setAppCacheEnabled(true);
特別注意:
每個(gè) Application 只調(diào)用一次 WebSettings.setAppCachePath() 和WebSettings.setAppCacheMaxSize()
2.Dom Storage 緩存機(jī)制
原理
通過(guò)存儲(chǔ)字符串的 Key - Value 對(duì)來(lái)提供.DOM Storage 分為 sessionStorage & localStorage娩践; 二者使用方法基本相同活翩,區(qū)別在于作用范圍不同:
- sessionStorage:具備臨時(shí)性,即存儲(chǔ)與頁(yè)面相關(guān)的數(shù)據(jù)翻伺,它在頁(yè)面關(guān)閉后無(wú)法使用
- localStorage:具備持久性材泄,即保存的數(shù)據(jù)在頁(yè)面關(guān)閉后也可以使用。
特點(diǎn) - 存儲(chǔ)空間大( 5MB):存儲(chǔ)空間對(duì)于不同瀏覽器不同吨岭,如Cookies 才 4KB
- 存儲(chǔ)安全拉宗、便捷: Dom Storage 存儲(chǔ)的數(shù)據(jù)在本地,不需要經(jīng)常和服務(wù)器進(jìn)行交互,不像 Cookies每次請(qǐng)求一次頁(yè)面辣辫,都會(huì)向服務(wù)器發(fā)送網(wǎng)絡(luò)請(qǐng)求
應(yīng)用場(chǎng)景
存儲(chǔ)臨時(shí)旦事、簡(jiǎn)單的數(shù)據(jù)
- 代替 **將 不需要讓服務(wù)器知道的信息 存儲(chǔ)到 cookies **的這種傳統(tǒng)方法
- Dom Storage 機(jī)制類似于 Android 的 SharedPreference機(jī)制
具體實(shí)現(xiàn)
// 通過(guò)設(shè)置 `WebView`的`Settings`類實(shí)現(xiàn)
WebSettings settings = getSettings();
// 開(kāi)啟DOM storage
settings.setDomStorageEnabled(true);
3.Web SQL Database 緩存機(jī)制
原理
基于 SQL 的數(shù)據(jù)庫(kù)存儲(chǔ)機(jī)制
特點(diǎn)
充分利用數(shù)據(jù)庫(kù)的優(yōu)勢(shì),可方便對(duì)數(shù)據(jù)進(jìn)行增加急灭、刪除姐浮、修改、查詢
應(yīng)用場(chǎng)景
存儲(chǔ)適合數(shù)據(jù)庫(kù)的結(jié)構(gòu)化數(shù)據(jù)
具體實(shí)現(xiàn)
// 通過(guò)設(shè)置WebView的settings實(shí)現(xiàn)
WebSettings settings = getSettings();
// 設(shè)置緩存路徑
String cacheDirPath = context.getFilesDir().getAbsolutePath()+"cache/";
settings.setDatabasePath(cacheDirPath);
// 開(kāi)啟 數(shù)據(jù)庫(kù)存儲(chǔ)機(jī)制
settings.setDatabaseEnabled(true);
特別說(shuō)明
根據(jù)官方說(shuō)明化戳,Web SQL Database存儲(chǔ)機(jī)制不再推薦使用(不再維護(hù)),取而代之的是 IndexedDB緩存機(jī)制单料,下面會(huì)詳細(xì)介紹
4.IndexedDB 緩存機(jī)制
原理
屬于 NoSQL 數(shù)據(jù)庫(kù),通過(guò)存儲(chǔ)字符串的 Key - Value 對(duì)來(lái)提供,類似于 Dom Storage 存儲(chǔ)機(jī)制 的key-value存儲(chǔ)方式
特點(diǎn)
應(yīng)用場(chǎng)景
存儲(chǔ) 復(fù)雜点楼、數(shù)據(jù)量大的結(jié)構(gòu)化數(shù)據(jù)
具體實(shí)現(xiàn)
// 通過(guò)設(shè)置WebView的settings實(shí)現(xiàn)
WebSettings settings = getSettings();
// 只需設(shè)置支持JS就自動(dòng)打開(kāi)IndexedDB存儲(chǔ)機(jī)制
// Android 在4.4開(kāi)始加入對(duì) IndexedDB 的支持扫尖,只需打開(kāi)允許 JS 執(zhí)行的開(kāi)關(guān)就好了。
settings.setJavaScriptEnabled(true);
5.File System
原理
- 為 H5頁(yè)面的數(shù)據(jù) 提供一個(gè)虛擬的文件系統(tǒng)
- 可進(jìn)行文件(夾)的創(chuàng)建掠廓、讀换怖、寫(xiě)、刪除蟀瞧、遍歷等操作沉颂,就像 Native App 訪問(wèn)本地文件系統(tǒng)一樣
- 虛擬的文件系統(tǒng)是運(yùn)行在沙盒中
- 不同 WebApp 的虛擬文件系統(tǒng)是互相隔離的,虛擬文件系統(tǒng)與本地文件系統(tǒng)也是互相隔離的悦污。
- 虛擬文件系統(tǒng)提供了兩種類型的存儲(chǔ)空間:臨時(shí) & 持久性:
- 臨時(shí)的存儲(chǔ)空間:由瀏覽器自動(dòng)分配铸屉,但可能被瀏覽器回收
- 持久性的存儲(chǔ)空間:需要顯式申請(qǐng);自己管理(瀏覽器不會(huì)回收切端,也不會(huì)清除內(nèi)容)彻坛;存儲(chǔ)空間大小通過(guò)配額管理,首次申請(qǐng)時(shí)會(huì)一個(gè)初始的配額踏枣,配額用完需要再次申請(qǐng)昌屉。
特點(diǎn)- 可存儲(chǔ)數(shù)據(jù)體積較大的二進(jìn)制數(shù)據(jù)
- 可預(yù)加載資源文件
- 可直接編輯文件
應(yīng)用場(chǎng)景
通過(guò)文件系統(tǒng) 管理數(shù)據(jù)
具體使用
由于 File System是 H5 新加入的緩存機(jī)制,所以Android WebView暫時(shí)不支持
緩存機(jī)制匯總
使用建議
- 綜合上述緩存機(jī)制的分析茵瀑,我們可以根據(jù) 需求場(chǎng)景的不同(緩存不同類型的數(shù)據(jù)場(chǎng)景) 從而選擇不同的緩存機(jī)制(組合使用)
-
以下是緩存機(jī)制的使用建議:
使用建議
緩存模式
- 定義
緩存模式是一種 當(dāng)加載H5網(wǎng)頁(yè)時(shí)該如何讀取之前保存到本地緩存從而進(jìn)行使用 的方式,即告訴Android WebView 什么時(shí)候去讀緩存间驮,以哪種方式去讀緩存 - Android WebView 自帶的緩存模式有4種:
// 緩存模式說(shuō)明:
// LOAD_CACHE_ONLY: 不使用網(wǎng)絡(luò),只讀取本地緩存數(shù)據(jù)
// LOAD_NO_CACHE: 不使用緩存马昨,只從網(wǎng)絡(luò)獲取數(shù)據(jù).
// LOAD_DEFAULT: (默認(rèn))根據(jù)cache-control決定是否從網(wǎng)絡(luò)上取數(shù)據(jù)竞帽。
// LOAD_CACHE_ELSE_NETWORK,只要本地有鸿捧,無(wú)論是否過(guò)期抢呆,或者no-cache,都使用緩存中的數(shù)據(jù)笛谦。
- 具體使用
// 設(shè)置參數(shù)即可
WebView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
WebView 存在的性能問(wèn)題
- Android WebView 里 H5 頁(yè)面加載速度慢
- 耗費(fèi)流量
下面會(huì)詳細(xì)介紹抱虐。
H5頁(yè)面加載速度慢
下面會(huì)詳細(xì)介紹:
渲染速度慢
前端H5頁(yè)面渲染的速度取決于兩個(gè)方面:
- Js 解析效率:Js 本身的解析過(guò)程復(fù)雜、解析速度不快 & 前端頁(yè)面涉及較多 JS 代碼文件饥脑,所以疊加起來(lái)會(huì)導(dǎo)致 Js 解析效率非常低恳邀。
- 手機(jī)硬件設(shè)備的性能:由于Android機(jī)型碎片化,這導(dǎo)致手機(jī)硬件設(shè)備的性能不可控灶轰,而大多數(shù)的Android手機(jī)硬件設(shè)備無(wú)法達(dá)到很好很好的硬件性能谣沸。
總結(jié):上述兩個(gè)原因 導(dǎo)致 H5頁(yè)面的渲染速度慢。
頁(yè)面資源加載緩慢
H5 頁(yè)面從服務(wù)器獲得笋颤,并存儲(chǔ)在 Android手機(jī)內(nèi)存里:
- H5頁(yè)面一般會(huì)比較多
- 每加載一個(gè) H5頁(yè)面乳附,都會(huì)產(chǎn)生較多網(wǎng)絡(luò)請(qǐng)求:
1.HTML 主 URL 自身的請(qǐng)求内地;
2.HTML外部引用的JS、CSS赋除、字體文件阱缓,圖片也是一個(gè)獨(dú)立的 HTTP 請(qǐng)求
每一個(gè)請(qǐng)求都串行的,這么多請(qǐng)求串起來(lái)举农,這導(dǎo)致 H5頁(yè)面資源加載緩慢
總結(jié):H5頁(yè)面加載速度慢的原因:渲染速度慢 & 頁(yè)面資源加載緩慢導(dǎo)致荆针。
耗費(fèi)流量
- 每次使用 H5頁(yè)面時(shí),用戶都需要重新加載 Android WebView的H5 頁(yè)面
- 每加載一個(gè) H5頁(yè)面颁糟,都會(huì)產(chǎn)生較多網(wǎng)絡(luò)請(qǐng)求(上面提到)
- 每一個(gè)請(qǐng)求都串行的航背,這么多請(qǐng)求串起來(lái),這導(dǎo)致消耗的流量也會(huì)越多
總結(jié)
綜上所述棱貌,產(chǎn)生Android WebView性能問(wèn)題主要原因是:
上述問(wèn)題導(dǎo)致了Android WebView的H5 頁(yè)面體驗(yàn) 與 原生Native 存在較大差距玖媚。
解決方案
針對(duì)上述Android WebView的性能問(wèn)題,結(jié)合剛剛分析的WebView和h5前端緩存機(jī)制延伸出3種優(yōu)化思路供參考:
- 利用WebView和前端H5的緩存機(jī)制(WebView 自帶)
- 資源預(yù)加載
- 資源攔截
第一種方式在上面已經(jīng)提到婚脱,下面對(duì)后面兩種方式進(jìn)行詳細(xì)介紹:
資源預(yù)加載
- 定義
提早加載將需使用的H5頁(yè)面最盅,即 提前構(gòu)建緩存,使用時(shí)直接取過(guò)來(lái)用而不用在需要時(shí)才去加載起惕。 - 具體實(shí)現(xiàn)
- 預(yù)加載WebView對(duì)象
- 預(yù)加載H5資源
預(yù)加載WebView對(duì)象
- 此處主要分為2方面:首次使用的WebView對(duì)象 & 后續(xù)使用的WebView對(duì)象
-
具體如下圖
示意圖
預(yù)加載H5資源
原理
在應(yīng)用啟動(dòng)涡贱、初始化第一個(gè)WebView對(duì)象時(shí),直接開(kāi)始網(wǎng)絡(luò)請(qǐng)求加載H5頁(yè)面,后續(xù)需打開(kāi)這些H5頁(yè)面時(shí)就直接從該本地對(duì)象中獲取惹想。
事先加載常用的H5頁(yè)面資源(加載后就有緩存了),此方法雖然不能減小WebView初始化時(shí)間问词,但數(shù)據(jù)請(qǐng)求和WebView初始化可以并行進(jìn)行,總體的頁(yè)面加載時(shí)間就縮短了嘀粱。
具體實(shí)現(xiàn)
在Android 的BaseApplication里初始化一個(gè)WebView對(duì)象(用于加載常用的H5頁(yè)面資源)激挪;當(dāng)需使用這些頁(yè)面時(shí)再?gòu)腂aseApplication里取過(guò)來(lái)直接使用。
應(yīng)用場(chǎng)景
對(duì)于Android WebView的首頁(yè)建議使用這種方案锋叨,能有效提高首頁(yè)加載的效率
自身構(gòu)建緩存
為了有效解決 Android WebView 的性能問(wèn)題垄分,除了使用 Android WebView 自身的緩存機(jī)制,還可以自己針對(duì)某一需求場(chǎng)景構(gòu)建緩存機(jī)制娃磺。
需求場(chǎng)景
實(shí)現(xiàn)步驟
- 事先將更新頻率較低薄湿、常用 & 固定的H5靜態(tài)資源 文件(如JS、CSS文件偷卧、圖片等) 放到本地
- 攔截H5頁(yè)面的資源網(wǎng)絡(luò)請(qǐng)求 并進(jìn)行檢測(cè)
- 如果檢測(cè)到本地具有相同的靜態(tài)資源 就 直接從本地讀取進(jìn)行替換而不發(fā)送該資源的網(wǎng)絡(luò)請(qǐng)求 到 服務(wù)器獲取
示意圖
具體實(shí)現(xiàn)
重寫(xiě)WebViewClient 的 shouldInterceptRequest 方法豺瘤,當(dāng)向服務(wù)器訪問(wèn)這些靜態(tài)資源時(shí)進(jìn)行攔截,檢測(cè)到是相同的資源則用本地資源代替
// 假設(shè)現(xiàn)在需要攔截一個(gè)圖片的資源并用本地資源進(jìn)行替代
mWebView.setWebViewClient(new WebViewClient() {
// 重寫(xiě) WebViewClient 的 shouldInterceptRequest ()
// API 21 以下用shouldInterceptRequest(WebView view, String url)
// API 21 以上用shouldInterceptRequest(WebView view, WebResourceRequest request)
// 下面會(huì)詳細(xì)說(shuō)明
// API 21 以下用shouldInterceptRequest(WebView view, String url)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
// 步驟1:判斷攔截資源的條件听诸,即判斷url里的圖片資源的文件名
// 假設(shè)網(wǎng)頁(yè)里該圖片資源的地址為:http://abc.com/imgage/logo.gif
// 圖片的資源文件名為:logo.gif
if (url.contains("logo.gif")) {
// 步驟2:創(chuàng)建一個(gè)輸入流
InputStream is = null;
try {
// 步驟3:獲得需要替換的資源(存放在assets文件夾里)
// a. 先在app/src/main下創(chuàng)建一個(gè)assets文件夾
// b. 在assets文件夾里再創(chuàng)建一個(gè)images文件夾
// c. 在images文件夾放上需要替換的資源(此處替換的是abc.png圖片)
is = getApplicationContext().getAssets().open("images/abc.png");
} catch (IOException e) {
e.printStackTrace();
}
// 步驟4:替換資源
// 參數(shù)1:http請(qǐng)求里該圖片的Content-Type,此處圖片為image/png
// 參數(shù)2:編碼類型
// 參數(shù)3:存放著替換資源的輸入流(上面創(chuàng)建的那個(gè))
WebResourceResponse response = new WebResourceResponse("image/png",
"utf-8", is);
return response;
}
return super.shouldInterceptRequest(view, url);
}
// API 21 以上用shouldInterceptRequest(WebView view, WebResourceRequest request)
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
// 步驟1:判斷攔截資源的條件坐求,即判斷url里的圖片資源的文件名
// 假設(shè)網(wǎng)頁(yè)里該圖片資源的地址為:http://abc.com/imgage/logo.gif
// 圖片的資源文件名為:logo.gif
if (request.getUrl().toString().contains("logo.gif")) {
// 步驟2:創(chuàng)建一個(gè)輸入流
InputStream is = null;
try {
// 步驟3:獲得需要替換的資源(存放在assets文件夾里)
// a. 先在app/src/main下創(chuàng)建一個(gè)assets文件夾
// b. 在assets文件夾里再創(chuàng)建一個(gè)images文件夾
// c. 在images文件夾放上需要替換的資源(此處替換的是abc.png圖片
is = getApplicationContext().getAssets().open("images/abc.png");
} catch (IOException e) {
e.printStackTrace();
}
// 步驟4:替換資源
// 參數(shù)1:http請(qǐng)求里該圖片的Content-Type,此處圖片為image/png
// 參數(shù)2:編碼類型
// 參數(shù)3:存放著替換資源的輸入流(上面創(chuàng)建的那個(gè))
WebResourceResponse response = new WebResourceResponse("image/png",
"utf-8", is);
return response;
}
return super.shouldInterceptRequest(view, request);
}
});
另:
Android加載網(wǎng)頁(yè)的選擇有三種方案,分別是:
- 使用Android系統(tǒng)自帶的WebView
- 使用騰訊X5內(nèi)核的WebView
-
使用基于chrome webkit的crosswalk WebView
方案對(duì)比及建議如下:
方案對(duì)比及建議
參考文章
Android開(kāi)發(fā):最全面晌梨、最易懂的Webview詳解
最全面總結(jié) Android WebView與 JS 的交互方式
手把手教你構(gòu)建 Android WebView 的緩存機(jī)制 & 資源預(yù)加載方案
你不知道的 Android WebView 使用漏洞
android webView 內(nèi)核對(duì)比