WebView 的使用

現在 Android 應用中都會多多少少嵌入一些 H5 頁面,在 Android SDK 中有一個名為 WebView 的組件,它是一個 webkit 內核的高性能瀏覽器,在 Android 應用中的 H5 頁面用它展示是非常合適的。本篇文章就介紹一下 WebView 相關的一些知識,相關代碼放在 GitHub 的 WebViewPractice 的工程里。

本篇文章主要介紹以下幾點:

  1. WebView 的簡單使用
  2. WebViewClient
  3. WebChromeClient
  4. WebViewClient 和 WebChromeClient 的區別
  5. Js 和 Java 代碼的交互
  6. WebView 的緩存
  7. WebView 引起的內存泄露

1. WebView 的簡單使用

如果只是想簡單地使用 WebView,通過下面幾步即可展示一般功能需求的 H5 頁面。

  • AndroidManifest.xml 申請網絡權限

  • 在 XML 布局文件中添加 WebView 控件,并在 Java 代碼中得到該 WebView 的對象

      <!-- activity_main.xml -->
      <?xml version="1.0" encoding="utf-8"?>
      <LinearLayout
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:id="@+id/ll_webview_root"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:orientation="vertical">
    
          ......
    
          <WebView
              android:id="@+id/webview"
              android:layout_width="match_parent"
              android:layout_height="match_parent"/>
    
          ......
    
      </LinearLayout>
    
        // MainActivity.java
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            WebView webView = (WebView) findViewById(R.id.webview);
            webView.getSettings().setJavaScriptEnabled(true);
            webView.setWebViewClient(new WebViewClient() {
    
                @Override
                public boolean shouldOverrideUrlLoading(WebView view, String url) {
                    view.loadUrl(url);
                    return true;
                }
    
                @TargetApi(21)
                @Override
                public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                    view.loadUrl(request.getUrl().toString());
                    return true;
                }
            });
            webView.loadUrl("https://ke.youdao.com");
        }
    
  • 如果訪問的頁面中含有 JavaScript 的代碼,則需要設置 WebView 支持 Javascript。

        webView.getSettings().setJavaScriptEnabled(true);
    
  • 如果頁面中含有連接,點擊鏈接如果想繼續在當前瀏覽器中瀏覽網頁,則需要重寫 WebView 的 WebViewClient 對象。

    webView.setWebViewClient(new WebViewClient() {
    
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            view.loadUrl(url);
            return true;
        }
    
        @TargetApi(21)
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
            view.loadUrl(request.getUrl().toString());
            return true;
        }
    });
    

    如果不重寫 WebView 的對象 WebViewClient,點擊頁面中的連接則會在手機系統自帶的瀏覽器中打開新的鏈接。

2. WebViewClient

在第一節中已經簡單使用過 WebViewClient 這個類,還有其他一些常用的方法可以重寫并使用,其含義如下所示:

webView.setWebViewClient(new WebViewClient() {

    // 給應用接管處理某些 url 請求的機會,返回 true 則攔截該請求,返回 false 不攔截,已經被廢棄
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        view.loadUrl(url);
        return true;
    }

    // 給應用接管處理某些 url 請求的機會,返回 true 則攔截該請求,返回 false 不攔截,是上面方法的替代方法
    // WebResourceRequest 中含有請求的 url,請求方法和請求頭等信息
    @TargetApi(21)
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        view.loadUrl(request.getUrl().toString());
        return true;
    }

    // 頁面開始加載時回調
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        super.onPageStarted(view, url, favicon);
    }

    // 頁面結束加載時回調
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
    }

    // 在加載 url 對應的資源時會回調此方法
    @Override
    public void onLoadResource(WebView view, String url) {
        super.onLoadResource(view, url);
    }

    // 在加載 url 對應資源時會回調此方法,不同的是,可以通過返回值控制加載的數據。此方法已被廢棄
    // 若返回 null,WebView 會正常加載該資源
    // 若返回 WebResourceResponse 類型的對象,則 WebView 會使用該對象
    // 需要注意的是,此方法不在 UI 線程中被調用
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
        return super.shouldInterceptRequest(view, url);
    }

    // 是上面方法的替代方法,使用方法和上面方法一致
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        return super.shouldInterceptRequest(view, request);
    }

    // 加載資源出錯時會被回調的方法
    @Override
    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
        super.onReceivedError(view, request, error);
    }

    // 加載資源時 HTTP 請求出錯會回調此方法
    @Override
    public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
        super.onReceivedHttpError(view, request, errorResponse);
    }

    // 請求 HTTPS 資源出錯時會回調此方法
    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        super.onReceivedSslError(view, handler, error);
    }

    // 攔截瀏覽器中的按鍵事件
    // 若返回 true,則攔截按鍵事件
    // 若返回 false,則由 WebView 處理該事件
    @Override
    public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
        return super.shouldOverrideKeyEvent(view, event);
    }

    // 當頁面的縮放比例發生變化時會回調此方法
    @Override
    public void onScaleChanged(WebView view, float oldScale, float newScale) {
        super.onScaleChanged(view, oldScale, newScale);
    }
});

3. WebChromeClient

同 WebViewClient 一樣,通過為 WebView 設置 WebChromeClient 對象,并重寫其中的一些方法可以對 WebView 的一些行為進行控制。

webView.setWebChromeClient(new WebChromeClient() {

     // 網頁的加載進度
     @Override
     public void onProgressChanged(WebView view, int newProgress) {
         L.i("onProgressChanged " + newProgress);
         super.onProgressChanged(view, newProgress);
     }

     // 接收到網頁的 title
     @Override
     public void onReceivedTitle(WebView view, String title) {
         L.i("onReceivedTitle " + title);
         super.onReceivedTitle(view, title);
     }

     // 接收到網頁的 icon
     @Override
     public void onReceivedIcon(WebView view, Bitmap icon) {
         super.onReceivedIcon(view, icon);
     }

     @Override
     public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) {
         super.onReceivedTouchIconUrl(view, url, precomposed);
     }

     // 當 H5 頁面中點擊播放的 flash video 的全屏按鈕時,會調用這個方法
     @Override
     public void onShowCustomView(View view, CustomViewCallback callback) {
         super.onShowCustomView(view, callback);
     }

     // 與 onShowCustomView() 對應的取消全屏時會調用的方法
     @Override
     public void onHideCustomView() {
         super.onHideCustomView();
     }

     @Override
     public void onShowCustomView(View view, int requestedOrientation, CustomViewCallback callback) {
         super.onShowCustomView(view, requestedOrientation, callback);
     }

     // http://www.cnblogs.com/ufreedom/p/4229590.html
     // 當創建新的 Window 時會調用此方法
     @Override
     public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
         return super.onCreateWindow(view, isDialog, isUserGesture, resultMsg);
     }

     // 和 onCreateWindow() 方法對應的,關閉 Window
     @Override
     public void onCloseWindow(WebView window) {
         super.onCloseWindow(window);
     }

     // 當 WebView 獲取焦點時會調用此方法
     @Override
     public void onRequestFocus(WebView view) {
         super.onRequestFocus(view);
     }

     // 在 Js 代碼中彈出 Alert 窗口時會調用此方法
     @Override
     public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
         return super.onJsAlert(view, url, message, result);
     }

     // 在 Js 代碼中彈出 Confirm 窗口時會調用此方法
     @Override
     public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
         return super.onJsConfirm(view, url, message, result);
     }

     // 在 Js 代碼中彈出 Prompt 窗口時會調用此方法
     @Override
     public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
         return super.onJsPrompt(view, url, message, defaultValue, result);
     }

     // 在 Js 加載之前會調用此方法
     @Override
     public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
         return super.onJsBeforeUnload(view, url, message, result);
     }

     // 打印 Js 中的日志 console 信息,被廢棄
     @Override
     public void onConsoleMessage(String message, int lineNumber, String sourceID) {
         super.onConsoleMessage(message, lineNumber, sourceID);
     }

     // 打印 Js 中的日志 console 信息,被廢棄
     @Override
     public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
         return super.onConsoleMessage(consoleMessage);
     }
 });

4. WebViewClient 和 WebChromeClient 的區別

WebViewClient 主要幫助 WebView 處理一些網絡請求方面的行為和操作,比如:各種資源的請求、請求資源出現錯誤、HTTPS請求出現錯誤的通知等。

WebChromeClient 主要幫助 WebView 處理一些 Javascript 相關的一些細節,比如:各種對話框的彈出、Js 中日志信息的打印、Window 的創建和關閉、以及 title 和 icon 的接收等。

5. Js 和 Java 代碼的交互

Java 代碼和 Js 代碼額相互調用已經老生常談了,現在也有一些很優秀的開源框架,使用起來非常的方便,但是其原理都是一樣的,下面就介紹一下 Js 代碼和 Java 代碼是如何進行交互的。

5.1 Js 代碼調用 Java 代碼

private class InnerClass {

    private Context mContext = null;

    public InnerClass(Context context) {
        mContext = context;
    }

    @JavascriptInterface
    public void toastMessage(String msg) {
        Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
    }
}
public class JSBridgeActivity extends AppCompatActivity {

    private WebView webView = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_jsbridge);
        initWebView();
    }

    private void initWebView() {
        webView = (WebView) findViewById(R.id.webView);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.addJavascriptInterface(new InnerClass(JSBridgeActivity.this), "innerClass");
        webView.loadUrl("file:///android_asset/demo.html");
        webView.setWebChromeClient(new WebChromeClient());
   }
}

  • 有名為 InnerClass 的類,其中的方法 toastMessage(String msg)@JavascriptInterface 注解所修飾
  • 設置 WebView 對象支持 JavaScript,并且調用 addJavascriptInterface(Object object, String name) 方法,將 InnerClass 的對象傳入 WebView 對象中
  • 在 WebView 加載頁面的 Js 代碼中,即可通過 name 調用 InnerClass 對象的 toastMessage(String msg) 方法,如下所示:
function testPrompt(){
      window.innerClass.toastMessage("jsbridge");
}

5.2 Java 代碼調用 Js 代碼

假如有如下的 Js 方法,如下所示:

function testConfirm(){
    var temp = "temp";
    confirm(temp);
}

在 Java 中可以通過如下方法調用該 Js 方法:

webView.loadUrl("javascript:testConfirm()");

注意

  • 在 Java 代碼中調用 Js 代碼彈出 alertconfirmprompt 對話框的時候,需要給 WebView 設置 WebChromeClient 對象才可以正常的彈出對話框,否則不會有效果。
  • Java 調用 Js 的代碼需要在主線程中調用才會生效
  • 在 Activity 的 onCreate() 方法中直接調用 WebView.loadUrl() 方法是不會生效的,需要在WebView 加載完成之后再調用 Js 的代碼,才會生效。

6. WebView 的緩存

6.1 CacheMode 緩存模式

通過 WebView 的 WebSettings 對象可以設置 WebView 的 CacheMode 緩存策略,共有如下幾個值可供設置:

  • LOAD_DEFAULT:默認設置,如果有本地緩存,且緩存有效未過期,則直接使用本地緩存,否則加載網絡數據
  • LOAD_NORMAL:已被廢棄,同 LOAD_DEFAULT 效果一樣
  • LOAD_CACHE_ELSE_NETWORK:如果存在緩存時,不論緩存是否過期,都使用緩存;若緩存不存在,則從網絡獲取數據
  • LOAD_NO_CACHE:不使用緩存,只從網絡獲取數據
  • LOAD_CACHE_ONLY:不從網絡獲取數據,只使用緩存數據

6.2 WebView 緩存路徑

我測試用的手機是小米 note,Android 系統是 4.4,WebView 緩存的文件存在于 /data/data/${package_name}/app_webview/cache/ 路徑下

WebViewCache.png

6.3 H5 緩存機制

H5 的緩存機制共有 5 種緩存機制,分別是:

  • 瀏覽器緩存機制:主要通過 HTTP 的協議頭中的 Cache-Control(或 Expires)和 Last-Modified(或 Etag)字段控制控制文件的緩存。
  • Application Cache(AppCache)緩存機制:AppCache 緩存是對瀏覽器緩存的補充,不可替代。
  • Dom Storage 緩存機制:Dom Storage 提供 5MB 大小的緩存空間,以鍵值對的形式存取文件。
  • Web SQL Database 緩存機制:H5 提供了基于 SQL 的數據庫存儲機制,用于存儲一些結構化的數據。
  • Indexed Database 緩存機制:不同于 SQL 數據庫,屬于 NoSQL 數據庫。Indexed Database 存儲的數據是以鍵值對的形式存儲數據的。

共有這 5 種緩存機制,值得詳細說明,在此就不再做過多的說明,爭取后續單獨寫一篇博客介紹緩存機制。

7. WebView 引起的內存泄露

Activity 中使用 WebView 有可能會出現內存泄露的情況,Android 5.1 WebView內存泄漏分析【Android】 WebView內存泄漏優化之路 這兩篇文章已經介紹的很詳細。

Android 5.1 中,如果 WebViewdestroy() 方法在 onDetachedFromWindow() 方法之前被執行,則會出現 WebView 持有 Activity 的引用,從而導致內存泄露。

為了避免 WebView 引起內存泄露,有兩個比較有效的辦法:

  • 使用代碼 New 一個 WebView 對象,而不是在 xml 文件中靜態寫入一個 WebView
  • ActivityonDestory() 方法中,主動的將 WebView 從父容器中移除并銷毀該 WebView 對象,如下所示:
@Override
protected void onDestroy() {
    super.onDestroy();
    if (webView != null) {
        ViewParent viewParent = webView.getParent();
        if (viewParent != null) {
            ((ViewGroup) viewParent).removeView(webView);
        }
        webView.getSettings().setJavaScriptEnabled(false);
        webView.clearHistory();
        webView.clearView();
        webView.removeAllViews();
        webView.destroy();
    }
}

本文中涉及到的自定義類的源碼都在 Github 上的 WebViewPractice 工程中。


參考資料:

Android 5.1 WebView內存泄漏分析 -- 永遠即等待

Android:手把手教你構建 WebView 的緩存機制 & 資源預加載方案 -- Carson_Ho

【Android】 WebView內存泄漏優化之路 -- kaka's blog

Android WebView 性能輕量優化

WebView緩存原理分析和應用 -- UncleChen的博客

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內容