您的位置:首页 > 移动开发

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

2017-08-20 09:07 471 查看


前言:

本篇给大家介绍的是 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的交互。有志共同进步的同学请头像右侧点击“(+)”保持对我的关注,后续好文第一时间推送给你。

联系方式:

简书:WillFlow

CSDN:WillFlow

微信公众号:WillFlow

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息