版权说明 :《OkHttp实现多线程断点续传下载,单例模式下多任务下载管理器,一起抛掉sp,sqlite的辅助吧》于当前CSDN博客乘月网属同一原创,转载请说明出处,谢谢。

最近项目需要使用到断点下载功能,笔者比较喜欢折腾,想方设法抛弃SharedPreferences,尤其是sqlite作记录辅助,改用临时记录文件的形式记录下载进度,本文以断点下载为例。先看看demo运行效果图:

断点续传:记录上次上传(下载)节点位置,下次接着该位置继续上传(下载)。多线程断点续传下载则是根据目标下载文件长度,尽可能地等分给多个线程同时下载文件块,当各个线程全部完成下载后,将文件块合并成一个文件,即目标文件。多线程断点续传不仅为用户避免了断网等突发事故需要重新下载浪费流量的尴尬局面,也大大提高了下载速率,当然,不是线程越多越好,网络带宽才是硬道理!以下为原理图:

java,android中可以使用RandomAccessFile类生成一个同目标文件大小的占位文件,以便于各个线程可以同时操作该文件,并写入各线程实时下载的数据。

下面贴出OkHttp实现的单个多线程下载任务类的DownloadTask.java文件:

package cn.icheny.download;import android.os.Handler;
import android.os.Message;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import okhttp3.Call;
import okhttp3.Response;/*** 多线程下载任务* Created by Cheny on 2017/05/03.*/public class DownloadTask extends Handler {private final int THREAD_COUNT = 4;//下载线程数量private FilePoint mPoint;private long mFileLength;//文件大小private boolean isDownloading = false;//是否正在下载private int childCanleCount;//子线程取消数量private int childPauseCount;//子线程暂停数量private int childFinishCount;//子线程完成下载数量private HttpUtil mHttpUtil;//http网络通信工具private long[] mProgress;//各个子线程下载进度集合private File[] mCacheFiles;//各个子线程下载缓存数据文件private File mTmpFile;//临时占位文件private boolean pause;//是否暂停private boolean cancel;//是否取消下载private final int MSG_PROGRESS = 1;//进度private final int MSG_FINISH = 2;//完成下载private final int MSG_PAUSE = 3;//暂停private final int MSG_CANCEL = 4;//暂停private DownloadListner mListner;//下载回调监听DownloadTask(FilePoint point, DownloadListner l) {this.mPoint = point;this.mListner = l;this.mProgress = new long[THREAD_COUNT];this.mCacheFiles = new File[THREAD_COUNT];this.mHttpUtil = HttpUtil.getInstance();}/*** 开始下载*/public synchronized void start() {try {if (isDownloading) return;isDownloading = true;mHttpUtil.getContentLength(mPoint.getUrl(), new okhttp3.Callback() {@Overridepublic void onResponse(Call call, Response response) throws IOException {if (response.code() != 200) {close(response.body());resetStutus();return;}// 获取资源大小mFileLength = response.body().contentLength();close(response.body());// 在本地创建一个与资源同样大小的文件来占位mTmpFile = new File(mPoint.getFilePath(), mPoint.getFileName() + ".tmp");if (!mTmpFile.getParentFile().exists()) mTmpFile.getParentFile().mkdirs();RandomAccessFile tmpAccessFile = new RandomAccessFile(mTmpFile, "rw");tmpAccessFile.setLength(mFileLength);/*将下载任务分配给每个线程*/long blockSize = mFileLength / THREAD_COUNT;// 计算每个线程理论上下载的数量./*为每个线程配置并分配任务*/for (int threadId = 0; threadId < THREAD_COUNT; threadId++) {long startIndex = threadId * blockSize; // 线程开始下载的位置long endIndex = (threadId + 1) * blockSize - 1; // 线程结束下载的位置if (threadId == (THREAD_COUNT - 1)) { // 如果是最后一个线程,将剩下的文件全部交给这个线程完成endIndex = mFileLength - 1;}download(startIndex, endIndex, threadId);// 开启线程下载}}@Overridepublic void onFailure(Call call, IOException e) {resetStutus();}});} catch (IOException e) {e.printStackTrace();resetStutus();}}/*** 下载* @param startIndex 下载起始位置* @param endIndex  下载结束位置* @param threadId 线程id* @throws IOException*/public void download(final long startIndex, final long endIndex, final int threadId) throws IOException {long newStartIndex = startIndex;// 分段请求网络连接,分段将文件保存到本地.// 加载下载位置缓存数据文件final File cacheFile = new File(mPoint.getFilePath(), "thread" + threadId + "_" + mPoint.getFileName() + ".cache");mCacheFiles[threadId] = cacheFile;final RandomAccessFile cacheAccessFile = new RandomAccessFile(cacheFile, "rwd");if (cacheFile.exists()) {// 如果文件存在String startIndexStr = cacheAccessFile.readLine();try {newStartIndex = Integer.parseInt(startIndexStr);//重新设置下载起点} catch (NumberFormatException e) {e.printStackTrace();}}final long finalStartIndex = newStartIndex;mHttpUtil.downloadFileByRange(mPoint.getUrl(), finalStartIndex, endIndex, new okhttp3.Callback() {@Overridepublic void onResponse(Call call, Response response) throws IOException {if (response.code() != 206) {// 206:请求部分资源成功码,表示服务器支持断点续传resetStutus();return;}InputStream is = response.body().byteStream();// 获取流RandomAccessFile tmpAccessFile = new RandomAccessFile(mTmpFile, "rw");// 获取前面已创建的文件.tmpAccessFile.seek(finalStartIndex);// 文件写入的开始位置./*  将网络流中的文件写入本地*/byte[] buffer = new byte[1024 << 2];int length = -1;int total = 0;// 记录本次下载文件的大小long progress = 0;while ((length = is.read(buffer)) > 0) {//读取流if (cancel) {close(cacheAccessFile, is, response.body());//关闭资源cleanFile(cacheFile);//删除对应缓存文件sendMessage(MSG_CANCEL);return;}if (pause) {//关闭资源close(cacheAccessFile, is, response.body());//发送暂停消息sendMessage(MSG_PAUSE);return;}tmpAccessFile.write(buffer, 0, length);total += length;progress = finalStartIndex + total;//将该线程最新完成下载的位置记录并保存到缓存数据文件中//建议转成Base64码,防止数据被修改,导致下载文件出错(若真有这样的情况,这样的朋友可真是无聊透顶啊)cacheAccessFile.seek(0);cacheAccessFile.write((progress + "").getBytes("UTF-8"));//发送进度消息mProgress[threadId] = progress - startIndex;sendMessage(MSG_PROGRESS);}//关闭资源close(cacheAccessFile, is, response.body());// 删除临时文件cleanFile(cacheFile);//发送完成消息sendMessage(MSG_FINISH);}@Overridepublic void onFailure(Call call, IOException e) {isDownloading = false;}});}/*** 轮回消息回调** @param msg*/@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);if (null == mListner) {return;}switch (msg.what) {case MSG_PROGRESS://进度long progress = 0;for (int i = 0, length = mProgress.length; i < length; i++) {progress += mProgress[i];}mListner.onProgress(progress * 1.0f / mFileLength);break;case MSG_PAUSE://暂停childPauseCount++;if (childPauseCount % THREAD_COUNT != 0) return;//等待所有的线程完成暂停,真正意义的暂停,以下同理resetStutus();mListner.onPause();break;case MSG_FINISH://完成childFinishCount++;if (childFinishCount % THREAD_COUNT != 0) return;mTmpFile.renameTo(new File(mPoint.getFilePath(), mPoint.getFileName()));//下载完毕后,重命名目标文件名resetStutus();mListner.onFinished();break;case MSG_CANCEL://取消childCanleCount++;if (childCanleCount % THREAD_COUNT != 0) return;resetStutus();mProgress = new long[THREAD_COUNT];mListner.onCancel();break;}}/*** 发送消息到轮回器** @param what*/private void sendMessage(int what) {//发送暂停消息Message message = new Message();message.what = what;sendMessage(message);}/*** 关闭资源** @param closeables*/private void close(Closeable... closeables) {int length = closeables.length;try {for (int i = 0; i < length; i++) {Closeable closeable = closeables[i];if (null != closeable)closeables[i].close();}} catch (IOException e) {e.printStackTrace();} finally {for (int i = 0; i < length; i++) {closeables[i] = null;}}}/*** 暂停*/public void pause() {pause = true;}/*** 取消*/public void cancel() {cancel = true;cleanFile(mTmpFile);if (!isDownloading) {//针对非下载状态的取消,如暂停if (null != mListner) {cleanFile(mCacheFiles);resetStutus();mListner.onCancel();}}}/*** 重置下载状态*/private void resetStutus() {pause = false;cancel = false;isDownloading = false;}/*** 删除临时文件*/private void cleanFile(File... files) {for (int i = 0, length = files.length; i < length; i++) {if (null != files[i])files[i].delete();}}/*** 获取下载状态* @return boolean*/public boolean isDownloading() {return isDownloading;}
}

先网络请求获取文件的长度mFileLength,根据长度借助RandomAccessFile类在本地生成相同长度的占位文件mTmpFile,再根据线程数量THREAD_COUNT拆分下载任务,最后for循环出THREAD_COUNT数量的异步请求下载拆分内容(字节)并从mTmpFile的对应位置写入mTmpFile,每个线程(任务)每写入一定的数据后将任务的下载进度写入通过RandomAccessFile生成的对应任务的记录缓存文件中,以便于下次下载读取该线程已下载的进度。注释比较多,好像也没啥好解释的,有问题的朋友下方留言。

在贴上由OkHttp简单封装的网络请求工具类HttpUtil的.java文件:

package cn.icheny.download;import java.io.IOException;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;/*** Http网络工具,基于OkHttp* Created by Cheny on 2017/05/03.*/public class HttpUtil {private OkHttpClient mOkHttpClient;private static HttpUtil mInstance;private final static long CONNECT_TIMEOUT = 60;//超时时间,秒private final static long READ_TIMEOUT = 60;//读取时间,秒private final static long WRITE_TIMEOUT = 60;//写入时间,秒/*** @param url        下载链接* @param startIndex 下载起始位置* @param endIndex   结束为止* @param callback   回调* @throws IOException*/public void downloadFileByRange(String url, long startIndex, long endIndex, Callback callback) throws IOException {// 创建一个Request// 设置分段下载的头信息。 Range:做分段数据请求,断点续传指示下载的区间。格式: Range bytes=0-1024或者bytes:0-1024Request request = new Request.Builder().header("RANGE", "bytes=" + startIndex + "-" + endIndex).url(url).build();doAsync(request, callback);}public void getContentLength(String url, Callback callback) throws IOException {// 创建一个RequestRequest request = new Request.Builder().url(url).build();doAsync(request, callback);}/*** 同步GET请求*/public void doGetSync(String url) throws IOException {//创建一个RequestRequest request = new Request.Builder().url(url).build();doSync(request);}/*** 异步请求*/private void doAsync(Request request, Callback callback) throws IOException {//创建请求会话Call call = mOkHttpClient.newCall(request);//同步执行会话请求call.enqueue(callback);}/*** 同步请求*/private Response doSync(Request request) throws IOException {//创建请求会话Call call = mOkHttpClient.newCall(request);//同步执行会话请求return call.execute();}/*** @return HttpUtil实例对象*/public static HttpUtil getInstance() {if (null == mInstance) {synchronized (HttpUtil.class) {if (null == mInstance) {mInstance = new HttpUtil();}}}return mInstance;}/*** 构造方法,配置OkHttpClient*/private HttpUtil() {//创建okHttpClient对象OkHttpClient.Builder builder = new OkHttpClient.Builder().connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS).writeTimeout(READ_TIMEOUT, TimeUnit.SECONDS).readTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS);mOkHttpClient = builder.build();}
}

header("RANGE", "bytes=" + startIndex + "-" + endIndex),在OkHttp请求头中添加RANGE(范围)参数,告诉服务器需要下载文件内容的始末位置。鉴于OkHttp的火热程度,好像人人都会使用OkHttp,我就不赘言了。

为了更清晰的教程思路,这里也贴出FilePoint.java:

package cn.icheny.download;/*** 目标文件* Created by Cheny on 2017/05/03.*/public class FilePoint {private String fileName;//文件名private String url;//文件urlprivate String filePath;//文件下载路径public FilePoint(String url) {this.url = url;}public FilePoint(String filePath, String url) {this.filePath = filePath;this.url = url;}public FilePoint(String url, String filePath, String fileName) {this.url = url;this.filePath = filePath;this.fileName = fileName;}public String getFileName() {return fileName;}public void setFileName(String fileName) {this.fileName = fileName;}public String getUrl() {return url;}public void setUrl(String url) {this.url = url;}public String getFilePath() {return filePath;}public void setFilePath(String filePath) {this.filePath = filePath;}}

下面是下载管理器DownloadManager 代码,统一管理所有文件的下载任务:

package cn.icheny.download;import android.os.Environment;
import android.text.TextUtils;
import java.io.File;
import java.util.HashMap;
import java.util.Map;/*** 下载管理器,断点续传** @author Cheny*/
public class DownloadManager {private String DEFAULT_FILE_DIR;//默认下载目录private Map<String, DownloadTask> mDownloadTasks;//文件下载任务索引,String为url,用来唯一区别并操作下载的文件private static DownloadManager mInstance;/*** 下载文件*/public void download(String... urls) {for (int i = 0, length = urls.length; i < length; i++) {String url = urls[i];if (mDownloadTasks.containsKey(url)) {mDownloadTasks.get(url).start();}}}/*** 通过url获取下载文件的名称*/public String getFileName(String url) {return url.substring(url.lastIndexOf("/") + 1);}/*** 暂停*/public void pause(String... urls) {for (int i = 0, length = urls.length; i < length; i++) {String url = urls[i];if (mDownloadTasks.containsKey(url)) {mDownloadTasks.get(url).pause();}}}/*** 取消下载*/public void cancel(String... urls) {for (int i = 0, length = urls.length; i < length; i++) {String url = urls[i];if (mDownloadTasks.containsKey(url)) {mDownloadTasks.get(url).cancel();}}}/*** 添加下载任务*/public void add(String url, DownloadListner l) {add(url, null, null, l);}/*** 添加下载任务*/public void add(String url, String filePath, DownloadListner l) {add(url, filePath, null, l);}/*** 添加下载任务*/public void add(String url, String filePath, String fileName, DownloadListner l) {if (TextUtils.isEmpty(filePath)) {//没有指定下载目录,使用默认目录filePath = getDefaultDirectory();}if (TextUtils.isEmpty(fileName)) {fileName = getFileName(url);}mDownloadTasks.put(url, new DownloadTask(new FilePoint(url, filePath, fileName), l));}/*** 获取默认下载目录** @return*/private String getDefaultDirectory() {if (TextUtils.isEmpty(DEFAULT_FILE_DIR)) {DEFAULT_FILE_DIR = Environment.getExternalStorageDirectory().getAbsolutePath()+ File.separator + "icheny" + File.separator;}return DEFAULT_FILE_DIR;}/*** 是否正在下载* @param urls* @return boolean*/public boolean isDownloading(String... urls) {boolean result = false;for (int i = 0, length = urls.length; i < length; i++) {String url = urls[i];if (mDownloadTasks.containsKey(url)) {result = mDownloadTasks.get(url).isDownloading();}}return result;}public static DownloadManager getInstance() {if (mInstance == null) {synchronized (DownloadManager.class) {if (mInstance == null) {mInstance = new DownloadManager();}}}return mInstance;}/*** 初始化下载管理器*/private DownloadManager() {mDownloadTasks = new HashMap<>();}
}

下载管理器通过一个Map将下载链接(url,教程图方便使用url的方式。建议使用其他唯一标识,毕竟一般url长度都很长,会影响一定性能。另外,考虑一个项目中可能需要下载同一个文件到不同的目录,url做索引显得生硬)与对应的下载任务( DownloadTask )绑定在一起,以便于根据url判断或获取对应的下载任务,进行下载,取消和暂停等操作。

OK,时间关系,文章到此结束,有问题或需要Demo源码的朋友下方留言。半夜了。。。浓浓的倦意。。。


        2017年6月2日更新:鉴于CSDN库无缘无故把我以前文章上传的Demo源码以及库弄没了,决定还是传github靠谱,下面贴上Demo源码地址:

        https://github.com/ausboyue/Okhttp-Multiple-Thread-Download-Demo  

临时赶时间写的,难免有些bug,有问题请及时下方反馈。。。

OkHttp实现多线程断点续传下载,单例模式下多任务下载管理器,一起抛掉sp,sqlite的辅助吧相关推荐

  1. 撸了个多线程断点续传下载器,我从中学习到了这些知识(附开源地址)

    2020年6月20日 上海张江 感谢看客老爷点进来了,周末闲来无事,想起同事强哥的那句话:"你有没有玩过断点续传?" 当时转念一想,断点续传下载用的确实不少,具体细节嘛,真的没有去 ...

  2. android 多线程断点续传下载

    今天跟大家一起分享下android开发中比较难的一个环节,可能很多人看到这个标题就会感觉头很大,的确如果没有良好的编码能力和逻辑思维,这块是很难搞明白的,前面2次总结中已经为大家分享过有关技术的一些基 ...

  3. android 多文件多线程断点续传下载

    今天跟大家一起分享下android开发中比较难的一个环节,可能很多人看到这个标题就会感觉头很大,的确如果没有良好的编码能力和逻辑思维,这块是很难搞明白的,前面2次总结中已经为大家分享过有关技术的一些基 ...

  4. android 多线程断点续传下载 三

    android 多线程断点续传下载 三 转载于:https://www.cnblogs.com/zhujiabin/p/5660093.html

  5. android多线程下载原理,安卓多线程断点续传下载功能(靠谱第三方组件,原理demo)...

    一,原生的DownloadManager 从Android 2.3(API level 9)开始,Android以Service的方式提供了全局的DownloadManager来系统级地优化处理长时间 ...

  6. Android进阶:多线程断点续传下载

    今天跟大家一起分享下android开发中比较难的一个环节,可能很多人看到这个标题就会感觉头很大,的确如果没有良好的编码能力和逻辑思维,这块是很难搞明白的. 什么是多线程下载? 多线程下载其实就是迅雷, ...

  7. ASP.NET WebAPi之断点续传下载(下)

    前言 上一篇我们穿插了C#的内容,本篇我们继续来讲讲webapi中断点续传的其他情况以及利用webclient来实现断点续传,至此关于webapi断点续传下载以及上传内容都已经全部完结,一直嚷嚷着把S ...

  8. 多线程断点续传下载软件-闪电下载2009

    今天终于把毕业设计做完了,题目是多线程断点续传的下载软件,从五月初到现在,日以继夜的工作,真是累啊....下面可以开始写论文了,哈哈...下面贴两张图,以示庆贺.. 源文件:LightDown.rar ...

  9. Android多线程断点续传下载原理及实现,移动开发工程师简历

    RandomAccessFile 文件写入 下面再讲讲文件写入问题,由于我们是多线程下载,因此文件并不是每次都是从前往后一个个字节写入的,随时可能在文件的任何一个地方写入数据.因此我们需要能够在文件的 ...

最新文章

  1. MySQL-WorkBench修改MySQL配置注意事项
  2. GMIS 2017嘉宾王小川:人工智能技术与应用思考
  3. Android开发-实现第三方APP跳转
  4. Java防止Xss注入json_浅谈 React 中的 XSS 攻击
  5. IETF:QUIC Version 1 (RFC 9000) 作为标准化版本现已发布
  6. nmon工具的安装记录
  7. iOS UITableView 移除单元格选中时的高亮状态
  8. Linux配置防火墙
  9. phpcms能做什么呢?有什么作用呢?
  10. 华为有国产芯片的服务器吗,华为国产电脑上市!还有一大批国产自主电脑发布:采用国产芯/系统...
  11. 【BAT面试题系列】Java面试必考题JVM详解,BAT师兄深度解析背后原理
  12. matlab epics,基于EPICS/MATLAB图像处理的光束位置测量系统
  13. 通信工程师传输与接入ATM网络组成和接口
  14. ORACLE 触发器控制用户登录之权限限制
  15. 计算机文档里的圆圈,电脑怎么打出圆圈符号?利用word或者输入法打出圆圈的方法介绍...
  16. Now trying to drop the old temporary tablespace, the session hangs.
  17. 上位机.net大佬博客大全-菜鸟学习上位机C#那些事儿
  18. Linux终端编程--termios
  19. Java刀_Java尖刀系列3:堆
  20. 帆软报表列表_帆软入门与报表设计

热门文章

  1. Java字节转字符串
  2. 访问github、人工智能论文网址
  3. Ubuntu离线安装Nvidia显卡驱动
  4. 60个项目管理甘特图模板,可编辑,可下载
  5. 我用rpgmaker mv制作插件版牧场游戏范例
  6. fr3报表的一点小总结
  7. 锁定计算机盘,使用U盘制作开机密码锁定引导密钥盘的三种方法_IT / computer_Professional...
  8. SQL,三种排名函数,用作排名使用
  9. opj 7221 拯救公主
  10. 图像放缩以及亚像素显示