2.3.1 (下)WebView 檔案下載、快取、記憶體洩露

weixin_34253539發表於2018-05-31
2682788-b2b89f291cde2c1f.jpg

前言:

本篇給大家介紹的是 WebView 下載檔案的知識點,當我們在使用普通瀏覽器的時候,比如UC, 當我們點選到一個可供下載連結的時候,就會進行下載。WebView 作為一個瀏覽器般的元件, 當然也是支援下載的,我們可以自己來寫下載的流程,設定下載後的檔名稱以及儲存位置,當然也可以呼叫其它內建的瀏覽器來進行下載,比如Chrome、UC等,下面給大家演示下用法。

本節例程下載地址:WillFlowWebViewDowmload

一、WebView檔案下載

(1)呼叫其它瀏覽器下載檔案

這個很簡單,我們只需為 WebView 設定 setDownloadListener,然後重寫 DownloadListener 的 onDownloadStart,然後在裡面寫個Intent,然後startActivity對應的Activity即可!

  • 關鍵程式碼如下:
        mWebView.setDownloadListener(new DownloadListener(){
            @Override
            public void onDownloadStart(String url, String userAgent, String contentDisposition,
                                        String mimetype, long contentLength) {
                Log.i(TAG,"onDownloadStart");
                Log.i(TAG,"url : " + url);
                Log.i(TAG,"userAgent : " + userAgent);
                Uri uri = Uri.parse(url);
                Intent intent = new Intent(Intent.ACTION_VIEW,uri);
                startActivity(intent);
            }
        });

如果你手機記憶體在多個瀏覽器的話,會開啟一個對話方塊供你選擇其中一個瀏覽器進行下載。

(2)自己寫執行緒下載檔案

當然,你可能不想把下載檔案放到預設路徑下,或者想自己定義檔名等等,你都可以自己來寫 一個執行緒來下載檔案,實現示例程式碼如下:

  • DownLoadThread.java
/**
 * Created by   : WGH.
 */
public class DownLoadThread implements Runnable {
    private static final String TAG = DownLoadThread.class.getSimpleName();

    private String dUrl;

    public DownLoadThread(String dUrl) {
        this.dUrl = dUrl;
    }
    
    @Override
    public void run() {
        Log.i(TAG, "開始下載!");
        InputStream in = null;
        FileOutputStream fout = null;
        try {
            URL httpUrl = new URL(dUrl);
            HttpURLConnection conn = (HttpURLConnection) httpUrl.openConnection();
            conn.setDoInput(true);
            conn.setDoOutput(true);
            in = conn.getInputStream();
            File downloadFile, sdFile;
            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                Log.i(TAG,"SD卡可寫");
                downloadFile = Environment.getExternalStorageDirectory();
                sdFile = new File(downloadFile, "testDownload.apk");
                fout = new FileOutputStream(sdFile);
            }else{
                Log.e(TAG,"SD卡不存在或者不可讀寫!");
            }
            byte[] buffer = new byte[1024];
            int len;
            while ((len = in.read(buffer)) != -1) {
                assert fout != null;
                fout.write(buffer, 0, len);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if (fout != null) {
                try {
                    fout.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        Log.i(TAG, "下載完畢!");
    }
}
  • 然後MainActivity.java中建立並啟動該執行緒:
wView.setDownloadListener(new DownloadListener(){
    @Override
    public void onDownloadStart(String url, String userAgent, String contentDisposition, 
    String mimetype, long contentLength) {
            Log.e("HEHE","onDownloadStart被呼叫:下載連結:" + url);
            new Thread(new DownLoadThread(url)).start();
    }
});
  • 另外,別忘了寫SD卡的讀寫許可權以及Internet訪問網路的許可權:
<uses-permission android:name="android.permission.INTERNET"/>
<!-- 在SDCard中建立與刪除檔案許可權 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<!-- 往SDCard寫入資料許可權 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

後續我們還會用Java實現多執行緒斷點續傳,用Kotlin實現非同步下載檔案等功能,保持關注就好!

二、WebView快取問題

現在很多門戶類資訊網站,比如虎嗅、鈦媒體等等的APP,簡單點說是資訊閱讀類的APP,很多都是直接巢狀一個WebView用來顯示相關資訊的,這可能就涉及到了WebView的快取了!
所謂的頁面快取就是指:
儲存載入一個網頁時所需的HTML,JS,CSS等頁面相關的資料以及其他資源,當沒網的時候或者網路狀態較差的時候,我們會優先載入本地儲存好的相關資料,而這個相關資料就是之前在請求成功後儲存好的資料!
實現這個快取的方式有兩種
一種是後臺寫一個下載的Service,將相關的資料按自己的需求下載到資料庫或者儲存到相應資料夾中,然後下次載入對應URL前先判斷是否存在本地快取,如果存在優先載入本地快取,不存在則執行聯網請求,成功後再次快取相關資源。典型的如舊版本的36Kr,在進去後會先離線文章,然後再顯示!

當然本篇要講解的不是這種自己寫邏輯的方式,而是通過WebView本身自帶的快取功能來快取頁面,這種方式很常用而且使用起來非常簡單,我們只需為WebView設定開啟相關功能,以及設定資料庫的快取路徑即可完成快取。

(1)快取的分類

首先要說的是快取的分類,我們快取的資料分為:頁面快取資料快取

  • 頁面快取:
    載入一個網頁時的html、JS、CSS等頁面或者資源資料,這些快取資源是由於瀏覽器的行為而產生,開發者只能通過配置HTTP響應頭影響瀏覽器的行為才能間接地影響到這些快取資料,而快取的索引放在:/data/data/<包名>/databases,對應的檔案放在:/data/data/package_name/cache/webviewCacheChromunm下。

  • 資料快取:
    分為AppCache和DOM Storage兩種,我們開發者可以自行控制的就是這些快取資源。

    • AppCache:
      我們能夠有選擇的緩衝web瀏覽器中所有的東西,從頁面、圖片到指令碼、css等,尤其在涉及到應用於網站的多個頁面上的CSS和JavaScript檔案的時候非常有用,其大小目前通常是5M。 在Android上需要手動開啟(setAppCacheEnabled),並設定路徑(setAppCachePath)和容量 (setAppCacheMaxSize),而Android中使用ApplicationCache.db來儲存AppCache資料!
    • DOM Storage:
      儲存一些簡單的用key/value對即可解決的資料,根據作用範圍的不同,有Session Storage和Local Storage兩種,分別用於會話級別的儲存(頁面關閉即消失)和本地化儲存(除非主動刪除,否則資料永遠不會過期),在Android中可以手動開啟DOM Storage(setDomStorageEnabled),設定儲存路徑(setDatabasePath)Android中Webkit會為DOMStorage產生兩個檔案:my_path/localstorage/xxx.localstorage和my_path/Databases.db

(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,都使用快取中的資料

總結:根據以上兩種模式,建議快取策略為,判斷是否有網路,有的話,使用LOAD_DEFAULT, 無網路時,使用LOAD_CACHE_ELSE_NETWORK。

(3)為WebView開啟快取功能

下面我們就來為WebView開啟快取功能,先來看下實現的效果圖:


2682788-0ce3ff386d5f53e6.gif

流程解析:
1.進入頁面後預設載入url,然後隨便點選一個連結跳到第二個頁面,退出APP。
2.關閉wifi以及行動網路,然後重新進入,發現無網路的情況下,頁面還是載入了, 開啟第一個連結也可以載入,開啟其他連結就發現找不到網頁!
3.點選清除快取,把應用關閉,重新進入,發現頁面已經打不開!

  • 接下來是程式碼實現:MainActivity.java
public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();

    private WebView mWebView;
    private Button btn_clear_cache;
    private Button btn_refresh;
    private static final String APP_CACHE_DIRNAME = "/webviewcache";
    private static final String URL = "http://blog.csdn.net/comwill";

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

        mWebView = (WebView) findViewById(R.id.mWebView);
        btn_clear_cache = (Button) findViewById(R.id.btn_clear_cache);
        btn_refresh = (Button) findViewById(R.id.btn_refresh);

        mWebView.loadUrl(URL);
        mWebView.setWebViewClient(new WebViewClient() {
            // 設定在webView點選開啟的新網頁在當前介面顯示,而不跳轉到新的瀏覽器中
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                Log.i(TAG, "shouldOverrideUrlLoading url : " + url);
                view.loadUrl(url);
                return true;
            }
        });

        WebSettings settings = mWebView.getSettings();
        settings.setJavaScriptEnabled(true);
        // 設定快取模式
        settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
        // 開啟DOM storage API 功能
        settings.setDomStorageEnabled(true);
        // 開啟database storage API功能
        settings.setDatabaseEnabled(true);
        String cacheDirPath = getFilesDir().getAbsolutePath() + APP_CACHE_DIRNAME;
        Log.i(TAG, "cacheDirPath : " + cacheDirPath);
        // 設定資料庫快取路徑
        settings.setAppCachePath(cacheDirPath);
        settings.setAppCacheEnabled(true);
        Log.i(TAG, "DatabasePath : " + settings.getDatabasePath());

        btn_clear_cache.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mWebView.clearCache(true);
            }
        });

        btn_refresh.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mWebView.reload();
            }
        });
    }

    // 重寫回退按鈕的點選事件
    @Override
    public void onBackPressed() {
        if(mWebView.canGoBack()){
            mWebView.goBack();
        }else{
            super.onBackPressed();
        }
    }
}

程式碼很簡單,我們做的僅僅是開啟快取的功能,以及設定快取模式以及快取的資料的路徑。

(4)刪除WebView的快取資料

上面的示例中,我們通過呼叫WebView的clearCache(true)方法,已經實現了對快取的刪除!除了這種方法外,還有這樣的方法:

setting.setCacheMode(WebSettings.LOAD_NO_CACHE);
deleteDatabase("WebView.db");和deleteDatabase("WebViewCache.db");
webView.clearHistory();
webView.clearFormData();
getCacheDir().delete();

這就是手動寫delete方法,然後迴圈迭代刪除快取資料夾!

當然,正如前面所說,我們能直接操作的只是部分資料而已,而頁面快取是由於瀏覽器的行為而產生的,我們只能通過配置HTTP響應頭影響瀏覽器的行為才能間接地影響到這些快取資料,所以上述的方法僅僅是刪除的資料部分的快取,這一點還請注意。

三、WebView處理網頁返回的錯誤碼資訊

假如你們公司是做HTML5端的移動APP的,即通過WebView來顯示網頁。那麼假如你訪問的網頁不存在或者其他錯誤,比如:404、401、403等錯誤的狀態碼,如果直接彈出WebView預設的錯誤提示頁面,可能顯得不那麼友好,這時我們就可以通過重寫WebViewClient的onReceivedError()方法來實現我們想要的效果。

一般的做法有兩種:
一種是我們自己在assets目錄下建立一個用於顯示錯誤資訊的HTML頁面,當發生錯誤即onReceivedError()被呼叫的時候我們呼叫webView的loadUrl跳到我們的錯誤頁面。
另外一種是單獨寫一個佈局或者直接放一個大大的圖片,平時設定這個佈局或者圖片為不可見,當頁面錯誤時,讓該佈局或者圖片可見,下面我們來寫個簡單的示例。

(1)頁面錯誤,載入自定義網頁

mWebView.setWebViewClient(new WebViewClient() {
//設定在webView點選開啟的新網頁在當前介面顯示,而不跳轉到新的瀏覽器中
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    view.loadUrl(url);
    return true;
}

@Override
public void onReceivedError(WebView view, int errorCode, String description,
    String failingUrl) {
        super.onReceivedError(view, errorCode, description, failingUrl);
        mWebView.loadUrl("file:///android_asset/error.html");
    }
});

(2)頁面錯誤,顯示相應的View

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private WebView mWebView;
    private ImageView img_error_back;
    private Button btn_refresh;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mWebView = (WebView) findViewById(R.id.wView);
        img_error_back = (ImageView) findViewById(R.id.img_error_back);
        btn_refresh = (Button) findViewById(R.id.btn_refresh);
        mWebView.loadUrl("http://www.baidu.com");
        mWebView.setWebViewClient(new WebViewClient() {
            // 設定在webView點選開啟的新網頁在當前介面顯示,而不跳轉到新的瀏覽器中
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);
                return true;
            }

            @Override
            public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
                super.onReceivedError(view, errorCode, description, failingUrl);
                mWebView.setVisibility(View.GONE);
                img_error_back.setVisibility(View.VISIBLE);
            }
        });
        btn_refresh.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        mWebView.loadUrl("http://www.baidu.com");
        img_error_back.setVisibility(View.GONE);
        mWebView.setVisibility(View.VISIBLE);
    }
}

接下來我麼就可以編寫自己的程式碼進行驗證啦!

四、WebView 如何避免記憶體洩露?

(1)動態生成

要使用WebView不造成記憶體洩漏,首先應該做的就是不能在xml中定義webview節點,而是在需要的時候動態生成,即:可以在使用WebView的地方放置一個LinearLayout類似ViewGroup的節點,然後在要使用WebView的時候動態生成:

LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        mWebView = new WebView(getApplicationContext());
        mWebView.setLayoutParams(params);
        mLayout.addView(mWebView);

(2)注意銷燬的次序

在呼叫 webview.destroy()的時候,必須確保webview已經從view tree中被刪除,否則這個函式不會執行的。如果是在xml中靜態定義的webview,只有在整個view退出後呼叫 webview.destroy( )才會被正確執行,但整個view退出後又找不到webview了,這個是很矛盾的。所以正確的銷燬順序是:在 Activity 銷燬 WebView 的時候,先讓 WebView 載入null內容,然後移除 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的activity另開一個程式,作為一個獨立程式展示,感興趣的同學可以自己嘗試下。

結語:

到此為止,我們學會了使用 WebView 進行檔案下載的兩種方式:呼叫其它瀏覽器下載檔案、自己寫執行緒下載檔案;學會了使用WebView設定快取和清除快取;學會了WebView處理網頁返回的錯誤碼資訊的兩種方式:頁面錯誤載入自定義網頁、頁面錯誤顯示相應的View。
我會在將來寫一篇來介紹 WebViewJavascriptBridge 以及如何使用 WebViewJavascriptBridge 進行Java和Js的互動。有志共同進步的同學請頭像右側點選“(+)”保持對我的關注,後續好文第一時間推送給你。

相關文章