Android 下载管理器封装实战:支持队列下载、取消、进度回调与自动保存相册

在实际项目中,我们经常会遇到如下需求:

  • 多文件顺序下载(避免并发过多)
  • 支持下载进度回调
  • 支持取消单个 / 全部下载任务
  • 下载完成后自动保存到相册(区分图片 / 视频)
  • 下载过程不阻塞 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 / 图片 / 视频等资源

相关推荐
wanghowie2 小时前
01.01 Spring核心|IoC容器深度解析
java·后端·spring
人道领域2 小时前
【零基础学java】(Map集合)
java·开发语言
@淡 定2 小时前
Seata AT模式详细实例:电商下单场景
java
杀死那个蝈坦2 小时前
JUC并发编程day1
java·开发语言
飞Link2 小时前
【Java】Linux(CentOS7)下安装JDK8(Java)教程
java·linux·运维·服务器
秋4272 小时前
基于tomcat的动静分离
java·tomcat
巨人张2 小时前
C++零基础游戏----“大鱼吃小鱼”
java·c++·游戏
伯明翰java2 小时前
Java接口
java·开发语言
凡小烦2 小时前
看完你就是古希腊掌管Compose输入框的神!!!
android·kotlin