WebView詳解與簡單實現Android與H5互調

風靈使發表於2018-04-02

為什麼要學習Android與H5互調?

微信,QQ空間等大量軟體都內嵌了H5,不得不說是一種趨勢。Android與H5互調可以讓我們的實現混合開發,至於混合開發就是在一個App中內嵌一個輕量級的瀏覽器,一部分原生的功能改為Html 5來開發。
優勢:使用H5實現的功能能夠在不升級App的情況下動態更新,而且可以在Android或iOS的App上同時執行,節約了成本,提高了開發效率。
原理:其實就是Java程式碼和JavaScript之間的呼叫。

WebView簡介

要實現Android與H5互調,WebView是一個很重要的控制元件,WebView可以很好地幫助我們展示html頁面,所以有必要先了解一下WebView。

一丶WebView常用方法

  • loadUrl

    載入介面,其次還有LoadData和LoadDataWithBase方法

    //載入assets目錄下的test.html檔案
    webView.loadUrl("file:///android_asset/test.html");
    //載入網路資源(注意要加上網路許可權)
    webView.loadUrl("http://blog.csdn.net");
  • setWebViewClient(如果使用者設定了WebViewClient,則在點選新的連結以後就不會跳轉到系統瀏覽器了,而是在本WebView中顯示。注意:並不需要覆蓋shouldOverrideUrlLoading 方法,同樣可以實現所有的連結都在 WebView 中開啟。)

WebViewClient主要用來輔助WebView處理各種通知、請求等事件,通過setWebViewClient方法設定。以下是它的幾種常見用法:

1.實現對網頁中超連結的攔截(比如如果是極客導航的主頁,則直接攔截轉到百度主頁):
當點選頁面中的連結後,會在WebView載入URL前回撥shouldOverrideUrlLoading(WebView view, String url)方法,一般點選一個連結此方法呼叫一次。

        webView.setWebViewClient(new WebViewClient(){
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            if("http://www.jikedaohang.com/".equals(url))                   {
                view.loadUrl("https://www.baidu.com/");
            }

                        return true;
                    }
                });

關於shouldOverrideUrlLoading返回值的誤區:網上很多解釋是return true代表在本WebView中開啟連結,return false代表呼叫系統瀏覽器開啟連結。其實只要設定了WebViewClient,則就不會呼叫系統瀏覽器。

那麼shouldOverrideUrlLoading的返回值到底代表什麼呢?return true,則在開啟新的url時WebView就不會再載入這個url了,所有處理都需要在WebView中操作,包含載入;return false,則系統就認為上層沒有做處理,接下來還是會繼續載入這個url的;預設return false。具體的區別展示如下:

載入百度主頁,設定WebViewClient後,重寫shouldOverrideUrlLoading(WebView view, String url)方法,第一張是返回false的截圖(點選後正常跳轉),第二章是返回true的截圖(點選無反應,如果希望能夠跳轉,則需要我們自己進行處理):
這裡寫圖片描述

這裡寫圖片描述

還有一點需要注意的是,如果我們攔截了某個url,那麼return falsereturn true區別不大,所以一般建議 return false

2.載入網頁時替換某個資源(比如在載入一個網頁時,需要載入一個logo圖片,而我們想要替換這個logo圖片,用我們assets目錄下的一張圖片替代)

我們知道我們在載入一個網頁的同時也會載入js,css,圖片等資源,所以會多次呼叫shouldInterceptRequest方法,我們可以在shouldInterceptRequest中進行圖片替換。

注意:shouldInterceptRequest有兩個過載:

①public WebResourceResponse shouldInterceptRequest (WebView view, String url) 【已過時】

②public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request)

這兩種方法主要是第二個引數的不同,WebResourceRequest 將能夠獲取更多的資訊,提供了getUrl(),getMethod,getRequestHeaders等方法。這裡主要是為了展示效果,使用了第一種回撥方法。實現方法如下:

        mWebView.setWebViewClient(new WebViewClient(){
                    @Override
                    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
                        WebResourceResponse response = null;
                        if (url.contains("logo")) {
                            try {
                                InputStream logo = getAssets().open("logo.png");
                                response = new WebResourceResponse("image/png", "UTF-8", logo);
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                        return response;
                    }
                });

3.設定開始載入網頁、載入完成、載入錯誤時處理

        webView.setWebViewClient(new WebViewClient() {    
        
            @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);  
                // 網頁載入完成時處理  如:讓 載入對話方塊 消失  
                ...
            }  

            @Override  
            public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {  
                super.onReceivedError(view, errorCode, description, failingUrl);  
                // 載入網頁失敗時處理 如:提示失敗,或顯示新的介面
                ...
            }    
        });  

處理https請求,為WebView處理ssl證照設定WebView預設是不處理https請求的,需要在WebViewClient子類中重寫父類的onReceivedSslError函式

     webView.setWebViewClient(new WebViewClient() {    

            @Override  
            public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {  
                handler.proceed();  // 接受信任所有網站的證照  
                // handler.cancel();   // 預設操作 不處理  
                // handler.handleMessage(null);  // 可做其他處理  
            }   
        });   
  • setWebChromeClient

WebChromeClient主要用來輔助WebView處理Javascript的對話方塊、網站圖示、網站標題以及網頁載入進度等。通過WebViewsetWebChromeClient()方法設定。

1.顯示頁面載入進度在WebChromeClient子類中重寫父類的onProgressChanged函式,progress表示當前頁面載入的進度,為1至100的整數

        webView.setWebChromeClient(new WebChromeClient() {    

            public void onProgressChanged(WebView view, int progress) {    
                setTitle("頁面載入中,請稍候..." + progress + "%");    
                setProgress(progress * 100);    

                if (progress == 100) {    
                    //... 
                }    
            }    
        });  

2.加快HTML網頁載入完成速度(預設情況html程式碼下載到WebView後,webkit開始解析網頁各個節點,發現有外部樣式檔案或者外部指令碼檔案時,會非同步發起網路請求下載檔案,但如果在這之前也有解析到image節點,那勢必也會發起網路請求下載相應的圖片。在網路情況較差的情況下,過多的網路請求就會造成頻寬緊張,影響到css或js檔案載入完成的時間,造成頁面空白loading過久。解決的方法就是告訴WebView先不要自動載入圖片,等頁面finish後再發起圖片載入。)

      //1.首先在WebView初始化時新增如下程式碼
        if(Build.VERSION.SDK_INT >= 19) {  
        /*對系統API在19以上的版本作了相容。因為4.4以上系統在onPageFinished時再恢復圖片載入時,如果存在多張圖片引用的是相同的src時,會只有一個image標籤得到載入,因而對於這樣的系統我們就先直接載入。*/        webView.getSettings().setLoadsImagesAutomatically(true);  
            } else {  
                webView.getSettings().setLoadsImagesAutomatically(false);  
            }  

        //2.在WebView的WebViewClient子類中重寫onPageFinished()方法新增如下程式碼: 
         @Override  
        public void onPageFinished(WebView view, String url) {  
            if(!webView.getSettings().getLoadsImagesAutomatically()) {  
                webView.getSettings().setLoadsImagesAutomatically(true);  
            }  
        }  
  • setDownloadListener

通常webview渲染的介面中含有可以下載檔案的連結,點選該連結後,應該開始執行下載的操作並儲存檔案到本地中。

1.建立DownloadListener

    class MyDownloadListenter implements DownloadListener{
              @Override
              public void onDownloadStart(String url, String userAgent,String contentDisposition, String mimetype, long contentLength) {
                  //下載任務...,主要有兩種方式
                  //(1)自定義下載任務
                  //(2)呼叫系統的download的模組
                  Uri uri = Uri.parse(url);
                  Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                  startActivity(intent);
              }
        }

webview加入監聽

       webview.setDownloadListener(new MyDownloadListenter());
  • goBack()

返回上一瀏覽頁面,通過重寫onKeyDown方法實現點選返回鍵返回上一瀏覽頁面而非退出程式

    public boolean onKeyDown(int keyCode, KeyEvent event) {  
    //其中webView.canGoBack()在webView含有一個可後退的瀏覽記錄時返回true

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

二丶WebSettings配置

1.獲取WebSettings物件

        WebSettings webSettings = webView.getSettings();

2.常用設定方法

(1)支援js

      settings.setJavaScriptEnabled(true);

(2)設定快取方式,主要有以下幾種:

LOAD_CACHE_ONLY: 不使用網路,只讀取本地快取資料。
LOAD_DEFAULT: 根據cache-control決定是否從網路上取資料。
LOAD_CACHE_NORMAL: API level 17中已經廢棄, 從API level 11開始作用同LOAD_DEFAULT模式。
LOAD_NO_CACHE: 不使用快取,只從網路獲取資料。
LOAD_CACHE_ELSE_NETWORK:只要本地有,無論是否過期,或者no-cache,都使用快取中的資料。

settings.setCacheMode(WebSettings.LOAD_NO_CACHE);

(3)開啟DOM storage API功能(HTML5 提供的一種標準的介面,主要將鍵值對儲存在本地,在頁面載入完畢後可以通過 JavaScript 來操作這些資料。)

settings.setDomStorageEnabled(true);

(4)設定資料庫快取路徑

settings.setDatabasePath(cacheDirPath);

(5)設定Application Caches快取目錄

settings.setAppCachePath(cacheDirPath);

(6)設定預設編碼

settings.setDefaultTextEncodingName(“utf-8);

(7)將圖片調整到適合webview的大小

settings.setUseWideViewPort(false);

(8)支援縮放

settings.setSupportZoom(true);

(9)支援內容重新佈局

settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);

(10)多視窗

settings.supportMultipleWindows();

(11)設定可以訪問檔案

settings.setAllowFileAccess(true);

(12)當webview呼叫requestFocus時為webview設定節點

settings.setNeedInitialFocus(true);

(13)設定支援縮放

settings.setBuiltInZoomControls(true);

(14)支援通過JS開啟新視窗

settings.setJavaScriptCanOpenWindowsAutomatically(true);

(15)縮放至螢幕的大小

settings.setLoadWithOverviewMode(true);

(16)支援自動載入圖片

settings.setLoadsImagesAutomatically(true);

三丶WebViewClient 的回撥方法列表

WebViewClient主要用來輔助WebView處理各種通知、請求等事件,通過setWebViewClient方法設定。

(1)更新歷史記錄

    doUpdateVisitedHistory(WebView view, String url, boolean isReload)

(2)應用程式重新請求網頁資料

    onFormResubmission(WebView view, Message dontResend, Message resend)

(3)在載入頁面資源時會呼叫,每一個資源(比如圖片)的載入都會呼叫一次。

    onLoadResource(WebView view, String url)

(4)開始載入頁面呼叫,通常我們可以在這設定一個loading的頁面,告訴使用者程式在等待網路響應。

    onPageStarted(WebView view, String url, Bitmap favicon)

(5)在頁面載入結束時呼叫。同樣道理,我們知道一個頁面載入完成,於是我們可以關閉loading 條,切換程式動作。

    onPageFinished(WebView view, String url)

(6)報告錯誤資訊

    onReceivedError(WebView view, int errorCode, String description, String failingUrl)

(7)獲取返回資訊授權請求

    onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host,String realm)

(8)重寫此方法可以讓webview處理https請求。

    onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)

(9)WebView發生改變時呼叫

    onScaleChanged(WebView view, float oldScale, float newScale)

(10)Key事件未被載入時呼叫

    onUnhandledKeyEvent(WebView view, KeyEvent event)

(11)重寫此方法才能夠處理在瀏覽器中的按鍵事件。

    shouldOverrideKeyEvent(WebView view, KeyEvent event)

(12)在網頁跳轉時呼叫,這個函式我們可以做很多操作,比如我們讀取到某些特殊的URL,於是就可以不開啟地址,取消這個操作,進行預先定義的其他操作,這對一個程式是非常必要的。

    shouldOverrideUrlLoading(WebView view, String url)

(13)在載入某個網頁的資源的時候多次呼叫(已過時)

    shouldInterceptRequest(WebView view, String url)

(14)在載入某個網頁的資源的時候多次呼叫

    shouldInterceptRequest(WebView view, WebResourceRequest request)

注意:

shouldOverrideUrlLoading在網頁跳轉的時候呼叫,且一般每跳轉一次只呼叫一次。
shouldInterceptRequest只要是網頁載入的過程中均會呼叫,資源載入的時候都會回撥該方法,會多次呼叫。

##四丶WebChoromeClient的回撥方法列表

WebChromeClient主要用來輔助WebView處理Javascript的對話方塊、網站圖示、網站標題以及網頁載入進度等。通過WebViewsetWebChromeClient()方法設定。

(1)監聽網頁載入進度

    onProgressChanged(WebView view, int newProgress)

(2)監聽網頁標題 : 比如百度頁面的標題是“百度一下,你就知道”

    onReceivedTitle(WebView view, String title)

(3)監聽網頁圖示

    onReceivedIcon(WebView view, Bitmap icon)

Java和JavaScript互調

為方便展示,使用addJavascriptInterface方式實現與本地js互動(存在漏洞)。也可通過其他方式實現,比如攔截ur進行引數解析l等。

Java調JS

  • 首先是JS的一段程式碼:
function javaCallJs(arg){
         document.getElementById("content").innerHTML =
             ("歡迎:"+arg );
    }

然後是在java中呼叫JS中的方法

webView.loadUrl("javascript:javaCallJs("+"'"+name+"'"+")");

以上程式碼就是呼叫了JS中一個叫javaCallJs(arg)的方法,並傳入了一個name引數。(具體效果下面有展示)

JS調java

  • 配置Javascript介面
webView.addJavascriptInterface(new JSInterface (),"Android");
  • 實現Javascript介面類
class JSInterface {
    @JavascriptInterface
     public void showToast(String arg){
                   Toast.makeText(MainActivity.this,arg,Toast.LENGTH_SHORT).show();
     }
}
  • JS中呼叫java程式碼
<input type="button" value="點選Android被呼叫" onclick="window.Android.showToast('JS中傳來的引數')"/>

window.Android.showToast(‘JS中傳來的引數’)”中的”Android”即addJavascriptInterface()中指定的,並且JS向java傳遞了引數,型別為String。而showToast(String arg)會以Toast的形式彈出此引數。

java與JS互調程式碼示例

先看效果圖:

這裡寫圖片描述

程式碼非常簡單,並且加了註釋,直接看程式碼就可以了。

  • 首先是本地的JavaAndJavaScriptCall.html檔案,放在asstes目錄下
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    <script type="text/javascript">

    function javaCallJs(arg){
         document.getElementById("content").innerHTML =
             ("歡迎:"+arg );
    }

    </script>
</head>
<body>
    <div id="content"> 請在上方輸入您的使用者名稱</div>
    <input type="button" value="點選Android被呼叫" onclick="window.Android.showToast('JS中傳來的引數')"/>
</body>
</html>

javaCallJs是java呼叫JS的方法,showToast方法是JS呼叫java的方法

  • 接下來是佈局檔案,activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/ll_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="20dp"
        android:background="#000088">
        <EditText
            android:id="@+id/et_user"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:hint="輸入WebView中要顯示的使用者名稱"
            android:background="#008800"
            android:textSize="16sp"
            android:layout_weight="1"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="40dp"
            android:layout_marginRight="20dp"
            android:textSize="16sp"
            android:text="確定"
            android:onClick="click"/>
    </LinearLayout>

</LinearLayout>

很簡單,就是一個輸入框和一個確定按鈕,點選按鈕會呼叫JS中的方法。

  • MainActivity
package com.wangjian.webviewdemo;

import android.annotation.SuppressLint;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.JavascriptInterface;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private WebView webView;
    private LinearLayout ll_root;
    private EditText et_user;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ll_root = (LinearLayout) findViewById(R.id.ll_root);
        et_user = (EditText) findViewById(R.id.et_user);
        initWebView();
    }

    //初始化WebView

    private void initWebView() {
        //動態建立一個WebView物件並新增到LinearLayout中
        webView = new WebView(getApplication());
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        webView.setLayoutParams(params);
        ll_root.addView(webView);
        //不跳轉到其他瀏覽器
        webView.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);
                return true;
            }
        });
        WebSettings settings = webView.getSettings();
        //支援JS
        settings.setJavaScriptEnabled(true);
        //載入本地html檔案
        webView.loadUrl("file:///android_asset/JavaAndJavaScriptCall.html");
        webView.addJavascriptInterface(new JSInterface(),"Android");
    }

    //按鈕的點選事件
    public void click(View view){
        //java呼叫JS方法
        webView.loadUrl("javascript:javaCallJs(" + "'" + et_user.getText().toString()+"'"+")");
    }

    //在頁面銷燬的時候將webView移除
    @Override
    protected void onDestroy() {
        super.onDestroy();
        ll_root.removeView(webView);
        webView.stopLoading();
        webView.removeAllViews();
        webView.destroy();
        webView = null;
    }

    private class JSInterface {
        //JS需要呼叫的方法
        @JavascriptInterface
        public void showToast(String arg){
            Toast.makeText(MainActivity.this,arg,Toast.LENGTH_SHORT).show();
        }
    }
}

需要注意的地方

參考連結:安卓webview的一些坑

  1. webView.addJavascriptInterface()方法在API 17之前有一些漏洞(有興趣的可以參考本篇文章,WebView 遠端程式碼執行漏洞淺析),所以在API 17以後,需要在JavaScript介面類的方法加上@JavascriptInterface註解。
  2. 仔細看的話你會發現我們上面的WebView物件並不是直接寫在佈局檔案中的,而是通過一個LinearLayout容器,使用addview(webview)動態向裡面新增的。另外需要注意建立webview需要使用applicationContext而不是activitycontext,銷燬時不再佔有activity物件,最後離開的時候需要及時銷燬webviewonDestory()中應該先從LinearLayoutremovewebview,再呼叫webview.removeAllViews();webview.destory();
  3. 如果想要webView在產生OOM的時候不影響主程式,可以另開一個程式,在androidmanifest.xmlactivity標籤里加上Android:process屬性就可以了。
  4. activity被殺死之後,依然保持webView的狀態,方便使用者下次開啟的時候可以回到之前的狀態。webview支援saveState(bundle)restoreState(bundle)方法。

儲存狀態

@Override  
protected void onSaveInstanceState(Bundle outState) {  
    super.onSaveInstanceState(outState);  
    wv.saveState(outState);  
    Log.e(TAG, "save state...");  
}  

恢復狀態(在activityonCreate(bundle savedInstanceState)裡)

if(null!=savedInstanceState){  
    wv.restoreState(savedInstanceState);  
    Log.i(TAG, "restore state");  
}else{  
    wv.loadUrl("http://3g.cn");  
}  

其他一些常見問題:

  1. WebViewClient.onPageFinished()
    你永遠無法確定當WebView呼叫這個方法的時候,網頁內容是否真的載入完畢了。當前正在載入的網頁產生跳轉的時候這個方法可能會被多次呼叫,StackOverflow上有比較具體的解釋(How to listen for a Webview finishing loading a URL in Android?), 但其中列舉的解決方法並不完美。所以當你的WebView需要載入各種各樣的網頁並且需要在頁面載入完成時採取一些操作的話,可能WebChromeClient.onProgressChanged()比WebViewClient.onPageFinished()都要靠譜一些。

  2. WebView後臺耗電問題。
    當你的程式呼叫了WebView載入網頁,WebView會自己開啟一些執行緒(?),如果你沒有正確地將WebView銷燬的話,這些殘餘的執行緒(?)會一直在後臺執行,由此導致你的應用程式耗電量居高不下。對此我採用的處理方式比較偷懶,簡單又粗暴(不建議),即在Activity.onDestroy()中直接呼叫System.exit(0),使得應用程式完全被移出虛擬機器,這樣就不會有任何問題了。

  3. 切換WebView閃屏問題。
    如果你需要在同一個ViewGroup中來回切換不同的WebView(包含了不同的網頁內容)的話,你就會發現閃屏是不可避免的。這應該是Android硬體加速的Bug,如果關閉硬體加速這種情況會好很多,但無法獲得很好的瀏覽體驗,你會感覺網頁滑動的時候一卡一卡的,不跟手。

  4. 在某些手機上,Webview有視訊時,activity銷燬後,視訊資源沒有被銷燬,甚至還能聽到在後臺播放。即便是像剛才那樣各種銷燬webview也無濟於事,解決辦法:在onDestory之前修改url為空地址。

5.WebView硬體加速導致頁面渲染閃爍問題
關於Android硬體加速 開始於Android 3.0 (API level 11),開啟硬體加速後,WebView渲染頁面更加快速,拖動也更加順滑。但有個副作用就是容易會出現頁面載入白塊同時介面閃爍現象。解決這個問題的方法是設定WebView暫時關閉硬體加速 程式碼如下:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    }

相關文章