美文网首页Android收藏集Android开发Android技术知识
Android实现多线程下载文件,支持断点

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

作者: 皮卡丘520 | 来源:发表于2018-04-26 11:38 被阅读959次

    本篇博客主要介绍多线程去下载文件,以下载多个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<DownloadTask> readyTasks = new ArrayDeque<>();
        private final Deque<DownloadTask> runningTasks = new ArrayDeque<>();
        //private final Deque<DownloadTask> 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<Runnable>(), 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 <= -1) {
                    return;
                }
                DownloadTask downloadTask = new DownloadTask(name, url, THREAD_SIZE, contentLength, callBack);
                downloadTask.init();
                runningTasks.add(downloadTask);
            }
        });
    }
    
    
    /**
     * @param downLoadTask 下载任务
     */
    public void recyclerTask(DownloadTask downLoadTask) {
        runningTasks.remove(downLoadTask);
        //参考OkHttp的Dispatcher()的源码
        //readyTasks.
    }
    
    public void stopDownLoad(String url) {
        //这个停止是不是这个正在下载的
    }
    }
    

    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<DownloadRunnable> 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 < mThreadSize; i++) {
            //初始化的时候,需要读取数据库
            //每个线程的下载的大小threadSize
            long threadSize = mContentLength / mThreadSize;
            //开始下载的位置
            long start = i * threadSize;
            //结束下载的位置
            long end = start + threadSize - 1;
            if (i == mThreadSize - 1) {
                end = mContentLength - 1;
            }
            DownloadRunnable downloadRunnable = new DownloadRunnable(name, url, mContentLength, i, start, end, new DownloadCallback() {
                @Override
                public void onFailure(Exception e) {
                    //有一个线程发生异常,下载失败,需要把其它线程停止掉
                    mDownloadCallback.onFailure(e);
                    stopDownload();
                }
    
                @Override
                public void onSuccess(File file) {
                    mSuccessNumber = mSuccessNumber + 1;
                    if (mSuccessNumber == mThreadSize) {
                        mDownloadCallback.onSuccess(file);
                        DownloadDispatcher.getInstance().recyclerTask(DownloadTask.this);
                        //如果下载完毕,清除数据库  todo
                    }
                }
    
                @Override
                public void onProgress(long progress, long currentLength) {
                    //叠加下progress,实时去更新进度条
                    //这里需要synchronized下
                    synchronized (DownloadTask.this) {
                        mTotalProgress = mTotalProgress + progress;
                        //Log.i(TAG, "mTotalProgress==" + mTotalProgress);
                        mDownloadCallback.onProgress(mTotalProgress, currentLength);
                    }
                }
            });
            //通过线程池去执行
            DownloadDispatcher.getInstance().executorService().execute(downloadRunnable);
            mDownloadRunnables.add(downloadRunnable);
        }
    }
    
    /**
     * 停止下载
     */
    public void stopDownload() {
        for (DownloadRunnable runnable : mDownloadRunnables) {
            runnable.stop();
        }
    }
    

    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;
    }
    
    @Override
    public 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;

    项目完整代码https://github.com/StevenYan88/MultiThreadDownload

    qrcode_for_gh_5a8632091879_344.jpg
    关注公众号,定期分享android技术干货、android资讯!

    相关文章

      网友评论

      本文标题:Android实现多线程下载文件,支持断点

      本文链接:https://www.haomeiwen.com/subject/xrkplftx.html