一、背景
最近需要个断线续传功能,但是觉得一些框架不太适合,所以基于原理编写了一个多线程断线续传功能
支持技术分享,但是复制和转发我的博客时候请标明出处,谢谢 https://my.oschina.net/grkj/blog/2907188
二、断线续传的个人理解:
1、断线续传在个人理解,其实就是在出现正常下载流程之外的事情的时候,保存好当前文件下载的进度,然后点击继续下载的时候,从上次的下载进度继续进行下载。
2、如何从上次下载进度继续进行下载呢? 主要就是设置头部信息进行告知实现的
setRequestProperty("Range", "bytes=" + progress + "-" + total);//设置下载范围
三、主要功能有
1、支持多线程断线续传
2、支持回调事件拓展,使用泛型定义对象,支持更加灵活的去拓展对象
3、如果要下载的资源在要保存的文件夹中存在,那么会自动进行下载位置校准和下载
4、支持自定义资源请求的方式(GET和POST方式)和请求超时时间
5、我编不下了,如果你发现了就帮我写上去,谢谢....... 效果图如下
下载3只是装饰,你可以换个地址和修改一下MainActivity的按钮监控那块的代码,抄下载下载1和下载2的代码即可

后面我会完善个功能,只要连上网络就进行检查,然后自动进行资源下载,如果有需要可以给我留言
四、直接上源码讲解
篇幅太长,贴不了那么多,只贴8点 代码下载地址为:点击下载 源码里面DownLoadTask构造函数里面有个地方写错了,写死成了GET方式,如果下载源码的要使用,可以复制下面的DownLoadTask源码进去覆盖掉就好了
1、多线程实例,主要的内容都在这里了
//执行下载的线程
public class DownLoadTask implements Runnable {private static final String TAG = "DownLoadTask";public static final int CACHE_SIZE = 4 * 1024;//缓冲区间,4应该足够了public static final int DEFAULT_TIME_OUT = 5000;//单位是毫秒,默认是5秒,支持自定义//线程安全的资源列表,key是文件名称,value是下载实例private static ConcurrentHashMap<String, DownLoadEntity> mResourceMap = new ConcurrentHashMap<String, DownLoadEntity>();/*** @Description 停止下载* [@author](https://my.oschina.net/arthor) 姚旭民* [@date](https://my.oschina.net/u/2504391) 2018/11/20 16:37*/public static void stop(String key) throws NullPointerException {try {if (key == null)throw new NullPointerException();mResourceMap.get(key).setStop(true);} catch (Exception e) {Log.e(TAG, e.toString());}}/*** [@param](https://my.oschina.net/u/2303379) key 文件凭证* @Description 资源删除* @author 姚旭民* @date 2018/11/20 17:22*/public static void remove(String key) throws NullPointerException {if (key == null || mResourceMap.get(key) == null)throw new NullPointerException("参数为null或者下载数据不存在");mResourceMap.get(key).setDelete(true);}//下载实体DownLoadEntity mDownLoadEntity;//回调对象,只要进行实现,就可以获得各种事件的观察回调,IDownLoadCallBack源码在 第2点 有贴出来IDownLoadCallBack mCallBack;//传输方式,是一个枚举类型,支持自定义传输TransmissionType mType;//下载的超时时间int mTimeout;public DownLoadTask(DownLoadEntity downLoadEntity, IDownLoadCallBack mCallBack) {this(downLoadEntity, mCallBack, TransmissionType.TYPE_GET);}public DownLoadTask(DownLoadEntity downLoadEntity, IDownLoadCallBack mCallBack, TransmissionType type) {this(downLoadEntity, mCallBack, type, DEFAULT_TIME_OUT);}public DownLoadTask(DownLoadEntity downLoadEntity, IDownLoadCallBack mCallBack, TransmissionType type, int timeout) {this.mDownLoadEntity = downLoadEntity;this.mCallBack = mCallBack;this.mType = type;this.mTimeout = timeout;//数据存储mResourceMap.put(downLoadEntity.getKey(), downLoadEntity);Log.v(TAG, "存放数据进入键值对,key:" + downLoadEntity.getKey() + ",downLoadEntity:" + downLoadEntity);}@Overridepublic void run() {//下载路径String downUrl = mDownLoadEntity.getDownUrl();//保存路径String savePath = mDownLoadEntity.getSavePath();//已经下载的进度long progress = mDownLoadEntity.getProgress();//已经下载好的长度long total = mDownLoadEntity.getTotal();//文件的总长度String key = mDownLoadEntity.getKey();HttpURLConnection connection = null;//有人可能觉得NIO 的 FileChannel 也可以的话,那么你也可以替换掉RandomAccessFile randomAccessFile = null;try {//设置文件写入位置File file = new File(savePath);//父类文件夹是否存在File fileParent = file.getParentFile();if (!fileParent.exists()) {//如果父类文件夹不存在,即创建文件夹Log.v(TAG, "父类文件夹:" + fileParent.getPath() + ",不存在,开始创建");fileParent.mkdirs();}if (file != null) {//这一步是针对于断线续传的文件,用于比对数据库和真实的数据,避免出现误差long fileSize = file.length();if (progress != fileSize) {//如果文件有问题,以实际下载的文件大小为准Log.v(TAG, "文件传输节点不一致,开始修复传数据节点");progress = fileSize;mDownLoadEntity.setProgress(progress);}}int precent = (int) ((float) progress / (float) total * 100);//开始下载之前先回调开始下载的进度mCallBack.onNext(key, precent);URL url = new URL(downUrl);connection = (HttpURLConnection) url.openConnection();//请求方式默认为GETconnection.setRequestMethod(mType.getType());//超时时间connection.setConnectTimeout(mTimeout);//从上次下载完成的地方下载//设置下载位置(从服务器上取要下载文件的某一段)connection.setRequestProperty("Range", "bytes=" + progress + "-" + total);//设置下载范围randomAccessFile = new RandomAccessFile(file, "rwd");//从文件的某一位置开始写入randomAccessFile.seek(progress);if (connection.getResponseCode() == 206) {//文件部分下载,返回码为206InputStream is = connection.getInputStream();byte[] buffer = new byte[CACHE_SIZE];//接收到的资源大小int len;while ((len = is.read(buffer)) != -1) {//写入文件randomAccessFile.write(buffer, 0, len);progress += len;precent = (int) ((float) progress / (float) total * 100);//更新进度回调mCallBack.onNext(key, precent);//停止下载if (mDownLoadEntity.isStop()) {mDownLoadEntity.setProgress(progress);mCallBack.onPause(mDownLoadEntity, key, precent, progress, total);return;}//取消下载if (mDownLoadEntity.isDelete()) {mResourceMap.remove(key);//文件删除file.delete();mCallBack.onDelete(mDownLoadEntity, key);return;}}}//资源删除mResourceMap.remove(mDownLoadEntity.getFileName());//下载完成mCallBack.onSuccess(mDownLoadEntity, key);} catch (Exception e) {//资源删除mResourceMap.remove(mDownLoadEntity.getFileName());mDownLoadEntity.setProgress(progress);//防止意外mDownLoadEntity.setStop(false);//失败原因回调mCallBack.onFail(mDownLoadEntity, key, e.toString());StringBuffer sb = new StringBuffer();Writer writer = new StringWriter();PrintWriter printWriter = new PrintWriter(writer);e.printStackTrace(printWriter);Throwable cause = e.getCause();while (cause != null) {cause.printStackTrace(printWriter);cause = cause.getCause();}printWriter.close();//异常的详细内容String result = writer.toString();Log.e(TAG, result);} finally {if (connection != null) {connection.disconnect();}try {if (randomAccessFile != null) {randomAccessFile.close();}} catch (IOException e) {StringBuffer sb = new StringBuffer();Writer writer = new StringWriter();PrintWriter printWriter = new PrintWriter(writer);e.printStackTrace(printWriter);Throwable cause = e.getCause();while (cause != null) {cause.printStackTrace(printWriter);cause = cause.getCause();}printWriter.close();//异常的详细内容String result = writer.toString();Log.e(TAG, result);}}}
}
2、IDownLoadCallBack源码,这里的泛型主要是因为和公司一些业务有关,这里没有列出来,这里的泛型其实可以去掉的,因为基本这里没什么用的,T 都改成 DownLoadEntity实例即可
public interface IDownLoadCallBack<T> {/*** @param key 下载的文件的标识,主要用于显示的时候辨别是哪个文件在操作,由使用的人去定义* @param precent 已经下载的百分比 取值区间为 [0,100]* @Description* @author 姚旭民* @date 2018/11/20 9:46*/public abstract void onNext(String key, int precent);/*** @param t 下载的文件的实体封装类* @param key 下载的文件的标识,主要用于显示的时候辨别是哪个文件在操作,由使用的人去定义* @param precent 已经下载的百分比* @param downLoadSize 已经下载的长度* @param total 资源的总长度* @Description* @author 姚旭民* @date 2018/11/20 10:48*/public abstract void onPause(T t, String key, int precent, long downLoadSize, long total);/*** @Description 删除文件回调* @author 姚旭民* @date 2018/11/22 10:47** @param t 操作的下载对象* @param key 文件凭证*/public abstract void onDelete(T t, String key);/*** @param t 自定义的值* @param key 下载的文件的标识,主要用于显示的时候辨别是哪个文件在操作,由使用的人去定义* @Description* @author 姚旭民* @date 2018/11/20 9:46*/public abstract void onSuccess(T t, String key);/*** @param t 自定义的值* @param key 下载的文件的标识,主要用于显示的时候辨别是哪个文件在操作,由使用的人去定义* @param reason 失败原因* @Description* @author 姚旭民* @date 2018/11/20 9:46*/public abstract void onFail(T t, String key, String reason);
3、IDownLoadCallBack包装类继承,包装类用于包装泛型对象,其实这一步可以不要的,只是有点别的考虑,所以这样写
/*** @Description 包装类* @author 姚旭民* @date 2018/11/20 13:57*/
public interface IResumeCallBack extends IDownLoadCallBack<ResumeEntity> {
}
4、ResumeEntity对象源码主要继承了DownLoadEntity(第5点),其他没什么的
public class ResumeEntity extends DownLoadEntity {public static enum STATUS {FAIL(-1),//下载失败DOWNLOAD(0),//下载中SUCCESS(1);//下载成功,可以使用private int value;private STATUS(int value) {this.value = value;}public int getValue() {return value;}}ResumeEntity(builder builder) {this.fileName = builder.fileName;this.downUrl = builder.downUrl;this.savePath = builder.savePath;this.total = builder.total;this.progress = builder.progress;this.status = builder.status;this.key = builder.key;}//链式编程,防止对象不一致,用static修饰,避免被保留强引用public static class builder {private String fileName;private String downUrl;private String savePath;private long total;private long progress;private int status;private boolean stop;private String key;public builder fileName(String fileName) {this.fileName = fileName;return this;}public builder downUrl(String downUrl) {this.downUrl = downUrl;return this;}public builder savePath(String savePath) {this.savePath = savePath;return this;}public builder total(long total) {this.total = total;return this;}public builder progress(long progress) {this.progress = progress;return this;}public builder status(int status) {this.status = status;return this;}public builder stop(boolean stop) {this.stop = stop;return this;}public builder key(String key) {this.key = key;return this;}public ResumeEntity builder() {return new ResumeEntity(this);}}@Overridepublic String toString() {return "{" +"fileName='" + fileName + '\'' +", downUrl='" + downUrl + '\'' +", savePath='" + savePath + '\'' +", total=" + total +", progress=" + progress +", status=" + status +", stop=" + stop +", key='" + key + '\'' +'}';}
}
5、DownLoadEntity源码区域
public class DownLoadEntity {//资源文件的名称protected String fileName;//资源文件的下载路径protected String downUrl;//资源文件的保存完整路径protected String savePath;//下载的资源的总长度protected long total;//已经下载的进度protected long progress;//资源的状态 //下载的状况 1为下载成功,0为可下载, -1为下载失败 默认为0protected int status;//是否暂停下载 true为暂停下载, false代表可以下载, 默认为falseprotected boolean stop;//下载的文件的标识,让使用者更加灵活的去定义如何识别正在下载的文件protected String key;//是否删除下载的文件protected boolean delete;//这里是各种set和get,不花费篇幅粘贴了,直接用工具生成就好了
}
6、IDownLoadCallBack的实现类,我是不想每次都创建一个匿名类了,太长了也繁琐,我直接用activity去实现IDownLoadCallBack,感觉也挺好的,这里是随便写的activity,主要用来测试的,UI界面源码在第7点
public class MainActivity extends AppCompatActivity implements View.OnClickListener, IResumeCallBack, INetCallBack {private static final String TAG = "MainActivity";//数据库操作辅助类private ResumeDbHelper mHelper = ResumeDbHelper.getInstance();private ResumeService mResumeService = ResumeService.getInstance();private MainActivity mInstance = this;private Button downloadBtn1, downloadBtn2, downloadBtn3;private Button pauseBtn1, pauseBtn2, pauseBtn3;private Button cancelBtn1, cancelBtn2, cancelBtn3;private ProgressBar mProgress1, mProgress2, mProgress3;private String url1 = "http://192.168.1.103/2.bmp";private String url2 = "http://192.168.1.103/testzip.zip";private String url3 = "http://192.168.1.103/photo.png";@Overrideprotected void onCreate(Bundle savedInstanceState) {try {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);NetReceiver.setCallBack(this);downloadBtn1 = bindView(R.id.main_btn_down1);downloadBtn2 = bindView(R.id.main_btn_down2);downloadBtn3 = bindView(R.id.main_btn_down3);pauseBtn1 = bindView(R.id.main_btn_pause1);pauseBtn2 = bindView(R.id.main_btn_pause2);pauseBtn3 = bindView(R.id.main_btn_pause3);cancelBtn1 = bindView(R.id.main_btn_cancel1);cancelBtn2 = bindView(R.id.main_btn_cancel2);cancelBtn3 = bindView(R.id.main_btn_cancel3);mProgress1 = bindView(R.id.main_progress1);mProgress2 = bindView(R.id.main_progress2);mProgress3 = bindView(R.id.main_progress3);downloadBtn1.setOnClickListener(this);downloadBtn2.setOnClickListener(this);downloadBtn3.setOnClickListener(this);pauseBtn1.setOnClickListener(this);pauseBtn2.setOnClickListener(this);pauseBtn3.setOnClickListener(this);cancelBtn1.setOnClickListener(this);cancelBtn2.setOnClickListener(this);cancelBtn3.setOnClickListener(this);} catch (Exception e) {Log.e(TAG, e.toString());}}@Overridepublic void onClick(View v) {try {switch (v.getId()) {case R.id.main_btn_down1:Log.d(TAG, "点击了下载1,url1:" + url1);ThreadUtils.exeMgThread3(new Runnable() {@Overridepublic void run() {mResumeService.download("1", url1, FileConts.IMG_PATH, mInstance);}});break;case R.id.main_btn_down2:Log.d(TAG, "点击了下载2");ThreadUtils.exeMgThread3(new Runnable() {@Overridepublic void run() {mResumeService.download("2", url2, FileConts.IMG_PATH, mInstance);}});break;case R.id.main_btn_down3:Log.d(TAG, "点击了下载3");break;case R.id.main_btn_pause1:ThreadUtils.exeMgThread3(new Runnable() {@Overridepublic void run() {Log.v(TAG, "点击暂停1");ResumeService.getInstance().stop("1");}});break;case R.id.main_btn_pause2:ThreadUtils.exeMgThread3(new Runnable() {@Overridepublic void run() {Log.v(TAG, "点击暂停2");ResumeService.getInstance().stop("2");}});break;case R.id.main_btn_pause3:
// ResumeService.getInstance().cancel(url3);break;case R.id.main_btn_cancel1:ThreadUtils.exeMgThread3(new Runnable() {@Overridepublic void run() {ResumeService.getInstance().remove(url1, "1");}});break;case R.id.main_btn_cancel2:ThreadUtils.exeMgThread3(new Runnable() {@Overridepublic void run() {ResumeService.getInstance().remove(url2, "2");}});break;case R.id.main_btn_cancel3:
// ResumeService.getInstance().cancel(url3);break;}} catch (Exception e) {StringBuffer sb = new StringBuffer();Writer writer = new StringWriter();PrintWriter printWriter = new PrintWriter(writer);e.printStackTrace(printWriter);Throwable cause = e.getCause();while (cause != null) {cause.printStackTrace(printWriter);cause = cause.getCause();}printWriter.close();//异常的详细内容String result = writer.toString();Log.e(TAG, result);}}private <T extends View> T bindView(@IdRes int id) {View viewById = findViewById(id);return (T) viewById;}@Overrideprotected void onDestroy() {super.onDestroy();Log.v(TAG, "onDestroy");}//IDownLoadCallBack 接口 的各种回调 事件 开始//下载的进度回调@Overridepublic void onNext(String key, int precent) {if ("1".equals(key)) {mProgress1.setMax(100);mProgress1.setProgress(precent);} else if ("2".equals(key)) {mProgress2.setMax(100);mProgress2.setProgress(precent);}}//下载的停止回调,同时会将暂停状态保存进入数据库@Overridepublic void onPause(ResumeEntity resumeEntity, String key, int precent, long downLoadSize, long total) {Log.v(TAG, "onNext| 下载 暂停 回调方法,resumeEntity:" + resumeEntity);mHelper.update(resumeEntity);}//删除文件回调@Overridepublic void onDelete(ResumeEntity resumeEntity, String key) {Log.v(TAG, "onDelete| 下载 删除 回调方法,resumeEntity:" + resumeEntity);mHelper.delete(resumeEntity.getFileName());}//下载成功的回调@Overridepublic void onSuccess(ResumeEntity resumeEntity, String key) {Log.v(TAG, "onNext| 下载 成功 回调方法,resumeEntity:" + resumeEntity);resumeEntity.setStatus(ResumeEntity.STATUS.SUCCESS.getValue());mHelper.update(resumeEntity);}//下载失败的回调@Overridepublic void onFail(ResumeEntity resumeEntity, String key, String reason) {Log.v(TAG, "onFail| 下载 失败 回调方法,resumeEntity:" + resumeEntity + ",reason:" + reason);resumeEntity.setStatus(ResumeEntity.STATUS.FAIL.getValue());mHelper.update(resumeEntity);}//IDownLoadCallBack 接口 的各种回调 事件 结束//网络状态回调区域,这里是我用来接着编写网络重连之后继续下载的东西的public void onStatusChange(String msg) {Log.v(TAG, "onStatusChange| 网络状态回调,内容为:" + msg);}
}
7、UI界面
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/activity_main"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context="com.yxm.resume.activity.MainActivity"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><ProgressBarandroid:id="@+id/main_progress1"style="@style/Widget.AppCompat.ProgressBar.Horizontal"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:progressDrawable="@drawable/progressbar" /> 进度条样式在第8点<Buttonandroid:id="@+id/main_btn_down1"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="下载1" /><Buttonandroid:id="@+id/main_btn_pause1"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="暂停1" /><Buttonandroid:id="@+id/main_btn_cancel1"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="取消1" /></LinearLayout><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><ProgressBarandroid:id="@+id/main_progress2"style="@style/Widget.AppCompat.ProgressBar.Horizontal"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1" /><Buttonandroid:id="@+id/main_btn_down2"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="下载2" /><Buttonandroid:id="@+id/main_btn_pause2"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="暂停2" /><Buttonandroid:id="@+id/main_btn_cancel2"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="取消2" /></LinearLayout><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><ProgressBarandroid:id="@+id/main_progress3"style="@style/Widget.AppCompat.ProgressBar.Horizontal"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1" /><Buttonandroid:id="@+id/main_btn_down3"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="下载3" /><Buttonandroid:id="@+id/main_btn_pause3"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="暂停3" /><Buttonandroid:id="@+id/main_btn_cancel3"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="取消3" /></LinearLayout>
</LinearLayout>
8、进度条样式
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" ><item android:id="@android:id/background"><shape><corners android:radius="5dip" /><gradientandroid:angle="0"android:centerColor="#ff5a5d5a"android:centerY="0.75"android:endColor="#ff747674"android:startColor="#ff9d9e9d" /></shape></item><item android:id="@android:id/secondaryProgress"><clip><shape><corners android:radius="5dip" /><gradientandroid:angle="0"android:centerColor="#80ffb600"android:centerY="0.75"android:endColor="#a0ffcb00"android:startColor="#80ffd300" /></shape></clip></item><item android:id="@android:id/progress"><clip><shape><corners android:radius="5dip" /><gradientandroid:angle="0"android:endColor="#8000ff00"android:startColor="#80ff0000" /></shape></clip></item></layer-list>