1. 程式人生 > >Android WebView全面講解

Android WebView全面講解

1.前言

WebView是Android中的原生UI控制元件,主要用於在app應用中方便地訪問遠端網頁或本地html資源。同時,WebView也在Android中充當Java程式碼和JS程式碼之間互動的橋樑。實際上,也可以將WebView看做一個功能最小化的瀏覽器。本文將全面講解WebView各方面的知識點。

2.基本使用

建立一個WebView元件

通常情況下,我們會在XML檔案中定義需要使用的UI控制元件,這也是官方提倡的使用方式。WebView當然也可以直接在XML中定義,但這種方式存在潛在的問題。如果在XML中定義WebView,那麼系統將把當前的Activity作為Context去例項化WebView物件。由於WebView保持著對Activity的引用,如果在Activity結束時WebView還未進入銷燬狀態,將導致Activity無法被系統回收,進而造成記憶體洩漏。

因此,並不建議直接在XML檔案中定義WebView,而是在需要使用WebView的時候手動建立,並將其加入合適的佈局中。下面將給出一個簡單的例子:

XML檔案:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
<!--WebView的容器--> <FrameLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout
>

我們在主佈局中定義了一個FrameLayout,這將作為WebView的容器。接下來,讓我們來看一下應該如何通過Java程式碼建立WebView並新增到這個容器中。

Java程式碼:

//通過程式碼建立
FrameLayout parentLayout=findViewById(R.id.container);
webView=new WebView(getApplicationContext());//使用應用級別的context,避免對Activity的引用
FrameLayout.LayoutParams layoutParams=new FrameLayout.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
parentLayout.addView(webView,layoutParams);

首先,通過findViewById方法獲取到之前定義在XML中的FrameLayout。然後,將ApplicationContext作為引數建立WebView物件。同時,建立FrameLayout.LayoutParams物件,這將是WebView的佈局引數。最後,呼叫FrameLayout的addView方法,就將WebView新增到了目標位置。

需要注意:使用ApplicationContext建立WebView,WebView就不會再持有當前Activity的引用,那麼前文提到的記憶體洩漏問題也就隨之解決了。但是這種方式也是有代價的,目前已知的問題有:使用第三方應用開啟網頁連結異常、彈出Dialog異常、在WebView中使用Flash異常。此外,如果載入的網頁中包含<select><option>這樣的下拉框元件,也必須使用Activity級別的Context。因此,建議自行測試並根據實際情況選擇是否需要採取這種方式。如果想避免這些問題,也可以直接使用當前Activity作為Context建立WebView,然後將這個Activity執行在獨立程序中。具體操作請參考下文[5.常見問題#WeView出現OOM影響主程序]以及[5.常見問題#WebView後臺耗電問題]部分。

載入網頁或資源

WebView可以載入多種資源,包括本地資源和遠端資源,同時也有多種用於載入資源的方法。

1.loadUrl(以url形式載入目標資源)

載入assets中的資源:

webView.loadUrl("file:///android_asset/test.html");//載入本地assets資料夾下的資源
webView.loadUrl("file:///android_asset/web/test.html");//載入本地assets的web子資料夾下的資源

載入res中的資源:

webView.loadUrl("file:///android_res/mipmap/ic_launcher.png");//載入本地res資料夾下的圖片
webView.loadUrl("file:///android_res/raw/ic_launcher.png");//載入本地res資料夾下raw資料夾下的圖片
webView.loadUrl("file:///android_res/raw/test.html");//載入本地res資料夾下raw資料夾下的html檔案

經過實際測試,WebView只能載入res的drawble和mipmap資料夾中的圖片資源,以及res的raw資料夾中的資源(圖片或html檔案皆可)。需要注意,url中的mipmap代指所有的mipmap資料夾,同理drawable代指所有drawable資料夾,不需要給出資料夾的具體限定符,系統會自行尋找合適的資源。此外,WebView**並不支援**載入raw的子資料夾下的資源。

載入sdcard中的資源:

webView.loadUrl("file:/sdcard/test.html");//載入本地sdcard下的資源
webView.loadUrl("file:///sdcard/test.html");//載入本地sdcard下的資源
webView.loadUrl("content://com.android.htmlfileprovider/sdcard/test.html");//載入本地sdcard下的資源

載入遠端資源:

webView.loadUrl("http://blog.csdn.net/codingending/article/details/78609902");//載入遠端網頁

需要注意,使用WebView載入遠端網頁前,需要在AndroidManifest檔案中為應用申請網路使用許可權:

<uses-permission android:name="android.permission.INTERNET"/>

新增請求頭:
loadUrl有一個過載方法,可以新增多個請求頭:

//additionalHttpHeaders以鍵值對的形式儲存請求頭
public void loadUrl(String url, Map<String, String> additionalHttpHeaders)

2.loadData(以字串形式載入html片段)

//data:html片段
//mimeType:資料型別,如"text/html"
//encoding:資料編碼,有兩種可選值("base64"和其他任何值),分別代表base64編碼和URL編碼
public void loadData(String data, String mimeType, String encoding)

為mimeType傳入null和傳入”text/html”意義相同。為encoding傳入除”base64”外的任何值,都相當於是指定資料編碼為URL編碼,一般我們傳入null即可。

需要注意,如果data中包含中文,結果將顯示為亂碼,解決方案請參考下文[5.常見問題#loadData載入中文亂碼]部分。

此外,data中的’#’,’%’,’\’,’?’應該分別被替換為%23,%25,%27,%3f

3.loadUrlWithBaseURL(以字串形式載入html片段)

//baseUrl:基礎url,傳入null相當於傳入了"about:blank"
//data:html片段
//mimeType:資料型別,如"text/html"
//encoding:資料編碼,有兩種可選值("base64"和其他任何值),分別代表base64編碼和URL編碼
//historyUrl:歷史url
public void loadDataWithBaseURL(String baseUrl, String data,String mimeType, String encoding, String historyUrl)

loadUrlWithBaseURL方法和loadData類似,主要是用於解決Javascript的同源限制問題。實際上,如果為baseUrl和history傳入null,那麼loadUrlWithBaseURL和loadData方法作用相同,並且載入中文資料不會出現亂碼的問題。

4.postUrl(以post請求的形式訪問url)

//postData:本次post請求攜帶的資料,必須是application/x-www-form-urlencoded編碼
public void postUrl(String url, byte[] postData)

如果傳入的url不是一個遠端網頁地址,那麼最終將通過loadUrl方法載入這個url,同時postData引數會被忽略。

前進和後退功能

WebView作為一個功能最小化的瀏覽器,內部維持著url跳轉的歷史記錄,因此可以輕鬆地實現前進/後退功能。

public boolean canGoBack() //判斷是否可以後退
public void goBack() //後退
public boolean canGoForward() //判斷是否可以前進
public void goForward() //前進

一般情況下,我們需要在點選手機的後退按鈕時,讓WebView執行後退操作。這一需求可以通過重寫Activity的onKeyDown或onBackPressed方法實現。下面以重寫onKeyDown方法為例:

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if(keyCode==KeyEvent.KEYCODE_BACK&&webView.canGoBack()){
        webView.goBack();
        return true;
    }
    return super.onKeyDown(keyCode, event);
}

理論上來說,通過上面提供的幾個方法就可以方便地實現後退/前進功能了。但是現實往往是殘酷的,url跳轉過程中的重定向動作會對後退功能帶來棘手的麻煩。

舉個簡單的例子:訪問頁面A->頁面A重定向至頁面B->呼叫goBack方法後退->回到頁面A->頁面A重定向至頁面B->….

在這種情況下,由於頁面A每次載入完成後的重定向動作,WebView將永遠無法通過後退功能退出頁面B。只有在連續點選兩次後退按鈕時,才能在頁面A進行重定向跳轉前直接退出頁面A,進而跳出這個迴圈。在實際開發中,必須避免使用者陷入這樣的困境中,因為不是每個使用者都知道通過按兩次後退按鈕跳出重定向迴圈。我們可以學習微信的處理方式,在Activity的左上角顯示一個關閉按鈕,允許使用者在任何情況下都能退出頁面。

重新整理頁面和停止載入

public void reload() //重新整理頁面(當前頁面的所有資源都會重新載入)
public void stopLoading() //停止載入

清除資料

public void clearCache(boolean includeDiskFiles) //清除快取
public void clearFormData() //清除表單資料
public void clearHistory() //清除歷史記錄

如果為clearCache傳入false,僅有記憶體快取會被清除;如果傳入true,磁碟中的快取也會被一塊清除。需要注意,這個方法針對的是當前應用程式中的所有WebView。

clearFormData僅僅清除表單的自動填充資料,並不會影響已經儲存到本地的表單資料。

WebView狀態

public void onPause() //盡最大努力暫停WebView當前可被安全暫停的行為(如動畫、定位),但並不暫停Javascript
public void onResume() //恢復WebView被onPause暫停的行為
public void pauseTimers() //暫停WebView的所有行為
public void resumeTimers()  //恢復WebView被pauseTimers暫停的行為

需要注意,pauseTimers是一個全域性方法,針對的是所有App的WebView。恰當地使用這一方法可以降低CPU功耗。

銷燬WebView

public void destroy() //銷燬WebView

需要注意,只有先把WebView從佈局中移除後,才能夠呼叫這個方法安全地銷燬WebView。正確地銷燬WebView,才能避免應用程式出現記憶體洩漏以及其它特殊問題。具體的解決方案請參考下文的[5.常見問題#記憶體洩漏]部分。

3.進階使用

WebViewClient

預設情況下,WebView會呼叫系統已安裝的其他瀏覽器載入傳入的網址或者資源。也就是說將跳出當前應用去處理網路請求,顯然這並不是我們想要的結果。如果我們希望直接使用自己的WebView載入網址或資源,就必須為WebView設定WebViewClient。WebViewClient有多個回撥方法,我們可以通過重寫這些方法來實現想要的功能。如:載入狀態、跳轉請求、資源請求、錯誤資訊等。

public void setWebViewClient(WebViewClient client)

只需要呼叫WebView的setWebViewClient方法,即可簡單地為當前WebView設定WebViewClient物件。下面,我們來看一下WebViewClient常用的回撥方法。

攔截url跳轉請求

//url:WebView即將載入的url
public boolean shouldOverrideUrlLoading(WebView view, String url)

//request:封裝本次請求的詳細資訊(包括url、請求方法、請求頭)
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request)

以上兩個方法都會在WebView載入新的url時觸發。不同之處在於,第二個方法是在Android 5.0(API 21)之後才加入的。換句話說,Android 5.0以下系統會回撥第一個方法,反之回撥第二個方法。因此,為了相容不同的系統版本,我們需要同時重寫這兩個方法。

可以看到,這兩個方法都有一個boolean返回值。但是,無論返回true還是false,只要為WebView設定了WebViewClient,系統就不會再將url交給第三方的瀏覽器去處理了。

這兩種返回值的真正區別是這樣的:shouldOverrideUrlLoading返回false,代表將url交給當前WebView載入,也就是正常的載入狀態;shouldOverrideUrlLoading返回true,代表開發者已經對url進行了處理,WebView就不會再對這個url進行載入了。使用true這個返回值的最大作用是遮蔽某些網址,可以藉此實現黑名單機制。實際上,無論返回true還是false,我們都可以實現正常的載入功能,程式碼如下:

//方法1
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    return false;
}
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
    return false;
}

//方法2
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    view.loadUrl(url);//手動載入
    return true;
}
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
    view.loadUrl(request.getUrl().toString());//手動載入
    return true;
}

需要注意,谷歌官方並不是很提倡第二種處理方法。因為shouldOverrideUrlLoading(WebView view, WebResourceRequest request)方法也會在url並非http協議時回撥,而在這裡我們並沒有對url進行判斷,直接使用loadUrl方法載入非http協議的url將會失敗。因此,如果沒有特殊的需求,建議直接返回false即可。否則,就注意要在呼叫loadUrl方法前先對url的協議進行判斷。

最後要知道,如果使用post方式載入url,shouldOverrideUrlLoading方法是不會被觸發的。

攔截資源請求

//url:本次請求的url
public WebResourceResponse shouldInterceptRequest(WebView view,String url)

//request:封裝本次請求的詳細資訊(包括url、請求方法、請求頭)
public WebResourceResponse shouldInterceptRequest(WebView view,WebResourceRequest request)

以上兩個方法都會在WebView發生資源請求的時候回撥(使用loadUrl載入遠端網頁時也會觸發這個方法),因此我們可以按照業務需求對資源進行替換。和shouldOverrideUrlLoading一樣,第二個方法是在Android 5.0(API 21)之後才加入的。Android 5.0以下系統會回撥第一個方法,反之回撥第二個方法。因此,為了相容不同的系統版本,我們需要同時重寫這兩個方法。

可以看到,shouldInterceptRequest的返回值是一個WebResourceResponse物件,在這個物件中封裝了本次url響應的資料。因此,我們只要構造出一個包含目標資料的WebResourceResponse物件,並將其返回,就實現了資源的替換。在需要使用本地資源替換遠端資源的場景中,這個回撥方法非常有用。當然,如果直接返回null,WebView將會正常地載入url對應的資源。下面提供一個簡單的資源替換例子:

//判斷是否需要替換資源
private WebResourceResponse shouldReplaceResource(String url){
    String targetHtml="<html><body><a href='http://blog.csdn.net/codingending'>先看看我的部落格</a></body></html>";
    if("http://blog.csdn.net/".equals(url)){//攔截目標url
        InputStream targetContent=new ByteArrayInputStream(targetHtml.getBytes());//構造InputStream
        return new WebResourceResponse("text/html","utf-8",targetContent);//返回重新構造的資源
    }
    return null;
}

//WebViewClient
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
    return shouldReplaceResource(url);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
    return shouldReplaceResource(request.getUrl().toString());
}

當使用loadUrl載入http://blog.csdn.net/時,WebView就會進行資源替換。

需要注意,shouldInterceptRequest會在非UI執行緒中回撥。因此,如果需要在這個方法中進行View操作,需要手動切換執行緒。

監聽頁面載入狀態

//favicon:當前網頁的圖示
public void onPageStarted(WebView view, String url, Bitmap favicon)
public void onPageFinished(WebView view, String url)

onPageStarted將在網頁開始載入時回撥,onPageFinished則會在頁面載入結束時回撥。需要注意,即使onPageFinished方法已經回撥,也並不代表當前頁面中的所有資源都已經載入完畢,可能當前網頁還在載入圖片等比較耗時的資源。

監聽資源載入

//url:需要載入的資源地址
public void onLoadResource(WebView view, String url)

onLoadResource將在WebView載入資源(如css、js、圖片等)時回撥。

監聽錯誤回撥

//errorCode:錯誤碼(定義在WebViewClient中,形式都是ERROR_*)
//description:對當前錯誤的描述
//failingUrl:發生錯誤的url
public void onReceivedError(WebView view, int errorCode,String description, String failingUrl)

//request:封裝本次請求的詳細資訊(包括url、請求方法、請求頭)
//error:封裝本次錯誤的詳細資訊(包括錯誤碼和錯誤描述)
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)

第一個方法會在當前頁面內容載入發生錯誤時回撥(不包括載入圖片、css等出錯的場景),第二個方法則會在當前頁面的任何資源加載出錯時回撥(包括載入圖片、css等出錯的場景)。此外,在Android 6.0以下,系統會回撥第一個方法,否則回撥第二個方法。

關鍵回撥方法的執行順序
1.使用loadUrl方法載入頁面A(無重定向)

onPageStarted->onPageFinished

2.使用loadUrl方法載入頁面A(頁面A重定向至頁面B)

onPageStarted->shouldOverrideUrlLoading->onPageStarted->onPageFinished->onPageFinished

3.在已載入的頁面中點選連結,載入頁面A(無重定向)

shouldOverrideUrlLoading->onPageStarted->onPageFinished

4.在已載入的頁面中點選連結,載入頁面A(頁面A重定向至頁面B)

shouldOverrideUrlLoading->onPageStarted->shouldOverrideUrlLoading->onPageStarted->onPageFinished->onPageFinished

5.執行goBack/goForward/reload方法

onPageStarted->onPageFinished

6.發生資源載入

shouldInterceptRequest->onLoadResource

WebChromeClient

除了為WebView設定WebViewClient,我們還可以呼叫WebView的setWebChromeClient方法設定WebChromeClient。WebChromeClient主要用於輔助WebView處理頁面載入進度、Javascript的對話方塊,獲取網站圖示、網站標題等。

public void setWebChromeClient(WebChromeClient client)

WebChromeClient同樣有多個回撥方法,只需要重寫合適的方法就能輕鬆地實現多種需求。

監聽網頁載入進度

//newProgress:網頁當前的載入進度(數值為0-100)
public void onProgressChanged(WebView view, int newProgress)

onProgressChanged會在網頁載入過程中多次觸發。當newProgress的值為100時,可以認為當前網頁已經載入完畢。因此,通過這個方法判斷頁面是否載入完成比使用上文提到的onPageFinished方法更準確。同時,由於這個方法在回撥中會不斷獲得最新的載入進度,因此我們可以藉助這個方法實現自定義的載入進度條。

這裡給出一個簡單的思路:在WebView的上方新增一個ProgressBar控制元件,並預設隱藏。在onPageStarted方法中顯示ProgressBar,並在onProgressChanged方法回撥時更新ProgressBar的進度值。當onProgressChanged方法中的newProgress達到100時,就隱藏這個ProgressBar。需要注意,為了在頁面加載出錯時也能正確隱藏進度條,也應該在onReceivedError方法中隱藏ProgressBar。

獲取網頁標題

//title:當前網頁的標題
public void onReceivedTitle(WebView view, String title)

獲取網頁圖示

//icon:當前網頁的圖示
public void onReceivedIcon(WebView view, Bitmap icon)

網頁彈窗

//message:alert彈出視窗中的提示資訊(提示或警告資訊對話方塊,僅一個確認按鈕)
//result:向網頁中的Javascript程式碼反饋本次操作結果(result.confirm代表點選了確定按鈕,result.cancel代表點選了取消按鈕)
public boolean onJsAlert(WebView view, String url, String message,JsResult result)

///message:confirm彈出視窗中的提示資訊(確認對話方塊,有確認、取消兩個按鈕)
//result:向網頁中的Javascript程式碼反饋本次操作結果(result.confirm代表點選了確定按鈕,result.cancel代表點選了取消按鈕)
public boolean onJsConfirm(WebView view, String url, String message,JsResult result)

//message:prompt彈出視窗中的提示資訊(輸入資訊對話方塊,有一個輸入框,還有確認、取消兩個按鈕)
//defaultValue:輸入框中的預設資訊
//result:向網頁中的Javascript程式碼反饋本次操作結果(result.confirm代表點選了確定按鈕,result.cancel代表點選了取消按鈕)
public boolean onJsPrompt(WebView view, String url, String message,String defaultValue, JsPromptResult result)

當網頁中的Javascript程式碼彈出alert、confirm、prompt三種類型的彈窗時,會分別觸發以上三種方法。在這些回撥方法中,我們可以實現符合應用風格的對話方塊(通過AlertDialog實現),這可以給使用者更棒的視覺體驗。需要注意,這三個方法都有一個boolean返回值。如果返回true,代表Android應用已經對彈窗進行了處理,Javascript程式碼不必再彈出視窗了;反之,代表本次彈窗請求未被處理,網頁將按照預設效果彈出視窗。

下面給出一個簡單的處理方案,可以作為參考:

@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
    new AlertDialog.Builder(MainActivity.this)
            .setTitle("JsAlert")
            .setMessage(message)
            .setPositiveButton("確定", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    result.confirm();
                }
            })
            .setCancelable(false)
            .show();
    return true;
}

@Override
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
    new AlertDialog.Builder(MainActivity.this)
            .setTitle("JsConfirm")
            .setMessage(message)
            .setPositiveButton("確定", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    result.confirm();
                }
            })
            .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    result.cancel();
                }
            })
            .setCancelable(false)
            .show();
    return true;
}

@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
    final EditText editText=new EditText(MainActivity.this);
    editText.setText("預設資料");//設定預設資料
    new AlertDialog.Builder(MainActivity.this)
            .setTitle("JsPromt")
            .setView(editText)//為彈出視窗設定輸入框
            .setPositiveButton("確定", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    result.confirm(editText.getText().toString());//向Javascript傳遞輸入值
                }
            })
            .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    result.cancel();
                }
            })
            .setCancelable(false)
            .show();
    return true;
}

WebSettings

除了上文提到的WebViewClient和WebChromeClient外,WebSettings也是一個非常重要的類。通過WebSettings,我們可以對WebView進行多種配置和管理。WebSettings的獲取方式如下:

WebSettings webSettings=webView.getSettings();

支援Javascript

webSettings.setJavaScriptEnabled(true);

預設情況下,WebView是不支援Javascript的,需要呼叫setJavaScriptEnabled方法並傳入true才能啟用Javascript功能。當然,如果想禁用Javascript,只要傳入false作為引數就行了。

需要注意,通過這種方式支援Javascript在Android 4.2(API 17)以下存在嚴重的安全隱患。如果需要支援Android 4.2以下的系統,請參考下文的[4.與JS的互動方式]部分。

設定頁面自適應螢幕

webSettings.setUseWideViewPort(true);
webSettings.setLoadWithOverviewMode(true);

進行上述配置後,如果網頁針對移動裝置進行了自適應處理,那麼頁面將縮放到最合適的大小,通常這將帶來更好的視覺效果。

支援縮放功能

webSettings.setSupportZoom(true);//啟用縮放功能
webSettings.setBuiltInZoomControls(true);//使用WebView內建的縮放功能
webSettings.setDisplayZoomControls(false);//隱藏螢幕中的虛擬縮放按鈕

進行上述配置後,就可以通過多指的捏合操作對網頁執行縮放了。另外提一下,之所以呼叫setDisplayZoomControls(false)隱藏虛擬縮放按鈕,主要是為了美觀。因為可以使用多指觸控執行縮放操作,就沒必要單獨提供按鈕了(外觀比較醜)。當然,如果有必要顯示按鈕,傳入true即可,並不影響最終的縮放功能。

但是要注意,setDisplayZoomControls(true)在某些系統版本中可能會導致應用出現意外崩潰,具體細節請參考下文[5.常見問題#setDisplayZoomControls(true)引起的崩潰問題]部分。

快取模式

webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);//設定快取模式為LOAD_DEFAULT

WebView是原生支援快取的,我們只需要設定合適的快取模式即可。setCacheMode方法需要一個int型別的引數,系統提供了四種可選值,區別如下:

  1. LOAD_DEFAULT:預設值。當存在快取且未過期時載入快取資料,否則通過網路載入資料。
  2. LOAD_NO_CACHE:不使用快取。僅通過網路載入資料。
  3. LOAD_CACHE_ONLY:只使用快取。僅載入快取資料,不通過網路載入資料。
  4. LOAD_CACHE_ELSE_NETWORK:只要存在快取,不管是夠過期都載入快取資料,否則通過網路極載入資料。

定位設定

webSettings.setGeolocationEnabled(true);//允許網頁執行定位操作

如果要禁用網頁的定位功能,傳入false作為引數即可。需要注意,這個方法只是允許網頁執行定位操作,但是最終定位操作的實現還是會委託給Android應用處理。因此,為了保證定位功能正常執行,需要滿足以下兩點:

  1. Android應用需要獲取定位許可權。需要在AndroidManifest檔案中宣告android.Manifest.permission.ACCESS_COARSE_LOCATION和android.Manifest.permission.ACCESS_FINE_LOCATION兩個許可權。
  2. 需要為WebView設定WebChromeClient,並重寫WebChromeClient的onGeolocationPermissionsShowPrompt方法。這個方法會在網頁中的Javascript程式碼執行定位操作時觸發。需要注意,Android6.0及以上引入了執行時許可權的概念。定位屬於危險許可權,需要在使用時手動獲取。因此我們可以在這個回撥方法中彈出一個請求定位的提示對話方塊(AlertDialog),在使用者選擇確定後獲得相應許可權。

如果不瞭解執行時許可權的概念,可以參考這篇部落格:

其他設定

//允許自動載入圖片(預設值為true)
webSettings.setLoadsImagesAutomatically(true);

//設定預設文字編碼(預設值為UTF-8)
webSettings.setDefaultTextEncodingName("UTF-8");

4.與JS的互動方式

如何安全實現Android程式碼與Javascript程式碼的互動一直是學習WebView的重點和難點。在這個部分,我們將學會如何使用合理的方式規避低版本下的JS安全漏洞問題。

Android呼叫JS程式碼

loadUrl
使用loadUrl方法就可以簡單地非同步執行JS程式碼,下面給出一個簡單的例子:

webView.loadUrl("javascript:alert('從Android呼叫Js的alert方法')");

這種方式使用起來很簡單,只要按照正確的格式傳入一個字串即可。格式:(javascript:JS方法名)。需要注意,被呼叫的方法需要已在當前頁面定義或者已經引入當前頁面(JS系統方法也行),並且使用這種方式將會重新整理當前頁面。缺點在於,使用這種方式無法獲取JS方法的返回值。

需要注意,只有在當前網頁的onPageFinished方法執行後,Javascript程式碼才算是載入完畢,這時使用loadUrl呼叫JS程式碼才會生效。

evaluateJavascript
在Android 4.4(API 19)中,新增了一個方法evaluateJavascript。通過這個方法就可以高效率地非同步執行JS程式碼。方法原型如下:

//script:需要執行的JS程式碼(格式和loadUrl一致)
//resultCallback:用於提供JS方法的返回值
public void evaluateJavascript(String script, ValueCallback<String> resultCallback)

這個方法的效率比loadUrl更高,並且不會重新整理當前頁面。需要注意,這個方法只能在UI執行緒中呼叫,最終resultCallback的回撥方法也會在UI執行緒中執行。

混合使用
為了在相容低版本的情況下達到最高的執行效率,我們往往需要混合使用loadUrl和evaluateJavascript方法。下面給出一個簡單的參考示例:

...
String jsStr="javascript:alert('從Android呼叫Js的alert方法')";
//根據當前系統版本選擇最合適的載入方式
if(Build.VERSION.SDK_INT<19){
    webView.loadUrl(jsStr);
}else{
    webView.evaluateJavascript(jsStr, new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            if(TextUtils.isEmpty(value)){
                Toast.makeText(MainActivity.this,"返回值為空",Toast.LENGTH_SHORT).show();
            }else{
                Toast.makeText(MainActivity.this,"返回值"+value,Toast.LENGTH_SHORT).show();
            }
        }
    });
}
...

JS呼叫Android程式碼

利用WebView的addJavascriptInterface方法

WebView提供了addJavascriptInterface方法實現Javascript呼叫Android程式碼。該方法如下:

//object:定義在Android中的物件,包含Javascript需要呼叫的方法
//name:object在Javascript中的對映名稱
public void addJavascriptInterface(Object object, String name)

首先,我們需要定義一個充當對映的類,下面給出一個簡單的例子:

//Android呼叫JS需要使用的對映物件
class JsBridge{
    @JavascriptInterface
    public String testJsToAndroid(String msg){
        return "[從Android返回]"+msg;
    }
}

需要注意,被JS呼叫的方法需要使用@JavascriptInterface註解。

然後,我們需要呼叫addJavascriptInterface方法建立JsBridge的對映關係,程式碼如下:

webView.addJavascriptInterface(new JsBridge(),"jsBridge");

在這個方法中,我們例項化了一個JsBridge物件,並將”jsBridge”作為這個物件的對映名稱。在Javascript程式碼中,可以直接使用這個對映名稱呼叫JsBridge中的方法。

//Javascript程式碼
function js_normal() {
    var msg=jsBridge.testJsToAndroid("使用原生方式實現Js呼叫Android方法");
    alert("輸出:"+msg);
}

可以看到,通過這種方式,我們還可以方便地獲取到Android方法的返回值。然而,這種方式在Android 4.2(API 17)以下存在嚴重的安全隱患,這也是接下來要介紹的兩種方式能夠存在的原因。

利用WebViewClient的shouldOverrideUrlLoading方法攔截url

上文說過,WebViewClient的shouldOverrideUrlLoading方法會在發生url跳轉時觸發。由此,我們只要規定好呼叫Android方法的url格式,並在需要執行方法時載入相應的url即可。下面提供一個簡單的示例:

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    Uri uri= Uri.parse(url);
    if("js".equals(uri.getScheme())){//先判斷scheme
        if("jsBridge".equals(uri.getAuthority())){//隨後判斷authority
            String msg=uri.getQueryParameter("msg");//獲取來自js的引數
            msg="[從Android返回]"+msg;
            //注意要在目標字串的左右新增單引號,這才代表引數是作為字串傳入JS中的方法
            webView.loadUrl("javascript:get_value_from_app('"+msg+"')");//將新的值返回js
            return true;//返回true代表本次url請求處理完畢
        }
    }
    return false;
}
//Javascript程式碼
function js_url() {
    document.location="js://jsBridge?msg=使用攔截Url的方式實現Js呼叫Android方法,這樣可以避免Android4.2以下的漏洞";
}

//接收來自Android的返回值
function get_value_from_app(value){
    alert("輸出:"+value);
}

在這個例子中,我們規定呼叫Android本地方法的url格式如下:(js://jsBridge?msg=xxx)。因此,我們在shouldOverrideUrlLoading方法中先將String型別的url解析為Uri物件,並判斷它的scheme是否為js,authority是否為jsBridge,然後使用Uri的getQueryParameter方法獲取到了msg引數。當然,在這個例子中並沒有執行任何Android本地方法,實際開發時應該根據業務需求呼叫相應的本地方法。

需要注意,使用這種方式並不能直接向Javascript程式碼傳遞Android本地方法的返回值。如果確實有傳遞返回值的需求,就應該像這個例子一樣,在Javascript中定義一個傳值的方法(在本例中是get_value_from_app)。當Android方法執行完畢後,呼叫JS中相應的傳值方法即可。但是一個容易出錯的地方在於,如果傳遞的返回值是字串,那麼就要在字串的左右新增單引號,這才代表引數是作為字串傳入JS方法。

利用WebChromeClient的onJsPrompt方法攔截message
和第二種方式類似,除了攔截url,還可以通過WebChromeClient的onJsPrompt方法攔截message實現同樣的功能。我們只要規定好呼叫Android方法的message格式,並在需要執行Android方法時呼叫JS的prompt方法並傳入相應的message即可。下面提供一個簡單的示例:

@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
    Uri uri=Uri.parse(message);
    //處理js和android互動
    if("js".equals(uri.getScheme())&&"jsBridge".equals(uri.getAuthority())){
        String msg="[來自Android返回]"+uri.getQueryParameter("msg");
        result.confirm(msg);
        return true;
    }
...
}
//Javascript程式碼
function js_prompt() {
    var msg = prompt("js://jsBridge?msg=通過在WebChromeClient中的onJsPrompt中攔截url實現Js呼叫Android方法,這樣可以避免Android4.2以下的漏洞");
    alert(msg);
}

在這個例子中,我們規定呼叫Android本地方法的message格式如下:(js://jsBridge?msg=xxx)。因此,我們在shouldOverrideUrlLoading方法中先將message解析為Uri物件,並判斷它的scheme是否為js,authority是否為jsBridge,然後使用Uri的getQueryParameter方法獲取到了msg引數。實際開發時只要根據業務需求呼叫相應的本地方法即可。

這種方式和第二種方式相比的顯著不同之處在於,可以通過JsPromptResult的confirm方法直接向Javascript程式碼傳遞Android方法的返回值,這無疑可以為開發帶來極大的便利。

可能存在的安全隱患
我們在前文介紹了三種Javascript呼叫Android方法的方式。可以明顯看出,第一種方式是最簡單的,因為這是Android官方提供的解決方案。問題在於,Android 4.2(API 17)以下使用這種方式存在著任意程式碼執行漏洞。簡單來說,在Android 4.2以前,只要為JS提供了對映物件,JS就可以呼叫這個物件的所有方法。通過呼叫這個物件的getClass方法,可以獲得一個Class型別的物件。然後,呼叫Class物件的forName方法可以載入Runtime類,之後就可以藉助Runtime執行本地命令,肆意獲取裝置中的敏感資訊。

在Android 4.2時,Android官方規定對映類必須為其中提供給JS的方法新增@JavascriptInterface註解,JS也只能呼叫對映物件中有@JavascriptInterface註解的方法。由於getClass是Object類中的方法,不存在該註解,因此JS無法呼叫這個方法,前面提到的安全漏洞也就不存在了。

當然,現在我們還不能徹底放棄Android 4.2以下的系統。因此為了保證相容性,推薦在Android 4.2以下使用第三種方式(傳遞返回值更方便),在Android 4.2及以上使用第一種方式。

此外,由於Android系統為WebView注入了searchBoxJavaBridge_、accessibility、accessibilityTraversal三個對映物件,在Android 4.2以下攻擊者同樣可以利用它們執行任意程式碼。因此,我們需要手動移除這三個物件。

webView.removeJavascriptInterface("searchBoxJavaBridge_")
webView.removeJavascriptInterface("accessibility");
webView.removeJavascriptInterface("accessibilityTraversal")

5.常見問題

loadData載入中文資料出現亂碼

問題描述:使用loadData方法載入含有中文的資料時,中文顯示為亂碼。

解決方案:

  1. 使用loadDataWithBaseURL方法代替loadData載入資料,不會出現亂碼問題。
  2. 為loadData的mimeType引數傳入“text/html;charset=UTF-8”,也可以解決亂碼問題。

密碼明文儲存問題

問題描述:在Android 4.3(API 18)以前,使用者在WebView載入的網頁中輸入密碼後,系統會彈出對話方塊詢問使用者是否需要儲存密碼。如果使用者選擇儲存,那麼密碼將會以明文的形式儲存在本地,顯然這是一個巨大的安全隱患。

解決方案:

WebView是否儲存密碼是由WebSettings的setSavePassword方法決定的。因此,我們只要呼叫這個方法並傳入false,就可以避免明文儲存的安全問題了。

webSettings.setSavePassword(false);

在Android 4.3及以上的版本,setSavePassword方法已經被棄用,WebView也不會預設儲存密碼,因此不再需要進行修復。

WeView出現OOM影響主程序

問題描述:由於WebView預設執行在應用程序中,如果WebView載入的資料過大(例如載入大圖片),就可能導致OOM問題,從而影響應用主程序。

解決方案:為了避免WebView影響主程序,可以嘗試將WebView所在的Activity執行在獨立程序中。這樣即使WebView出現了OOM問題,應用主程序也不會受到影響。具體做法也很簡單,只要在AndroidManifest檔案中為相應的Activity設定process屬性即可。

<activity android:name=".MainActivity"
    android:process=":remote">
</activity>

在這個例子中我們為Activity設定了process屬性,意思就是讓這個Activity執行在名為:remote的私有程序中。

需要注意,這種方式可能會有程序通訊方面的問題,因此需要根據實際情況決定是否需要使用。

WebView後臺耗電問題

問題描述:在某些情況下,即使Activity已經退出,WebView依舊佔據著記憶體空間,這會導致裝置耗電量增加。

解決方案:在上文提到過將WebView執行在獨立程序中,然後只要在Activity的onDestroy方法中呼叫System.exit(0)退出虛擬機器,就可以避免WebView繼續佔據記憶體空間。

@Override
protected void onDestroy() {
    super.onDestroy();
    System.exit(0);
}

記憶體洩漏

問題描述:如果在XML中定義WebView,那麼系統將把當前的Activity作為Context去例項化WebView物件。由於WebView保持著對Activity的引用,如果在Activity結束時WebView還未進入銷燬狀態,將導致Activity無法被系統回收,進而造成記憶體洩漏。

解決方案:

首先,避免在XML中直接定義WebView,而是在需要的時候動態建立WebView並加入合適的ViewGroup中。

//通過程式碼建立
FrameLayout parentLayout=findViewById(R.id.container);
webView=new WebView(getApplicationContext());//使用應用級別的context,避免對Activity的引用
FrameLayout.LayoutParams layoutParams=new FrameLayout.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
parentLayout.addView(webView,layoutParams);

其次,在Activity的onDestroy方法中,先讓WebView停止載入,然後載入空資料,再把它從ViewGroup中移除。最