在 Android 开发中,WebView 是一个强大的组件,用于在应用中加载网页或 HTML 内容。然而,WebView 默认并不直接支持文件下载(如 PDF、图片、APK 等),需要开发者通过拦截下载请求并结合 Android 的文件下载机制来实现。以下是 WebView 文件下载的详细中文讲解,涵盖基本实现、进度监听、权限管理、注意事项以及代码示例,特别针对 Android 4.4 及以上版本(基于 Chromium 引擎)。
一、WebView 文件下载概述
- 目标:拦截 WebView 中的文件下载请求(如点击网页中的下载链接),并使用 Android 的下载机制(如
DownloadManager或自定义下载)将文件保存到设备存储。 - 常见场景:
- 用户在 WebView 中点击下载链接(如
https://example.com/file.pdf)。 - 下载图片、视频、文档等文件。
- 显示下载进度或通知用户下载完成。
- 实现方式:
- 使用
WebViewClient的shouldOverrideUrlLoading或onDownloadStart拦截下载链接。 - 结合
DownloadManager(系统下载服务)或自定义下载(如 OkHttp、HttpURLConnection)处理文件。 - 关键点:
- 处理权限(网络和存储)。
- 适配 Android 10+ 的 Scoped Storage。
- 支持下载进度监听和错误处理。
- 确保 WebView 的安全性和性能。
二、准备工作
- 权限声明:
在AndroidManifest.xml中添加以下权限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Android 9 及以下需要 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
- 动态权限(Android 6.0+):
- 对于存储权限,需动态请求:
java if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100); } }
- 布局文件:
在res/layout/activity_main.xml中添加 WebView:
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
- 依赖(可选):
- 如果使用 OkHttp 进行自定义下载:
gradle implementation 'com.squareup.okhttp3:okhttp:4.12.0'
- 文件保存路径:
- 内部存储:
context.getFilesDir()。 - 外部存储:
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)。 - Android 10+:使用
MediaStore或Scoped Storage保存文件。
三、WebView 文件下载实现
以下是使用 WebView 实现文件下载的详细步骤,重点介绍两种主要方式:DownloadManager(推荐用于简单下载)和 自定义下载(支持进度监听和断点续传)。
1. 使用 DownloadManager 实现文件下载
DownloadManager 是 Android 提供的系统级下载服务,适合简单文件下载,支持通知栏显示进度和完成状态。
- 代码示例:
import android.app.DownloadManager;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.webkit.DownloadListener;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
webView = findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true); // 启用 JavaScript(谨慎)
webView.setWebViewClient(new WebViewClient());
// 设置下载监听器
webView.setDownloadListener((url, userAgent, contentDisposition, mimetype, contentLength) -> {
// 创建下载请求
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setTitle("Downloading File");
request.setDescription("File from WebView");
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, getFileNameFromUrl(url));
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI | DownloadManager.Request.NETWORK_MOBILE);
// 获取 DownloadManager 并开始下载
DownloadManager downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
downloadManager.enqueue(request);
Toast.makeText(this, "Download started", Toast.LENGTH_SHORT).show();
});
// 加载网页
webView.loadUrl("https://example.com");
}
// 从 URL 提取文件名
private String getFileNameFromUrl(String url) {
return url.substring(url.lastIndexOf('/') + 1);
}
}
- 说明:
setDownloadListener:拦截 WebView 中的下载请求,获取 URL、MIME 类型等信息。DownloadManager.Request:配置下载任务,设置保存路径和通知。- 优点:简单,自动显示通知栏进度,无需手动管理下载。
- 局限性:不支持自定义进度监听或断点续传。
- 监听下载完成(可选):
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.IntentFilter;
@Override
protected void onCreate(Bundle savedInstanceState) {
// ... 其他代码
// 注册广播监听下载完成
registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
Toast.makeText(context, "Download completed", Toast.LENGTH_SHORT).show();
}
}, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
2. 自定义下载(使用 OkHttp)
对于需要进度监听或断点续传的场景,可以通过 OkHttp 实现自定义下载。
- 代码示例:
import android.content.Context;
import android.os.Bundle;
import android.os.Environment;
import android.webkit.DownloadListener;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.appcompat.app.AppCompatActivity;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okio.BufferedSink;
import okio.Okio;
import java.io.File;
public class MainActivity extends AppCompatActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
webView = findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient());
// 设置下载监听器
webView.setDownloadListener((url, userAgent, contentDisposition, mimetype, contentLength) -> {
new Thread(() -> {
String result = downloadFileWithOkHttp(url, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/" + getFileNameFromUrl(url));
runOnUiThread(() -> Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show());
}).start();
});
// 加载网页
webView.loadUrl("https://example.com");
}
private String getFileNameFromUrl(String url) {
return url.substring(url.lastIndexOf('/') + 1);
}
private String downloadFileWithOkHttp(String fileUrl, String savePath) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(fileUrl)
.get()
.addHeader("Accept", "*/*")
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
File file = new File(savePath);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
BufferedSink sink = Okio.buffer(Okio.sink(file));
sink.writeAll(response.body().source());
sink.close();
return "Download completed: " + savePath;
} else {
return "Error: HTTP " + response.code();
}
} catch (Exception e) {
e.printStackTrace();
return "Exception: " + e.getMessage();
}
}
}
- 说明:
setDownloadListener:捕获下载请求,启动异步线程使用 OkHttp 下载。OkHttp:高效处理文件流,适合自定义下载。- 优点:支持进度监听(需扩展,见下文)和断点续传。
3. 添加下载进度监听
通过自定义 OkHttp 的 Interceptor 实现下载进度监听。
- 代码示例:
import android.os.Bundle;
import android.os.Environment;
import android.webkit.DownloadListener;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okio.BufferedSink;
import okio.Okio;
import java.io.File;
public class MainActivity extends AppCompatActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
webView = findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient());
// 设置下载监听器
webView.setDownloadListener((url, userAgent, contentDisposition, mimetype, contentLength) -> {
new Thread(() -> {
String result = downloadFileWithProgress(url, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/" + getFileNameFromUrl(url), progress -> {
runOnUiThread(() -> Toast.makeText(MainActivity.this, "Progress: " + progress + "%", Toast.LENGTH_SHORT).show());
});
runOnUiThread(() -> Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show());
}).start();
});
// 加载网页
webView.loadUrl("https://example.com");
}
private String getFileNameFromUrl(String url) {
return url.substring(url.lastIndexOf('/') + 1);
}
public interface ProgressListener {
void onProgress(int progress);
}
private String downloadFileWithProgress(String fileUrl, String savePath, ProgressListener listener) {
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(chain -> {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.body(new ProgressResponseBody(originalResponse.body(), listener))
.build();
})
.build();
Request request = new Request.Builder()
.url(fileUrl)
.get()
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
File file = new File(savePath);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
BufferedSink sink = Okio.buffer(Okio.sink(file));
sink.writeAll(response.body().source());
sink.close();
return "Download completed: " + savePath;
} else {
return "Error: HTTP " + response.code();
}
} catch (Exception e) {
e.printStackTrace();
return "Exception: " + e.getMessage();
}
}
private static class ProgressResponseBody extends ResponseBody {
private final ResponseBody responseBody;
private final ProgressListener listener;
private BufferedSource bufferedSource;
public ProgressResponseBody(ResponseBody responseBody, ProgressListener listener) {
this.responseBody = responseBody;
this.listener = listener;
}
@Override
public MediaType contentType() {
return responseBody.contentType();
}
@Override
public long contentLength() {
return responseBody.contentLength();
}
@Override
public BufferedSource source() {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(new ProgressSource(responseBody.source(), contentLength(), listener));
}
return bufferedSource;
}
private static class ProgressSource extends ForwardingSource {
private final ProgressListener listener;
private final long totalBytes;
private long bytesRead = 0;
public ProgressSource(Source source, long totalBytes, ProgressListener listener) {
super(source);
this.totalBytes = totalBytes;
this.listener = listener;
}
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesReadThisTime = super.read(sink, byteCount);
if (bytesReadThisTime != -1) {
bytesRead += bytesReadThisTime;
if (totalBytes > 0) {
int progress = (int) (bytesRead * 100 / totalBytes);
listener.onProgress(progress);
}
}
return bytesReadThisTime;
}
}
}
}
- 说明:
- 使用 OkHttp 的
Interceptor包装ResponseBody,监听下载进度。 - 通过
ProgressListener回调更新 UI(如显示进度条)。
4. 支持断点续传
通过 Range 请求头实现断点续传,需记录已下载的文件大小。
- 代码示例:
private String downloadFileWithResume(String fileUrl, String savePath, ProgressListener listener) {
File file = new File(savePath);
long downloadedSize = file.exists() ? file.length() : 0;
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(chain -> {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.body(new ProgressResponseBody(originalResponse.body(), listener))
.build();
})
.build();
Request request = new Request.Builder()
.url(fileUrl)
.get()
.addHeader("Range", "bytes=" + downloadedSize + "-")
.build();
try (Response response = client.newCall(request).execute()) {
if ((response.isSuccessful() || response.code() == 206) && response.body() != null) {
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
BufferedSink sink = Okio.buffer(Okio.appendingSink(file));
sink.writeAll(response.body().source());
sink.close();
return "Download completed: " + savePath;
} else {
return "Error: HTTP " + response.code();
}
} catch (Exception e) {
e.printStackTrace();
return "Exception: " + e.getMessage();
}
}
- 说明:
- 使用
Range头指定下载起始位置。 - 使用
Okio.appendingSink支持追加写入。 - 服务器需支持
206 Partial Content。
四、注意事项
- 权限管理:
- Android 6.0+:动态请求存储权限。
- Android 10+:使用
MediaStore保存文件:java ContentValues values = new ContentValues(); values.put(MediaStore.Downloads.DISPLAY_NAME, "file.pdf"); values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS); Uri uri = getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values); OutputStream out = getContentResolver().openOutputStream(uri); - 配置
AndroidManifest.xml:xml <application android:requestLegacyExternalStorage="true">
- 安全问题:
- HTTPS:Android 9+ 默认禁用 HTTP,优先使用 HTTPS 下载链接。
- 验证 URL:检查下载 URL 是否来自可信域名:
java if (!url.startsWith("https://")) { Toast.makeText(this, "Only HTTPS downloads allowed", Toast.LENGTH_SHORT).show(); return; } - 文件类型:验证 MIME 类型,避免下载恶意文件:
java if (!mimetype.startsWith("application/") && !mimetype.startsWith("image/")) { Toast.makeText(this, "Unsupported file type", Toast.LENGTH_SHORT).show(); return; }
- 错误处理:
- 处理 HTTP 错误和网络异常:
java if (response.code() >= 400) { runOnUiThread(() -> Toast.makeText(this, "Download failed: HTTP " + response.code(), Toast.LENGTH_SHORT).show()); } - 检查网络状态:
java ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = cm.getActiveNetworkInfo(); if (networkInfo == null || !networkInfo.isConnected()) { Toast.makeText(this, "No network", Toast.LENGTH_SHORT).show(); }
- 性能优化:
- 使用缓冲区(4KB 或 8KB)读取流,减少 IO 开销。
- 启用 WebView 缓存:
java webView.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT); webView.getSettings().setAppCacheEnabled(true); - 异步下载,避免阻塞主线程。
- WebView 生命周期:
- 暂停/恢复 WebView:
@Override protected void onPause() { super.onPause(); webView.onPause(); webView.pauseTimers(); } @Override protected void onResume() { super.onResume(); webView.onResume(); webView.resumeTimers(); } - 销毁 WebView:
java @Override protected void onDestroy() { if (webView != null) { webView.stopLoading(); webView.clearCache(true); webView.loadUrl("about:blank"); webView.removeAllViews(); webView.destroy(); webView = null; } super.onDestroy(); }
- Android 4.4+ 特性:
- Chromium 引擎:支持现代 Web 标准,但内存占用较高。
- WebView 更新:通过 Google Play 更新,可能导致版本差异,需测试兼容性:
java String webViewVersion = WebView.getCurrentWebViewPackage().versionName; Log.d("WebView", "Version: " + webViewVersion);
五、学习建议与实践
- 学习路径:
- 掌握
setDownloadListener拦截下载请求。 - 使用
DownloadManager实现简单下载。 - 学习 OkHttp 实现自定义下载,支持进度监听和断点续传。
- 适配 Android 10+ 的 Scoped Storage。
- 调试 WebView 下载行为。
- 实践项目:
- 简单项目:在 WebView 中加载网页,点击下载链接使用
DownloadManager保存文件。 - 进阶项目:使用 OkHttp 实现下载,支持进度条显示。
- 高级项目:实现断点续传,保存下载状态。
- 调试工具:
- Chrome DevTools:通过
chrome://inspect调试 WebView(需开发者模式)。 - Charles/Fiddler:抓包分析下载请求。
- Logcat:查看下载日志和错误。
- Android Studio Network Profiler:监控网络请求。
- 推荐资源:
- Android 官方文档:https://developer.android.com/reference/android/webkit/WebView
- DownloadManager 文档:https://developer.android.com/reference/android/app/DownloadManager
- OkHttp 文档:https://square.github.io/okhttp/
- 测试 URL:https://speedtest.tele2.net/1MB.zip
六、总结
- 实现方式:
- DownloadManager:简单,自动通知,适合基本下载。
- OkHttp 自定义下载:支持进度监听和断点续传,适合复杂场景。
- 核心步骤:
- 使用
setDownloadListener拦截下载请求。 - 配置保存路径,处理权限。
- 处理下载进度和错误。
- 注意事项:
- 权限管理:动态请求存储权限,适配 Scoped Storage。
- 安全性:优先 HTTPS,验证文件类型。
- 性能:异步下载,启用缓存。
- 生命周期:正确暂停和销毁 WebView。
- 推荐:简单场景使用
DownloadManager,复杂需求使用 OkHttp。
七、完整代码示例
以下是一个完整的 WebView 文件下载示例,使用 DownloadManager 和 OkHttp,支持进度监听:
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.webkit.DownloadListener;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okio.BufferedSink;
import okio.Okio;
import java.io.File;
public class MainActivity extends AppCompatActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
webView = findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
webView.getSettings().setAppCacheEnabled(true);
webView.getSettings().setAppCachePath(getCacheDir().getAbsolutePath());
webView.setWebViewClient(new WebViewClient());
// 设置下载监听器
webView.setDownloadListener((url, userAgent, contentDisposition, mimetype, contentLength) -> {
if (!url.startsWith("https://")) {
Toast.makeText(this, "Only HTTPS downloads allowed", Toast.LENGTH_SHORT).show();
return;
}
// 使用 DownloadManager 下载
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setTitle(getFileNameFromUrl(url));
request.setDescription("Downloading from WebView");
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, getFileNameFromUrl(url));
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI | DownloadManager.Request.NETWORK_MOBILE);
DownloadManager downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
downloadManager.enqueue(request);
Toast.makeText(this, "Download started", Toast.LENGTH_SHORT).show();
// 或者使用 OkHttp 自定义下载(支持进度)
/*
new Thread(() -> {
String result = downloadFileWithProgress(url, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/" + getFileNameFromUrl(url), progress -> {
runOnUiThread(() -> Toast.makeText(this, "Progress: " + progress + "%", Toast.LENGTH_SHORT).show());
});
runOnUiThread(() -> Toast.makeText(this, result, Toast.LENGTH_SHORT).show());
}).start();
*/
});
// 注册下载完成广播
registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Download completed", Toast.LENGTH_SHORT).show();
}
}, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
// 加载网页
webView.loadUrl("https://example.com");
}
private String getFileNameFromUrl(String url) {
return url.substring(url.lastIndexOf('/') + 1);
}
public interface ProgressListener {
void onProgress(int progress);
}
private String downloadFileWithProgress(String fileUrl, String savePath, ProgressListener listener) {
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(chain -> {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.body(new ProgressResponseBody(originalResponse.body(), listener))
.build();
})
.build();
Request request = new Request.Builder()
.url(fileUrl)
.get()
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
File file = new File(savePath);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
BufferedSink sink = Okio.buffer(Okio.sink(file));
sink.writeAll(response.body().source());
sink.close();
return "Download completed: " + savePath;
} else {
return "Error: HTTP " + response.code();
}
} catch (Exception e) {
e.printStackTrace();
return "Exception: " + e.getMessage();
}
}
private static class ProgressResponseBody extends ResponseBody {
private final ResponseBody responseBody;
private final ProgressListener listener;
private BufferedSource bufferedSource;
public ProgressResponseBody(ResponseBody responseBody, ProgressListener listener) {
this.responseBody = responseBody;
this.listener = listener;
}
@Override
public MediaType contentType() {
return responseBody.contentType();
}
@Override
public long contentLength() {
return responseBody.contentLength();
}
@Override
public BufferedSource source() {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(new ProgressSource(responseBody.source(), contentLength(), listener));
}
return bufferedSource;
}
private static class ProgressSource extends ForwardingSource {
private final ProgressListener listener;
private final long totalBytes;
private long bytesRead = 0;
public ProgressSource(Source source, long totalBytes, ProgressListener listener) {
super(source);
this.totalBytes = totalBytes;
this.listener = listener;
}
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesReadThisTime = super.read(sink, byteCount);
if (bytesReadThisTime != -1) {
bytesRead += bytesReadThisTime;
if (totalBytes > 0) {
int progress = (int) (bytesRead * 100 / totalBytes);
listener.onProgress(progress);
}
}
return bytesReadThisTime;
}
}
}
@Override
protected void onPause() {
super.onPause();
webView.onPause();
webView.pauseTimers();
}
@Override
protected void onResume() {
super.onResume();
webView.onResume();
webView.resumeTimers();
}
@Override
protected void onDestroy() {
if (webView != null) {
webView.stopLoading();
webView.clearCache(true);
webView.loadUrl("about:blank");
webView.removeAllViews();
webView.destroy();
webView = null;
}
super.onDestroy();
}
}
如果需要更详细的代码(如断点续传、MediaStore 集成)或特定场景的讲解,请告诉我!