ITPub博客

首页 > IT职业 > IT图书 > Android实现多线程下载文件,支持断点

Android实现多线程下载文件,支持断点

IT图书 作者:sprest 时间:2021-09-09 09:37:16 0 删除 编辑

本篇博客主要介绍多线程去下载文件,以下载多个apk为例。不管去下在apk,音视频等文件,实现起来都一样。
篇幅有点长,先贴张美女图看看

图片描述

meinv.jpg

正在下载的效果图

图片描述

2018-04-25_13_36_47.gif


下载完成效果图

图片描述

screen.png


小编的下载路径是放在sd卡的绝对路径中,方便验证!

工程目录图

图片描述

content.png


介绍下每个类是干什么的

DownloadCallback:下载完成回调接口,包含三个方法 void onSuccess(File file)、void onFailure(Exception e)、void onProgress(long progress,long currentLength);
DownloadDispatcher:负责创建线程池,连接下载的文件;
DownloadRunnable:每个线程的执行对应的任务;
DownloadTask:每个apk的下载,这个类需要复用的;
OkHttpManager:封装下okhttp,管理okhttp;
CircleProgressbar:自定义的圆形进度条;

具体思路:

1、首先自定义一个圆形进度条CircleProgressbar
2、创建线程池,计算每个线程对应的不同的Range
3、每个线程下载完毕之后的回调,若出现了异常怎么处理

OkHttpManager类

public class OkHttpManager {private static final OkHttpManager sOkHttpManager = new OkHttpManager();private OkHttpClient okHttpClient;private OkHttpManager() {
    okHttpClient = new OkHttpClient();
}public static OkHttpManager getInstance() {    return sOkHttpManager;
}public Call asyncCall(String url) {

    Request request = new Request.Builder()
            .url(url)
            .build();    return okHttpClient.newCall(request);
}public Response syncResponse(String url, long start, long end) throws IOException {
    Request request = new Request.Builder()
            .url(url)            //Range 请求头格式Range: bytes=start-end
            .addHeader("Range", "bytes=" + start + "-" + end)
            .build();    return okHttpClient.newCall(request).execute();
}
}

大家可能会看到这个Range很懵,Range是啥?

什么是Range?  
当用户在听一首歌的时候,如果听到一半(网络下载了一半),网络断掉了,用户需要继续听的时候,文件服务器不支持断点的话,则用户需要重新下载这个文件。而Range支持的话,客户端应该记录了之前已经读取的文件范围,网络恢复之后,则向服务器发送读取剩余Range的请求,服务端只需要发送客户端请求的那部分内容,而不用整个  文件发送回客户端,以此节省网络带宽。

例如:
Range: bytes=10- :第10个字节及最后个字节的数据 。
Range: bytes=40-100 :第40个字节到第100个字节之间的数据。
注意,这个表示[start,end],即是包含请求头的start及end字节的,所以,下一个请求,应该是上一个请求的[end+1, nextEnd]

DownloadCallback类

public interface DownloadCallback {    /**
     * 下载成功
     *
     * @param file
     */
    void onSuccess(File file);    /**
     * 下载失败
     *
     * @param e
     */
    void onFailure(Exception e);    /**
     * 下载进度
     *
     * @param progress
     */
    void onProgress(long progress,long currentLength);
}

DownloadCallback:下载完成回调接口,包含三个方法 void onSuccess(File file)下载文件成功回调、void onFailure(Exception e)下载文件失败回调、void onProgress(long progress,long currentLength) 下载文件实时更新下圆形进度条。

DownloadDispatcher 类

public class DownloadDispatcher {    private static final String TAG = "DownloadDispatcher";    private static volatile DownloadDispatcher sDownloadDispatcher;    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();    private static final int THREAD_SIZE = Math.max(3, Math.min(CPU_COUNT - 1, 5));    //核心线程数
    private static final int CORE_POOL_SIZE = THREAD_SIZE;    //线程池
    private ExecutorService mExecutorService;    //private final Deque readyTasks = new ArrayDeque();
    private final Deque runningTasks = new ArrayDeque();    //private final Deque stopTasks = new ArrayDeque();private DownloadDispatcher() {
}public static DownloadDispatcher getInstance() {    if (sDownloadDispatcher == null) {        synchronized (DownloadDispatcher.class) {            if (sDownloadDispatcher == null) {
                sDownloadDispatcher = new DownloadDispatcher();
            }
        }
    }    return sDownloadDispatcher;
}/**
 * 创建线程池
 *
 * @return mExecutorService
 */public synchronized ExecutorService executorService() {    if (mExecutorService == null) {
        mExecutorService = new ThreadPoolExecutor(CORE_POOL_SIZE, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,                new SynchronousQueue(), new ThreadFactory() {            @Override
            public Thread newThread(@NonNull Runnable r) {
                Thread thread = new Thread(r);
                thread.setDaemon(false);                return thread;
            }
        });
    }    return mExecutorService;
}/**
 * @param name     文件名
 * @param url      下载的地址
 * @param callBack 回调接口
 */public void startDownload(final String name, final String url, final DownloadCallback callBack) {
    Call call = OkHttpManager.getInstance().asyncCall(url);
    call.enqueue(new Callback() {        @Override
        public void onFailure(@NonNull Call call, @NonNull IOException e) {
            callBack.onFailure(e);
        }        @Override
        public void onResponse(@NonNull Call call, @NonNull Response response) {            //获取文件的大小
            long contentLength = response.body().contentLength();
            Log.i(TAG, "contentLength=" + contentLength);            if (contentLength 

DownloadDispatcher这个类主要负责创建线程池,连接下载的文件,如果你要控制下载文件的个数,比如3-5个,可以在这个类控制,比如你最大允许同时下载三个文件,每个文件有五个线程去下载,那么maxRequest只有15个线程,其余的可以放到readyTasks 中,有一个线程下载完毕了可以remove()掉,总结起来说一句话,去仿照okhttp的Dispatcher源码去写,runningTasks、readyTasks、stopTasks。

DownloadTask

public class DownloadTask {    private static final String TAG = "DownloadTask";    //文件下载的url
    private String url;    //文件的名称
    private String name;    //文件的大小
    private long mContentLength;    //下载文件的线程的个数
    private int mThreadSize;    //线程下载成功的个数,变量加个volatile,多线程保证变量可见性
    private volatile int mSuccessNumber;    //总进度=每个线程的进度的和
    private long mTotalProgress;    private List mDownloadRunnables;    private DownloadCallback mDownloadCallback;public DownloadTask(String name, String url, int threadSize, long contentLength, DownloadCallback callBack) {    this.name = name;    this.url = url;    this.mThreadSize = threadSize;    this.mContentLength = contentLength;    this.mDownloadRunnables = new ArrayList();    this.mDownloadCallback = callBack;
}public void init() {    for (int i = 0; i 

DownloadTask负责每个apk的下载,这个类需要复用的。计算每个线程下载范围的大小,具体的每个变量是啥?注释写的很清楚。注意的是这个变量mSuccessNumber,线程下载成功的个数,变量加个volatile,多线程保证变量可见性。还有的就是叠加下progress的时候mTotalProgress = mTotalProgress + progress,需要synchronized(DownloadTask.this)下,保证这个变量mTotalProgress内存可见,并同步下。

DownloadRunnable类

public class DownloadRunnable implements Runnable {    private static final String TAG = "DownloadRunnable";    private static final int STATUS_DOWNLOADING = 1;    private static final int STATUS_STOP = 2;    //线程的状态
    private int mStatus = STATUS_DOWNLOADING;    //文件下载的url
    private String url;    //文件的名称
    private String name;    //线程id
    private int threadId;    //每个线程下载开始的位置
    private long start;    //每个线程下载结束的位置
    private long end;    //每个线程的下载进度
    private long mProgress;    //文件的总大小 content-length
    private long mCurrentLength;    private DownloadCallback downloadCallback;public DownloadRunnable(String name, String url, long currentLength, int threadId, long start, long end, DownloadCallback downloadCallback) {    this.name = name;    this.url = url;    this.mCurrentLength = currentLength;    this.threadId = threadId;    this.start = start;    this.end = end;    this.downloadCallback = downloadCallback;
}@Overridepublic void run() {
    InputStream inputStream = null;
    RandomAccessFile randomAccessFile = null;    try {
        Response response = OkHttpManager.getInstance().syncResponse(url, start, end);
        Log.i(TAG, "fileName=" + name + " 每个线程负责下载文件大小contentLength=" + response.body().contentLength()
                + " 开始位置start=" + start + "结束位置end=" + end + " threadId=" + threadId);
        inputStream = response.body().byteStream();        //保存文件的路径
        File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), name);
        randomAccessFile = new RandomAccessFile(file, "rwd");        //seek从哪里开始
        randomAccessFile.seek(start);        int length;        byte[] bytes = new byte[10 * 1024];        while ((length = inputStream.read(bytes)) != -1) {            if (mStatus == STATUS_STOP)                break;            //写入
            randomAccessFile.write(bytes, 0, length);            //保存下进度,做断点 todo
            mProgress = mProgress + length;            //实时去更新下进度条,将每次写入的length传出去
            downloadCallback.onProgress(length, mCurrentLength);
        }
        downloadCallback.onSuccess(file);
    } catch (IOException e) {
        e.printStackTrace();
        downloadCallback.onFailure(e);
    } finally {
        Utils.close(inputStream);
        Utils.close(randomAccessFile);        //保存到数据库 怎么存?? todo
    }
}public void stop() {
    mStatus = STATUS_STOP;
}
}

DownloadRunnable负责每个线程的执行对应的任务,使用RandomAccessFile写入文件。最后看一张截图

图片描述

D2OD.png

哈,看到最后断点下载并没有实现,这个不急,小编还没写,但是实现多线程下载文件整体的思路和代码都已经出来了,至于断点怎么弄,其实具体的思路,在代码注解也已经写出来了,主要是DownloadRunnable 这个类,向数据库保存下文件的下载路径url,文件的大小currentLength,每个线程id,对应的每个线程的start位置和结束位置,以及每个线程的下载进度progress。用户下次进来可以读取数据库的内容,有网的情况下重新发请请求,对应Range: bytes=start-end;


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/3088/viewspace-2805258/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2019-08-13

  • 博文量
    101
  • 访问量
    821