本篇博客主要介绍多线程去下载文件,以下载多个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; }@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;