在实际项目中,我们经常会遇到如下需求:
- 多文件顺序下载(避免并发过多)
- 支持下载进度回调
- 支持取消单个 / 全部下载任务
- 下载完成后自动保存到相册(区分图片 / 视频)
- 下载过程不阻塞 UI 线程
本文将完整介绍一个 基于 OkHttp + 线程池 + 队列机制的 DownloadManager 实现方案,适用于 App 中 APK、图片、视频等文件下载场景。
一、整体设计思路
核心目标
- 单线程下载队列:保证下载顺序可控
- 异步执行 + 主线程回调
- 支持取消下载 & 取消保存相册
- 下载完成自动保存到相册(图片 / 视频)
- 解耦 UI 层,方便复用
主要结构说明
| 模块 | 作用 |
|---|---|
ExecutorService |
执行下载任务 |
BlockingQueue |
下载任务队列 |
Handler |
回调主线程 |
ConcurrentHashMap |
控制下载 / 保存取消状态 |
OkHttp |
网络下载 |
MediaStore |
保存到系统相册 |
二、核心 DownloadManager 实现
1️⃣ DownloadManager 类结构
java
public class DownloadManager {
private final OkHttpClient client = new OkHttpClient();
private final ExecutorService executorService = Executors.newSingleThreadExecutor(); // 单线程下载
private final ExecutorService saveExecutor = Executors.newFixedThreadPool(2); // 保存相册线程池
private final Map<String, Boolean> cancelFlags = new ConcurrentHashMap<>();
private final Map<String, Boolean> saveCancelFlags = new ConcurrentHashMap<>();
private final Handler mainHandler = new Handler(Looper.getMainLooper());
private final BlockingQueue<DownloadTask> downloadQueue = new LinkedBlockingQueue<>();
private boolean isDownloading = false;
private OnAllDownloadsCompleteListener onAllDownloadsCompleteListener;
2️⃣ 下载回调接口定义
java
public interface DownloadCallback {
void onProgress(String url, int progress);
void onComplete(String url, boolean success);
}
public interface OnAllDownloadsCompleteListener {
void onFinish();
}
3️⃣ 添加下载任务(队列模式)
java
public void enqueueDownload(String url, String savePath, DownloadCallback callback) {
downloadQueue.add(new DownloadTask(url, savePath, callback));
if (!isDownloading) {
startNextDownload();
}
}
4️⃣ 下载核心逻辑(重点)
java
private void startNextDownload() {
DownloadTask task = downloadQueue.poll();
if (task == null) {
isDownloading = false;
if (onAllDownloadsCompleteListener != null) {
mainHandler.post(onAllDownloadsCompleteListener::onFinish);
}
return;
}
isDownloading = true;
cancelFlags.put(task.url, false);
executorService.execute(() -> {
try {
Request request = new Request.Builder().url(task.url).build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
notifyComplete(task, false);
startNextDownload();
return;
}
InputStream inputStream = response.body().byteStream();
long totalSize = response.body().contentLength();
File file = new File(task.savePath);
if (!FileUtils.createFileAndPath(file)) {
notifyComplete(task, false);
startNextDownload();
return;
}
FileOutputStream outputStream = new FileOutputStream(file);
byte[] buffer = new byte[4096];
long downloaded = 0;
int len;
while ((len = inputStream.read(buffer)) != -1) {
if (Boolean.TRUE.equals(cancelFlags.get(task.url))) {
file.delete();
notifyComplete(task, false);
startNextDownload();
return;
}
outputStream.write(buffer, 0, len);
downloaded += len;
int progress = (int) (downloaded * 100 / totalSize);
mainHandler.post(() -> task.callback.onProgress(task.url, progress));
}
outputStream.close();
inputStream.close();
notifyComplete(task, true);
handleSaveToAlbum(task);
} catch (Exception e) {
notifyComplete(task, false);
}
startNextDownload();
});
}
5️⃣ 下载完成回调
java
private void notifyComplete(DownloadTask task, boolean success) {
mainHandler.post(() -> task.callback.onComplete(task.url, success));
}
6️⃣ 取消下载
java
public void cancelDownload(String url) {
cancelFlags.put(url, true);
saveCancelFlags.put(url, true);
}
public void cancelAll() {
for (String url : cancelFlags.keySet()) {
cancelFlags.put(url, true);
saveCancelFlags.put(url, true);
}
downloadQueue.clear();
}
三、下载完成后自动保存到相册
支持区分 图片 / 视频:
java
private void handleSaveToAlbum(DownloadTask task) {
File file = new File(task.savePath);
if (!file.exists()) return;
String path = file.getAbsolutePath().toLowerCase();
//通过路径识别
boolean isPhoto = path.contains("/photo/");
boolean isVideo = path.contains("/video/");
if (!isPhoto && !isVideo) return;
saveCancelFlags.put(task.url, false);
saveExecutor.execute(() -> {
if (Boolean.TRUE.equals(saveCancelFlags.get(task.url))) return;
String mimeType = MediaFileUtils.getMimeTypeFromFile(file);
boolean result = MediaFileUtils.saveFileToAlbum(
MyApplication.getContext(), file, mimeType);
mainHandler.post(() ->
LogUtils.d("DownloadManager", "保存到相册结果:" + result)
);
});
}
四、UI 层使用示例
java
downloadManager = new DownloadManager();
downloadManager.enqueueDownload(appUploadBean.getApkUrl(), filePath,
new DownloadManager.DownloadCallback() {
@Override
public void onProgress(String url, int progress) {
mViewBinding.pbLayout.setProgress(progress);
mViewBinding.tvPrpgress.setText(
"下载中 " + progress + "%"
);
}
@Override
public void onComplete(String url, boolean success) {
if (success) {
mViewBinding.pbLayout.setProgress(100);
mViewBinding.tvPrpgress.setText("下载完成");
} else {
mViewBinding.pbLayout.setProgress(-1);
mViewBinding.tvPrpgress.setText("下载失败");
}
}
});
取消下载
java
if (downloadManager != null) {
downloadManager.cancelAll();
}
五、完成代码
✅ 以下代码可直接复制使用
1.DownloadManager
java
import android.os.Handler;
import android.os.Looper;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import com.haizhen.dvrtesttoolandroid.base.MyApplication;
import com.haizhen.dvrtesttoolandroid.utils.FileUtils;
import com.haizhen.dvrtesttoolandroid.utils.LogUtils;
import com.haizhen.dvrtesttoolandroid.utils.MediaFileUtils;
/**
* Author: Su
* Date: 2025/5/13
* Description: Description
*/
public class DownloadManager {
private final OkHttpClient client = new OkHttpClient();
private final ExecutorService executorService = Executors.newSingleThreadExecutor(); // 单线程下载器
private final Map<String, Boolean> cancelFlags = new ConcurrentHashMap<>(); // 用于标记取消状态,线程安全
private final ExecutorService saveExecutor = Executors.newFixedThreadPool(2); // 保存相册线程池
private final Map<String, Boolean> saveCancelFlags = new ConcurrentHashMap<>(); // 保存取消标记
private final Handler mainHandler = new Handler(Looper.getMainLooper()); // 主线程 Handler
public interface DownloadCallback {
void onProgress(String url, int progress); // 下载进度回调
void onComplete(String url, boolean success); // 下载完成回调
}
public interface OnAllDownloadsCompleteListener {
void onFinish(); // 所有文件下载完成
}
private final BlockingQueue<DownloadTask> downloadQueue = new LinkedBlockingQueue<>(); // 下载任务队列
private OnAllDownloadsCompleteListener onAllDownloadsCompleteListener;
private boolean isDownloading = false; // 当前是否有下载任务在执行
/**
* 设置所有任务完成后的回调监听器
*/
public void setOnAllDownloadsCompleteListener(OnAllDownloadsCompleteListener listener) {
this.onAllDownloadsCompleteListener = listener;
}
/**
* 添加一个下载任务到队列中
*
* @param url 下载链接
* @param savePath 保存路径
* @param callback 下载回调接口
*/
public void enqueueDownload(String url, String savePath, DownloadCallback callback) {
downloadQueue.add(new DownloadTask(url, savePath, callback));
if (!isDownloading) {
startNextDownload();
}
}
/**
* 开始执行下一个下载任务
*/
private void startNextDownload() {
DownloadTask task = downloadQueue.poll();
if (task == null) {
isDownloading = false;
if (onAllDownloadsCompleteListener != null) {
mainHandler.post(onAllDownloadsCompleteListener::onFinish); // 所有任务完成后通知主线程
}
return;
}
isDownloading = true;
cancelFlags.put(task.url, false);
executorService.execute(() -> {
try {
Request request = new Request.Builder().url(task.url).build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
notifyComplete(task, false);
startNextDownload();
return;
}
InputStream inputStream = response.body().byteStream();
long totalSize = response.body().contentLength();
File file = new File(task.savePath);
boolean filePath = FileUtils.createFileAndPath(file);
if (!filePath) {
notifyComplete(task, false);
startNextDownload();
return;
}
FileOutputStream outputStream = new FileOutputStream(file);
byte[] buffer = new byte[4096];
long downloadedSize = 0;
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
if (Boolean.TRUE.equals(cancelFlags.get(task.url))) {
file.delete();
notifyComplete(task, false);
startNextDownload();
return;
}
outputStream.write(buffer, 0, bytesRead);
downloadedSize += bytesRead;
int progress = (int) ((downloadedSize * 100) / totalSize);
int finalProgress = Math.min(progress, 100);
mainHandler.post(() -> task.callback.onProgress(task.url, finalProgress));
}
outputStream.flush();
outputStream.close();
inputStream.close();
notifyComplete(task, true);
// 下载完成后,根据文件类型保存到相册
handleSaveToAlbum(task);
} catch (Exception e) {
notifyComplete(task, false);
}
startNextDownload();
});
}
/**
* 通知下载完成结果(主线程)
*/
private void notifyComplete(DownloadTask task, boolean success) {
mainHandler.post(() -> task.callback.onComplete(task.url, success));
}
/**
* 取消指定 URL 的下载任务
*
* @param url 要取消的任务 URL
*/
public void cancelDownload(String url) {
cancelFlags.put(url, true);
saveCancelFlags.put(url, true);
}
/**
* 取消所有下载任务
*/
public void cancelAll() {
for (String url : cancelFlags.keySet()) {
cancelFlags.put(url, true);
saveCancelFlags.put(url, true);
}
downloadQueue.clear(); // 清空队列中未开始的任务
}
/**
* 下载任务数据结构
*/
private static class DownloadTask {
String url;
String savePath;
DownloadCallback callback;
DownloadTask(String url, String savePath, DownloadCallback callback) {
this.url = url;
this.savePath = savePath;
this.callback = callback;
}
}
// 下载完成后判断是否保存相册
private void handleSaveToAlbum(DownloadTask task) {
File file = new File(task.savePath);
if (!file.exists()) return;
String path = file.getAbsolutePath().toLowerCase();
boolean isPhoto = path.contains("/" + DVRCommand.DOWNLOAD_FILE_PHOTO + "/"); // 判断路径里是否包含 /photo/
boolean isVideo = path.contains("/" + DVRCommand.DOWNLOAD_FILE_VIDEO + "/"); // 判断路径里是否包含 /video/
if (!isPhoto && !isVideo) return; // 其他文件不保存
saveCancelFlags.put(task.url, false);
// 在保存线程池中保存,不阻塞下载线程
saveExecutor.execute(() -> {
if (Boolean.TRUE.equals(saveCancelFlags.get(task.url))) return;
String mimeType = MediaFileUtils.getMimeTypeFromFile(file);
boolean result = MediaFileUtils.saveFileToAlbum(MyApplication.getContext(), file, mimeType);
mainHandler.post(() -> {
LogUtils.d("DownloadManager", "文件保存到相册:" + task.savePath + " result=" + result);
});
});
}
}
2.使用
java
downloadManager = new DownloadManager();
/**
* 开始下载文件列表中的每一个文件,并监听进度与完成状态
*/
private void startDownLoadFile() {
if (downloadManager != null) {
// 所有下载完成后的回调
downloadManager.setOnAllDownloadsCompleteListener(() -> {
if (onAllDownloadsCompleteListener != null) {
onAllDownloadsCompleteListener.onFinish();
}
// 更新按钮文案为"下载完成"
mViewBinding.tvLayout2.setVisibility(View.INVISIBLE);
mViewBinding.llLayout1.setVisibility(View.VISIBLE);
});
}
for (FileNodeBean fileNodeBean : mFileNodeList) {
// 构造下载地址
String downLoadFilePath = DVRCommand.API_DVR_DOWNLOAD_FILE(fileNodeBean.getName());
// 构造本地保存路径
String path = FileUtils.getFilePathDir(mContext) + "/" + mBasePath + "/" + OtherUtils.getFileNameFromPath(fileNodeBean.getName());
// 添加下载任务
downloadManager.enqueueDownload(downLoadFilePath, path, new DownloadManager.DownloadCallback() {
@Override
public void onProgress(String url, int progress) {
// 更新进度并刷新 UI
progressMap.put(url, progress);
downLoadProgressAdapter.setProgressMap(progressMap);
}
@Override
public void onComplete(String url, boolean success) {
// 下载完成(成功或失败),更新 UI
progressMap.put(url, success ? 100 : -1);
downLoadProgressAdapter.setProgressMap(progressMap);
}
});
}
}
/**
* 设置所有下载完成监听器
*/
public void setOnAllDownloadsCompleteListener(DownloadManager.OnAllDownloadsCompleteListener onAllDownloadsCompleteListener) {
this.onAllDownloadsCompleteListener = onAllDownloadsCompleteListener;
}
// 取消所有下载任务
if (downloadManager != null) {
downloadManager.cancelAll();
}
如需 FileUtils 及 MediaFileUtils 请参考一下文章
FileUtils :
MediaFileUtils:MediaFileUtils 文件入口
六、方案优点总结
✅ 支持顺序下载(防止资源竞争)
✅ 支持实时进度回调
✅ 支持取消下载 / 取消保存
✅ 下载与保存解耦,性能友好
✅ 可扩展为多线程下载
✅ 适用于 APK / 图片 / 视频等资源