美文网首页Android技术知识Android开发经验谈Android开发
2.3.1 (下)WebView 文件下载、缓存、内存泄露

2.3.1 (下)WebView 文件下载、缓存、内存泄露

作者: 常思行 | 来源:发表于2018-05-31 14:22 被阅读27次

    前言:

    本篇给大家介绍的是 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点击打开的新网页在当前界面显示,而不跳转到新的浏览器中
    @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的交互。有志共同进步的同学请头像右侧点击“(+)”保持对我的关注,后续好文第一时间推送给你。

    相关文章

      网友评论

        本文标题:2.3.1 (下)WebView 文件下载、缓存、内存泄露

        本文链接:https://www.haomeiwen.com/subject/qtuqsftx.html