侧边栏壁纸
博主头像
落叶人生博主等级

走进秋风,寻找秋天的落叶

  • 累计撰写 130562 篇文章
  • 累计创建 28 个标签
  • 累计收到 9 条评论
标签搜索

目 录CONTENT

文章目录

不使用第三方框架编写的多线程断线续传功能

2023-12-10 星期日 / 0 评论 / 0 点赞 / 22 阅读 / 30459 字

一、背景最近需要个断线续传功能,但是觉得一些框架不太适合,所以基于原理编写了一个多线程断线续传功能支持技术分享,但是复制和转发我的博客时候请标明出处,谢谢 https://my.oschina.net

一、背景

最近需要个断线续传功能,但是觉得一些框架不太适合,所以基于原理编写了一个多线程断线续传功能

支持技术分享,但是复制和转发我的博客时候请标明出处,谢谢 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);    }    @Override    public 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();            //请求方式默认为GET            connection.setRequestMethod(mType.getType());            //超时时间            connection.setConnectTimeout(mTimeout);            //从上次下载完成的地方下载            //设置下载位置(从服务器上取要下载文件的某一段)            connection.setRequestProperty("Range", "bytes=" + progress + "-" + total);//设置下载范围            randomAccessFile = new RandomAccessFile(file, "rwd");            //从文件的某一位置开始写入            randomAccessFile.seek(progress);            if (connection.getResponseCode() == 206) {//文件部分下载,返回码为206                InputStream 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);        }    }    @Override    public 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为下载失败 默认为0    protected int status;    //是否暂停下载 true为暂停下载, false代表可以下载, 默认为false    protected 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";    @Override    protected 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());        }    }    @Override    public void onClick(View v) {        try {            switch (v.getId()) {                case R.id.main_btn_down1:                    Log.d(TAG, "点击了下载1,url1:" + url1);                    ThreadUtils.exeMgThread3(new Runnable() {                        @Override                        public 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() {                        @Override                        public 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() {                        @Override                        public void run() {                            Log.v(TAG, "点击暂停1");                            ResumeService.getInstance().stop("1");                        }                    });                    break;                case R.id.main_btn_pause2:                    ThreadUtils.exeMgThread3(new Runnable() {                        @Override                        public 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() {                        @Override                        public void run() {                            ResumeService.getInstance().remove(url1, "1");                        }                    });                    break;                case R.id.main_btn_cancel2:                    ThreadUtils.exeMgThread3(new Runnable() {                        @Override                        public 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;    }    @Override    protected void onDestroy() {        super.onDestroy();        Log.v(TAG, "onDestroy");    }//IDownLoadCallBack 接口 的各种回调 事件 开始    //下载的进度回调    @Override    public 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);        }    }    //下载的停止回调,同时会将暂停状态保存进入数据库    @Override    public void onPause(ResumeEntity resumeEntity, String key, int precent, long downLoadSize, long total) {        Log.v(TAG, "onNext| 下载 暂停 回调方法,resumeEntity:" + resumeEntity);        mHelper.update(resumeEntity);    }    //删除文件回调    @Override    public void onDelete(ResumeEntity resumeEntity, String key) {        Log.v(TAG, "onDelete| 下载 删除 回调方法,resumeEntity:" + resumeEntity);        mHelper.delete(resumeEntity.getFileName());    }    //下载成功的回调    @Override    public void onSuccess(ResumeEntity resumeEntity, String key) {        Log.v(TAG, "onNext| 下载 成功 回调方法,resumeEntity:" + resumeEntity);        resumeEntity.setStatus(ResumeEntity.STATUS.SUCCESS.getValue());        mHelper.update(resumeEntity);    }    //下载失败的回调    @Override    public 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">    <LinearLayout        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:orientation="horizontal">        <ProgressBar            android: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点        <Button            android:id="@+id/main_btn_down1"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="下载1" />        <Button            android:id="@+id/main_btn_pause1"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="暂停1" />        <Button            android:id="@+id/main_btn_cancel1"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="取消1" />    </LinearLayout>    <LinearLayout        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:orientation="horizontal">        <ProgressBar            android:id="@+id/main_progress2"            style="@style/Widget.AppCompat.ProgressBar.Horizontal"            android:layout_width="0dp"            android:layout_height="match_parent"            android:layout_weight="1" />        <Button            android:id="@+id/main_btn_down2"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="下载2" />        <Button            android:id="@+id/main_btn_pause2"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="暂停2" />        <Button            android:id="@+id/main_btn_cancel2"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="取消2" />    </LinearLayout>    <LinearLayout        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:orientation="horizontal">        <ProgressBar            android:id="@+id/main_progress3"            style="@style/Widget.AppCompat.ProgressBar.Horizontal"            android:layout_width="0dp"            android:layout_height="match_parent"            android:layout_weight="1" />        <Button            android:id="@+id/main_btn_down3"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="下载3" />        <Button            android:id="@+id/main_btn_pause3"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="暂停3" />        <Button            android: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" />            <gradient                android: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" />                <gradient                    android: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" />                <gradient                    android:angle="0"                    android:endColor="#8000ff00"                    android:startColor="#80ff0000" />            </shape>        </clip>    </item></layer-list>

广告 广告

评论区