3.1.3 WebView 文件下载、缓存、内存泄露

来源:互联网 发布:java 工具类打包jar包 编辑:程序博客网 时间:2024/06/07 09:00

前言:

本篇给大家介绍的是 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开启缓存功能,先来看下实现的效果图:

流程解析:
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点击打开的新网页在当前界面显示,而不跳转到新的浏览器中@Overridepublic boolean shouldOverrideUrlLoading(WebView view, String url) {    view.loadUrl(url);    return true;}@Overridepublic 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的交互。有志共同进步的同学请头像右侧点击“(+)”保持对我的关注,后续好文第一时间推送给你。

联系方式:

简书:WillFlow
CSDN:WillFlow
微信公众号:WillFlow

微信公众号:WillFlow