Android 文件下载(2)
继上一部分(Android 文件下载(1))介绍了使用 HttpURLConnection
实现文件下载后,本部分将重点讲解使用 OkHttp 和 Retrofit 进行文件下载,包括基本下载、进度监听和断点续传的实现。还将补充 DownloadManager 的高级用法,并提供更全面的注意事项和实践建议。本指南假设你已熟悉 HTTP GET 请求和文件存储基础,旨在深入探讨现代 Android 文件下载的实现方式。
一、文件下载概述(回顾)
- 目标:从服务器下载文件(如图片、视频、APK)并保存到本地存储。
- 关键点:
- 使用 HTTP GET 请求,结合
Content-Length
和Range
头。 - 异步处理,避免阻塞主线程。
- 支持进度监听和断点续传。
- 处理存储权限和 Android 10+ 的 Scoped Storage。
- 工具:
- OkHttp:支持异步请求、进度监听、连接池,适合复杂场景。
- Retrofit:基于 OkHttp,简化 RESTful API 下载。
- DownloadManager:系统级工具,适合简单下载。
- 场景:
- 基本下载:下载小文件(如图片)。
- 高级需求:大文件下载、进度显示、断点续传。
二、准备工作(回顾)
- 权限声明:
在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+:动态请求存储权限。
- Android 10+:使用
Scoped Storage
或MediaStore
保存文件。
- 依赖添加:
在build.gradle
(app 模块)中添加:
// OkHttp
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
- 文件保存路径:
- 内部存储:
context.getFilesDir()
。 - 外部存储:
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
。 - MediaStore(Android 10+):用于媒体文件。
- 服务器要求:
- 提供文件下载 URL。
- 支持
Range
请求(断点续传)。
三、使用 OkHttp 实现文件下载
OkHttp 是一个功能强大的第三方 HTTP 客户端,支持异步请求、进度监听和断点续传,适合复杂下载需求。
1. 基本文件下载
下载文件并保存到本地存储。
- 代码示例:
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okio.BufferedSink;
import okio.Okio;
import java.io.File;
public class OkHttpDownloader {
public static String downloadFile(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();
}
}
}
- 使用示例(异步调用):
new Thread(() -> {
String fileUrl = "https://example.com/image.jpg";
String savePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/image.jpg";
String result = OkHttpDownloader.downloadFile(fileUrl, savePath);
runOnUiThread(() -> {
Log.d("Download", result);
});
}).start();
- 异步调用(OkHttp 回调):
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://example.com/image.jpg")
.get()
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> Log.e("Download", "Exception: " + e.getMessage()));
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful() && response.body() != null) {
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/image.jpg");
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
BufferedSink sink = Okio.buffer(Okio.sink(file));
sink.writeAll(response.body().source());
sink.close();
runOnUiThread(() -> Log.d("Download", "Download completed"));
} else {
runOnUiThread(() -> Log.e("Download", "Error: HTTP " + response.code()));
}
}
});
- 说明:
- 使用
Okio
高效写入文件。 - 支持异步回调,适合 UI 响应。
- 自动管理连接池。
2. 下载进度监听
通过自定义 Interceptor
或包装 ResponseBody
实现进度监听。
- 代码示例:
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.BufferedSink;
import okio.Okio;
import java.io.File;
public class OkHttpDownloader {
public interface ProgressListener {
void onProgress(int progress);
}
public static 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;
}
}
}
}
- 使用示例:
new Thread(() -> {
String fileUrl = "https://example.com/video.mp4";
String savePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/video.mp4";
String result = OkHttpDownloader.downloadFileWithProgress(fileUrl, savePath, progress -> {
runOnUiThread(() -> {
Log.d("Download", "Progress: " + progress + "%");
// 更新 ProgressBar
});
});
runOnUiThread(() -> {
Log.d("Download", result);
});
}).start();
- 说明:
- 使用
Interceptor
包装ResponseBody
,监听读取进度。 - 适合大文件下载,实时更新 UI。
3. 断点续传
通过 Range
请求头实现断点续传。
- 代码示例:
public class OkHttpDownloader {
public static 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();
}
}
}
- 使用示例:
new Thread(() -> {
String fileUrl = "https://example.com/video.mp4";
String savePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/video.mp4";
String result = OkHttpDownloader.downloadFileWithResume(fileUrl, savePath, progress -> {
runOnUiThread(() -> {
Log.d("Download", "Progress: " + progress + "%");
});
});
runOnUiThread(() -> {
Log.d("Download", result);
});
}).start();
- 说明:
- 使用
Range
头指定下载起始位置。 - 使用
Okio.appendingSink
支持追加写入。 - 服务器需支持
206 Partial Content
。
四、使用 Retrofit 实现文件下载
Retrofit 基于 OkHttp,适合 RESTful API 下载,简化代码。
1. 基本文件下载
- 定义接口:
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Streaming;
import retrofit2.http.Url;
public interface DownloadService {
@Streaming // 防止大文件内存溢出
@GET
Call<ResponseBody> downloadFile(@Url String fileUrl);
}
- 下载代码:
import okhttp3.ResponseBody;
import retrofit2.Retrofit;
import retrofit2.Call;
import okio.BufferedSink;
import okio.Okio;
import java.io.File;
public class RetrofitDownloader {
public static String downloadFile(String baseUrl, String fileUrl, String savePath) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.build();
DownloadService service = retrofit.create(DownloadService.class);
try {
Call<ResponseBody> call = service.downloadFile(fileUrl);
Response<ResponseBody> response = call.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();
}
}
}
- 使用示例(Kotlin 协程):
interface DownloadService {
@Streaming
@GET
suspend fun downloadFile(@Url fileUrl: String): Response<ResponseBody>
}
suspend fun downloadFile(baseUrl: String, fileUrl: String, savePath: String): String = withContext(Dispatchers.IO) {
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.build()
val service = retrofit.create(DownloadService::class.java)
try {
val response = service.downloadFile(fileUrl)
if (response.isSuccessful && response.body() != null) {
val file = File(savePath)
if (!file.parentFile.exists()) {
file.parentFile.mkdirs()
}
val sink = Okio.buffer(Okio.sink(file))
sink.writeAll(response.body()!!.source())
sink.close()
"Download completed: $savePath"
} else {
"Error: HTTP ${response.code()}"
}
} catch (e: Exception) {
"Exception: ${e.message}"
}
}
// 调用
lifecycleScope.launch {
val result = downloadFile(
"https://example.com/",
"https://example.com/image.jpg",
"${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)}/image.jpg"
)
Log.d("Download", result)
}
- 说明:
- 使用
@Streaming
注解避免大文件内存溢出。 - 结合协程简化异步处理。
- 依赖 OkHttp 处理底层流。
2. 进度监听
需要结合 OkHttp 的 Interceptor
,如 OkHttp 示例中的 ProgressResponseBody
。
3. 断点续传
通过 OkHttp 的 Range
头实现,Retrofit 需自定义 OkHttpClient:
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(chain -> {
Request request = chain.request();
if (downloadedSize > 0) {
request = request.newBuilder()
.header("Range", "bytes=" + downloadedSize + "-")
.build();
}
return chain.proceed(request);
})
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.build();
五、DownloadManager 高级用法
DownloadManager
适合简单下载,支持通知和基本配置。
- 代码示例(带通知和错误处理):
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.Environment;
public class DownloadManagerUtil {
public static void downloadFile(Context context, String fileUrl, String fileName) {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(fileUrl))
.setTitle(fileName)
.setDescription("Downloading file")
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI | DownloadManager.Request.NETWORK_MOBILE);
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
long downloadId = downloadManager.enqueue(request);
// 注册广播监听下载完成
context.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
if (id == downloadId) {
Log.d("Download", "Download completed: " + fileName);
}
}
}, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
}
- 使用示例:
DownloadManagerUtil.downloadFile(this, "https://example.com/image.jpg", "image.jpg");
- 说明:
- 自动显示下载通知。
- 通过广播监听下载完成。
- 不支持进度监听或断点续传。
六、注意事项
- 异步处理:
- 下载操作必须在子线程,OkHttp 和 Retrofit 支持异步回调或协程。
- 权限管理:
- Android 6.0+:动态请求存储权限。
- Android 10+:使用
MediaStore
保存媒体文件:java ContentValues values = new ContentValues(); values.put(MediaStore.Downloads.DISPLAY_NAME, "image.jpg"); values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS); Uri uri = context.getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values); OutputStream out = context.getContentResolver().openOutputStream(uri);
- 错误处理:
- 处理
IOException
和 HTTP 错误(4xx、5xx)。 - 检查
Content-Length
是否有效。
- 资源释放:
- OkHttp 自动管理连接,Retrofit 需关闭流。
- DownloadManager 无需手动释放。
- 性能优化:
- 使用缓冲区(4KB 或 8KB)读取流。
- 支持断点续传,减少重复下载。
- 安全性:
- 使用 HTTPS,验证服务器证书。
- 添加认证头(如
Authorization: Bearer token
)。
七、学习建议与实践
- 学习路径:
- 掌握 OkHttp 的基本下载和进度监听。
- 学习 Retrofit 的
@Streaming
注解和异步下载。 - 实现断点续传,使用
Range
头。 - 探索 DownloadManager 的简单场景。
- 实践项目:
- 简单项目:下载图片并显示。
- 进阶项目:下载视频,显示进度条。
- 高级项目:实现断点续传,保存下载状态。
- 调试工具:
- Postman:测试下载 URL。
- Charles/Fiddler:抓包分析。
- Android Studio Network Profiler:查看请求详情。
- 推荐资源:
- OkHttp 文档:https://square.github.io/okhttp/
- Retrofit 文档:https://square.github.io/retrofit/
- Android 官方文档:https://developer.android.com/reference/android/app/DownloadManager
八、总结
- OkHttp:支持异步、进度监听、断点续传,适合复杂下载。
- Retrofit:简化 RESTful 下载,结合协程优雅高效。
- DownloadManager:适合简单下载,自动通知。
- 注意事项:异步处理、权限管理、错误处理、资源释放。
- 推荐:优先使用 OkHttp 或 Retrofit,DownloadManager 用于简单任务。
如果需要更详细的代码示例(如多文件下载、MediaStore 集成)或特定场景讲解,请告诉我!