[Android基礎]WebView的簡單使用

我啥時候說啦jj發表於2017-12-27

[TOC]


[ Demo下載 ]

資源

  1. Web Apps
  2. WebView
  3. Android4.4 webview實現分析
  4. Android WebView使用深入淺出
  5. 深入講解WebView(上) - 互調,快取,異常處理等
  6. 深入講解WebView(下) - session,cookie等
  7. Android WebView Memory Leak WebView記憶體洩漏 ==! 這個我用leakcanary沒檢測出來
  8. PHP、Android、iOS 的恩恩怨怨

從Android 4.4(KitKat)開始,WebView元件是基於開源的Chromium專案.包含V8 js引擎並支援新的web標準,新webView也共享Chrome for Android的渲染引擎,另外,從5.0(Lollipop)開始,WebView被移到獨立的apk中,因此它可以進行單獨更新,可以從 "settings -- Apps -- Android System WebView" 中檢視其版本;

用途

預設情況下,webView不啟用js互動,並會忽略頁面錯誤,適用於展示靜態資訊; 也可以啟用js功能,實現與使用者的互動

輔助類

  • WebChromeClient 當可能影響webView UI的操作發生時會呼叫到該類,比如進度變化或者js提示框...
  • WebViewClient 當可能影響內容渲染的操作發生時會呼叫到該類,比如錯誤等...另外,可以通過重寫 shouldOverrideUrlLoading() 來中斷url的載入;
  • WebSettings 功能設定,比如可否允許js程式碼;

基本操作

  • 訪問網路的話需要新增網路許可權
<uses-permission android:name="android.permission.INTERNET" />
複製程式碼
  • 啟用js功能
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
複製程式碼

操作localStorage

專案中接到的要求,要傳給H5頁面新增一些token值,方便其傳送非url請求的時候呼叫 P.S. shouldOverrideUrlLoading() 只能攔截url超連結請求,對於H5頁面自己傳送其他非跳轉請求的話這個方法是沒法攔截的 而 shouldInterceptRequest() 是返回給app端一個response,如果方法返回的是null則走正常網路請求返回,否則就返回給定的response, 想到的方案是呼叫js程式碼給localstorage中設定一些值,方便h5呼叫,當然給出一個原生方法給h5呼叫也一樣

mWebSettings = mWebView.getSettings();
mWebSettings.setJavaScriptEnabled(true);
mWebSettings.setDomStorageEnabled(true);//給許可權

mWebView.setWebViewClient(new WebViewClient() {

        // 不在 onPageStart() 中去設定是因為設定完以後又loadUrl(url),之前設定的值就無效了
        // 當然,在 onPageFinished() 設定的話也得H5中在document.ready()之後才能去獲取
        // 或者也可以考慮在 WebChromeClient 的 onProgressChanged() 方法中作設定

        @Override
        public void onPageFinished(WebView view, String url) {
                                          LogUtils.d("footTest", "onPageFinished " + url);
                                          view.loadUrl(
                                                  "javascript:" +
                                                          "localStorage.setItem('token', '" + UacDataInstance.getUserTokenWithoutBear() + "');");
        }
    }
);
複製程式碼

設定返回鍵回退功能

mWv.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                // 需要新增 mWv.canGoBack(),不然當返回到初始頁面時,可能無法繼續通過返回鍵關閉頁面
                if (keyCode == KeyEvent.KEYCODE_BACK && mWv.canGoBack()) {
                    mWv.goBack();
                    return true;
                }
                return false;
            }
        });
複製程式碼

也可以通過設定所在Activity的onBackPressed()方法來支援webView回退:

@Override
public void onBackPressed() {
    if (mWv.canGoBack()) {
        mWv.goBack();
    } else {
        super.onBackPressed();
    }
}
複製程式碼

設定標題

mWv.setWebChromeClient(new WebChromeClient(){
    @Override
    public void onReceivedTitle(WebView view, String title) {
        // title 是獲取到的網頁title,可以將之設定為webView所在頁面的標題
        MainActivity.this.setTitle(title);
    }
)};
複製程式碼

設定載入進度

@Override
protected void onCreate(Bundle savedInstanceState) {
    //requestWindowFeature(Window.FEATURE_PROGRESS);
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_VISIBILITY_ON);
    ......

    mProgressDlg = new ProgressDialog(this);
    mProgressDlg.setMessage("loading...");
    
    mWv.setWebChromeClient(new WebChromeClient() {
        @Override
        public void onProgressChanged(WebView view, int newProgress) {
            //更新進度條示數
            
            //這種方式我沒看到效果...
            //MainActivity.this.setProgress(newProgress);
            
            //使用控制元件ProgressDialog來顯示進度
            //但記得這種方式需要在error發生時也進行取消
            if (newProgress <= 90) {
                mProgressDlg.setProgress(newProgress);
            } else {
                mProgressDlg.dismiss();
            }
        }
    });

    mWv.setWebViewClient(new WebViewClient() {
        @Override
        public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
            super.onReceivedError(view, request, error);
            // 載入某些網站的時候會報:ERR_CONNECTION_REFUSED,因此需要在這裡取消進度條的顯示
            Toast.makeText(MainActivity.this, "error", Toast.LENGTH_SHORT).show();
            if (mProgressDlg.isShowing()) {
                mProgressDlg.dismiss();
            }
        }

        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon) {
            super.onPageStarted(view, url, favicon);
            if (!mProgressDlg.isShowing()) {
                mProgressDlg.show();
            }
        }

        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            if (mProgressDlg.isShowing()) {
                mProgressDlg.dismiss();
            }
        }
    });
複製程式碼

控制url跳轉

mWv.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        // 這個方法我沒有重寫的話也還是使用webView來載入連結
        // 而且我這裡測試返回的true/false貌似沒什麼影響
        
        if (Uri.parse(url).getHost().endsWith("jianshu.com")) {
            //若是指定伺服器的連結則在當前webView中跳轉
            view.loadUrl(url);
            return false;
        } else if (Uri.parse(url).getHost().length() == 0) {
            // 本地連結的話直接在webView中跳轉
            return false;
        }

        // 其他情況則使用系統瀏覽器開啟網址
        Uri uri = Uri.parse(url);
        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
        startActivity(intent);
        return true;
    }
});
複製程式碼

載入頁面

1. 載入本地asset檔案

 mWv.loadUrl("file:///android_asset/index.html");
複製程式碼

2. 載入本地網頁2

//index.html檔案放置於 src/main/assets 目錄中
myWebView.loadUrl("file:///android_asset/index.html");
複製程式碼

3. 載入網頁

myWebView.loadUrl("http://www.jianshu.com/users/302253a7ed00/latest_articles");
複製程式碼

4. 解析html字串

String summary = "<!DOCTYPE html>\n" +
        "<html lang=\"zh_CN\">\n" +
        "<head>\n" +
        "    <meta charset=\"UTF-8\">\n" +
        "    <title>webViewDemoFromAsset</title>\n" +
        "    <script src=\"js/basic.js\"></script>\n" +
        "</head>\n" +
        "<body>\n" +
        "<div>\n" +
        "    <button id=\"btn\" onclick='showToast()'>呼叫android toast</button>\n" +
        "</div>\n" +
        "\n" +
        "<label id='label'>js android程式碼互調測試</label>\n" +
        "\n" +
        "<br>\n" +
        "<a href=\"http://www.jianshu.com/users/302253a7ed00/latest_articles/\">個人主頁</a>\n" +
        "</body>\n" +
        "</html>";

// 官網例子給的下面的寫法,但是會出現中文亂碼,
// 原因:http://blog.csdn.net/top_code/article/details/9163597
// mWv.loadData(summary, "text/html", "utf-8");

mWv.loadData(summary, "text/html;charset=UTF-8", null);
複製程式碼

使用android studio的話,專案結構中沒有asset目錄,需要手動建立 src/main/assets 目錄即可; 擴充套件:

  1. 如果html檔案存於sdcard:則加字首: content://com.android.htmlfileprovider/sdcard/ 另外, content 字首可能導致異常,直接使用 file:///sdcard/ 或者 file:/sdcard 也可以;
  2. 也可使用 locaData() ,先將檔案讀取出來,在傳入字串到方法中,可以用於展示頁面,但不會引用css,js等檔案;

js與andorid互調

  1. 通過 addJavaScriptInterface() 來設定介面,傳入例項和類名,讓js可以呼叫;

Note: The object that is bound to your JavaScript runs in another thread and not in the thread in which it was constructed. 允許網頁呼叫android功能可以存在風險,比如載入其他網頁,預設做法是使用瀏覽器去載入外部其他網頁;

  1. 自定義的js對應andoird實現類
//通過webView按鈕呼叫android toast功能
public class BasicJsAppInterface {
    private Context cxt;
    public BasicJsAppInterface(Context cxt) {
        this.cxt = cxt;
    }
    // 如果targetSDKVersion設定為17以上,這裡需要新增該annotation標誌
    @JavascriptInterface
    public void showToast() {
        Toast.makeText(this.cxt, "toast in android", Toast.LENGTH_SHORT).show();
    }
}
複製程式碼
// 實現js呼叫android功能
WebView mWv = (WebView) findViewById(R.id.wv);
 WebSettings wvSettings = mWv.getSettings();
wvSettings.setJavaScriptEnabled(true);
wvSettings.setDefaultTextEncodingName("utf-8");
//傳入實現js功能的android例項 以及 js呼叫時使用的名稱
mWv.addJavascriptInterface(new BasicJsAppInterface(this), "AndroidApp");
//載入本地asset檔案,以 `file:///` 開頭
mWv.loadUrl("file:///android_asset/index.html");
複製程式碼

1. js 呼叫 android 功能

// 在src/main/assets 目錄(不存在則手動建立)中建立該html檔案
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webViewDemoFromAsset</title>
    <script src="js/basic.js"></script>
</head>
<body>
<div>
    <button id="btn" onclick='showToast()'>呼叫android toast</button>
</div>

<label id='label'>js android程式碼互調測試</label>

<br>
<a href="http://lucid-lynxz.github.io/">github主頁</a>
<a href="http://www.jianshu.com/users/302253a7ed00/latest_articles">簡書主頁</a>

<video width="400" controls>
    <source src="res/shuai_dan_ge.mp4" type="video/mp4">
    <source src="res/gongsi_de_liliang.flv" type="video/flv">
    <p>不支援該格式視訊</p>
</video>
</body>
</html>
複製程式碼

注意:這裡引入的獨立js檔案標籤不能簡寫成 <script src="..."/> ,否則解析可能會出錯,參見

  1. 自閉合標籤;
  2. Whe don't self-closing script tags work
    webview_js
//在 assets/js/ 目錄下建立js獨立檔案basic.js,當然也可以把這些程式碼直接嵌入到html中
function setLabel(id, label) {
    document.getElementById(id).innerHTML = label;
}

function showToast(){
  AndroidApp.showToast(); //也可以寫成window.AndroidApp.showToast();
}
複製程式碼

2. android 呼叫js程式碼:

//字首javascript, `setLabel()是網頁js檔案中定義的方法`
mWv.loadUrl("javascript:setLabel('label','通過android呼叫js程式碼')");
複製程式碼

快取/Cookie

webview應用的快取檔案放置於 /data/data/{yourProjectName}/ 下面,之前想提取webview快取的圖片,往上查詢的資料大都是通過 webviewcache.db 來獲取圖片對應的快取檔案,但是我在紅米1s4.4以及nexus6p 6.0系統上都沒有再發現這個檔案了,新的快取目錄結構:

webview快取的資原始檔位置

從上圖可以發現 Cookies 檔案存在,使用16進位制編輯器開啟檢視,也可在程式中獲取:

private String getCookie() {
    CookieManager cm = CookieManager.getInstance();
    String cookie = cm.getCookie(mUrl);
    if (TextUtils.isEmpty(cookie)) {
        cookie = "there is no cookie exist";
    }
    return cookie;
}
複製程式碼

另外,圖中紅色方框內的檔案就是快取的檔案了,它們名稱是如何跟實際資原始檔對應起來的,這個我還沒弄懂,不過還是可以獲取快取圖片的,我們使用16進位制編輯器來檢視,可以發現頭部有該圖片的url地址("?g....d64d.png"):

ff8def31493d3be1_0 檔案內容

我們刪除該檔案的url地址資訊,儲存後,修改字尾名為png,即可看到實際的圖片:

刪除圖片地址資訊
實際的圖片(右側)

播放視訊

支援標準MP4,ogg之類的,flash得啟用外掛進行播放,不考慮 官網 建議播放視訊的時候開啟硬體加速,不過我在nexus6p上沒有開(預設開了嗎?)也ok的;

全屏播放

參考

頁面適應

Pixel-Perfect UI in the WebView

  • 一個針對移動端優化過的頁面帶有如下類似的屬性:
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
複製程式碼

系統將頁面顯示在一個虛擬的Viewport中,這個視口通常比螢幕大,這樣網頁就不會被限制在很小的範圍內,使用者可以通過縮放和平移來檢視內容;

  • 對於無法控制內容的線上網頁,可以通過程式碼方式設定ViewPort:
//強制手機使用 desktop-size viewport
wvSettings.setUseWideViewPort(true);
wvSettings.setLoadWithOverviewMode(true);
複製程式碼

擴充套件-響應式

除錯

  1. chrome 需要在電腦上安裝Chrome32以上的版本;

  2. 在電腦上啟動瀏覽器開啟網址 chrome://inspect ,

  3. android啟用webView除錯 條件:

    • android 4.4以上
    • 允許遠端除錯
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    // 官網: https://developer.chrome.com/devtools/docs/remote-debugging#debugging-webviews
    // 官網說WebView不受manifest的debuggable標籤的影響,若需要在該標籤啟用時才允許除錯,則新增如下條件判斷(注意:儘量不要在manifest中顯式指定debuggable屬性,放空即可,這樣Android Studio會自動在除錯時設定成true,在release版本中設定成false)
    int debuggable = getApplicationInfo().flags &= ApplicationInfo.FLAG_DEBUGGABLE;
    if (0 != debuggable) {
        WebView.setWebContentsDebuggingEnabled(true);
    }
}
複製程式碼

注意事項

  1. All WebView methods must be called on the same thread wv.loadUrl() 方法放在主執行緒(根據錯誤提示來指定)中執行; P.S. webview類中有checkThread()方法,跟初始的Lopper.myLooper做比較,想跟蹤setContentView看看,結果原始碼不全,斷點跟蹤不知道跟到哪裡去了...後續得再補補;
  2. html頁面應用獨立的js檔案時,script不能寫成自閉合標籤,否則瀏覽器解析可能會出錯;
  3. 官方建議WebView的height屬性設定為 match_parent 或者指定值,而非 wrap_content ,同事設定為 match_parent 後,其各個父容器不允許設定height為 wrap_content ,否則可能導致異常發生;
  4. android 4.4對webView做了些變化,可以參考 [這篇文章](Migrating to WebView in Android 4.4);
  5. 混淆時,需要設定javaScriptInterface不被混淆
# app/proguard-rules.pro
-keep public class org.lynxz.webviewdemo.BasicJsAppInterface{
    public <methods>;
}
-keepattributes *Annotation*
-keepattributes *JavascriptInterface*
複製程式碼

異常

1. 記憶體洩露

這個我還沒測試,Android WebView Memory Leak WebView記憶體洩漏 ==! 然後查了記憶體檢查: Android最佳效能實踐(二)——分析記憶體的使用情況

這裡有人發現android 5.1也有類似的情況,我沒有嘗試載入很多頁面,先記錄下來: Android 5.1 Webview 記憶體洩漏新場景

2. loadData() 中文亂碼

參考這篇

mWv.loadData(yourHtmlString, "text/html;charset=UTF-8", null);
複製程式碼

有人說這麼設定也可以避免亂碼,但是我在nexus 6p上沒測試成功:

wvSettings.setDefaultTextEncodingName("utf-8");
複製程式碼

3. eglCodecCommon: **** ERROR unknown type 0x73000f (glSizeof,80)

Genymotion模擬器不支援硬體加速,關閉即可: mWv.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

4. html中含有angular.js,資料獲取成功,但是顯示空白:

已啟用js支援: mWv.getSettings().setJavaScriptEnabled(true);

logCat報錯:

I/xxx: url = http://fep-web.debug.web.nd/#!/report/student/compositive?client=phone&mode=debug&user_id=2079947956
D/dalvikvm: GC_FOR_ALLOC freed 37K, 14% free 21151K/24528K, paused 17ms, total 17ms
I/dalvikvm-heap: Grow heap (frag case) to 25.837MB for 3288976-byte allocation
W/AwContents: nativeOnDraw failed; clearing to background color.
I/Timeline: Timeline: Activity_idle id: android.os.BinderProxy@425bdee8 time:31230137
I/chromium: [INFO:CONSOLE(39)] "Uncaught Error: [$injector:modulerr] http://errors.angularjs.org/1.4.10/$injector/modulerr?p0=app&p1=Error%3A%20%5B%24injector%3Amodulerr%5D%20http%3A%2F%2Ferrors.angularjs.org%2F1.4.10%2F%24injector%2Fmodulerr%3Fp0%3Dapp-theme%26p1%3DTypeError%253...<omitted>...3)", source: http://fep-web.debug.web.nd/bower_components/angular/angular.min.js?v=201604181940 (39)
I/chromium: [INFO:CONSOLE(72)] "error_log:localStorage error", source: http://fep-web.debug.web.nd/js-error.no-ng.js (72)
複製程式碼

我也不懂angular.js用到了什麼,新增下dom支援就可以了:

settings.setDomStorageEnabled(true);
複製程式碼

5. A WebView method was called on thread 'JavaBridge'. All WebView methods must be called on the same thread.

webview載入了網頁後,在html中點選重新載入網頁,我之前直接在介面類的方法中直接執行,

@JavascriptInterface
public void retriveToUrl(String url) {
    mWv.loadUrl(url);
}
複製程式碼

需要將其放置在ui執行緒中執行:

The JavaScript method is executed on a background (i.e. non-UI) thread. You need to call all Android View related methods on the UI thread.

@JavascriptInterface
public void retriveToUrl(String url) {
    mWv.post(new Runnable() {
        @Override
        public void run() {
            mWebView.loadUrl(...).
        }
    });
}
複製程式碼

6. "TypeError: Object [object Object] has no method 'callNative' - 混淆

在release版本中,js程式碼呼叫不到我定義的介面類中的方法,從混淆檔案中把這個類排除即可; js interface proguard

相關文章