Android【封装】多线程下载以及断点续传

多线程下载,以及断点续传


  • 效果图
  • 多线程下载的原理
  • 需要解决的几个问题
  • 理清思路
  • 简单封装核心代码(有修改,加入了线程池,具体看源码)
  • 使用方法
  • 源码下载地址

1.效果图这里写图片描述

2.多线程下载的原理

通常服务器同时与多个用户连接,用户之间共享带宽。如果N个用户的优先级都相同,那么每个用户连接到该服务器上的实际带宽就是服务器带宽的N分之一。可以想象,如果用户数目较多,则每个用户只能占有可怜的一点带宽,下载将会是个漫长的过程。
这里写图片描述
假设服务器的带宽为20M/s,服务器上有很多电影资源,现在有三位同学都想要下载 小泽.avi 这部电影,现在三位同学都在下载,所以每位同学的速度应该为1/3 * 20M/s = 6.7M/s ,但是 小泽.avi 这部电影的大小有 2G左右,这时王五同学可能有点赶时间,等不及,下的这么慢,所以他就使用他所学的多线程的知识多开了几个线程,结果他最先下完。
这里写图片描述
这次可以看到分给每个线程的带宽为1/5 * 20M/s = 4M/s,但是后面三个线程都是王五同学的,这时王五同学的带宽其实为 12M/s ,没错,王五同学成功运用多线程知识解决了下载慢的问题。(神不知鬼不觉)

看到这里我们可以知道,影响用户带宽的因素:

①服务器的带宽

②线程数

3.需要解决的几个问题

  • 问题1:怎么在一个文件里面写数据的时候按照指定的位置写(因为每个线程的下载区间需要不一样,不然数据会覆盖,导致文件下不全)
  • 问题2:如何去获取要下载的文件大小(因为怕下载中途需要下载其他东西,导致本次需要下载的文件内存不足,所以需要先预留一个和要下载的文件大小一样大的空间)
  • 问题3:计算每个子线程的下载区间(因为每个线程的下载区间肯定不一样,不然怎么加快速度呢)
  • 问题4:如何实现断点续传?

第一个问题的解决办法:

借助RandomAccessFile 随机文件访问类的 seek(long offset)方法,这个方法可以把文件的写入位置移动至offset。

第二个问题的解决办法:

我们可以使用HttpURLConnection 对象的 getContentLength() 方法得到你当前请求文件的大小。

第三个问题的解决办法:
这里写图片描述
假设下载的文件大小为10B(0-9,数组下标从0开始),线程数为3,那么

线程0的下载区间应该是: 0—2

线程1的下载区间应该是: 3—5

线程2的下载区间应该是: 6—9

每个线程下载文件的大小 = 文件长度 / 线程数 (最后一个线程除外,因为可能不能均分)

那么i线程的下载开始位置: i*每个线程下载文件的大小
i线程的下载结束位置: (i+1)*每个线程下载文件的大小 - 1
最后一个线程的结束位置为:文件长度 - 1

第四个问题的解决办法:

因为有时我们在下载下到一半的时候突然停电了,等来电时我们应该接到上次下载的地方继续下载。如何实现呢??

我们可以把每个线程下载的进度都存在一个文件里,等来电时我们先去检索有没有进度文件,如果有,说明上次下载过,但没下完,就将次进度取出来继续下载。不过线程下载的开始位置应该是 原来的开始位置+上次的进度,为了用户体验,我们应该在线程全部下载完成之后将保存的下载进度文件删除

4.理清思路

①请求网络得到需要下载的文件的大小,并生成一个和原文件一样大小的文件(先占空间)(响应码为200)

②确定每个线程的下载区间(最后一个线程的结束位置应该单独考虑)

③先查看有没有进度文件,有则从上次进度开始下载,没有则请求网络获取需要下载区间的数据,并生成下载进度文件以便断点续传。(记住请求的数据不是所有数据,而是各个线程它需要下载的那部分区间,响应码为206)

注:不是所有的服务器都支持断点续传,这取决于服务器那边。

④待各个线程全部下载完成,将进度文件删掉。

⑤开启线程下载

5.简单封装核心代码(有修改,加入了线程池,具体看源码)

public class MultiThreadUtils implements Watcher {

    private static final int CODE_COUNT_PROGRESS = 0;

    //完成线程数
    private int finishedThread = 0;
    //总得进度
    private int totalProgress = 0;
    //当前下载进度
    private int currentProgress = 0;
    //接口回调
    private DownCount downCount;

    /**
     * @param path        下载路径
     * @param fileName    文件名字
     * @param threadcount 线程数
     * @param downCount   接口回调
     */
    public void startDownload(String path, String fileName, int threadcount, DownCount downCount) {
        this.downCount = downCount;
        new Multithreading(path, fileName, threadcount).start();
    }

    @Override
    public void updataNotify(int code, Object o) {
        switch (code) {
            case CODE_COUNT_PROGRESS:
                int currentCount = (int) o;
                downCount.count(currentCount);
                break;
        }
    }


    class Multithreading extends Thread {

        private String path;
        private String fileName;
        private int threadcount;

        public Multithreading(String path, String fileName, int threadcount) {
            this.path = path;
            this.fileName = fileName;
            this.threadcount = threadcount;
        }

        @Override
        public void run() {
            try {
                URL url = new URL(path);
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod("GET");
                connection.setConnectTimeout(3000);
                connection.setReadTimeout(8000);
                //请求成功是响应码等于200
                if (connection.getResponseCode() == 200) {
                    //拿到需要下载的文件的大小
                    int length = connection.getContentLength();
                    //先占一个位置,生成临时文件
                    File file = new File(Environment.getExternalStorageDirectory(), fileName);
                    RandomAccessFile raf = new RandomAccessFile(file, "rwd");
                    raf.close();

                    totalProgress = length;

                    //每个线程应该下载的长度。(最后一个线程除外,因为不一定能够平分)
                    int size = length / threadcount;
                    for (int i = 0; i < threadcount; i++) {
                        //1.确定每个线程的下载区间
                        //2.开启对应的子线程
                        int startIndex = i * size;//开始位置
                        int endIndex = (i + 1) * size - 1;//结束位置
                        //最后一个线程特殊处理它的结束位置
                        if (i == threadcount - 1) {
                            endIndex = length - 1;
                        }
                        Log.e("DownLoadThread", "文件总大小为" + totalProgress);
                        Log.e("DownLoadThread", "第" + (i + 1) + "个线程的下载区间为:"
                                + startIndex + "-" + endIndex);
                        //开启线程进行下载
                        DownLoadThread downLoadThread = new DownLoadThread(startIndex, endIndex, i, path, fileName, threadcount);
                        downLoadThread.start();
                        downLoadThread.interrupt();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    class DownLoadThread extends Thread {

        private int lastProgress;
        private int startIndex, endIndex, threadId;
        private String path;
        private String fileName;
        private int threadcount;

        public DownLoadThread(int startIndex, int endIndex, int threadId, String path, String fileName, int threadcount) {
            this.startIndex = startIndex;
            this.endIndex = endIndex;
            this.threadId = threadId;
            this.path = path;
            this.fileName = fileName;
            this.threadcount = threadcount;
            AchieveWatched.getInstance().add(MultiThreadUtils.this);
        }

        @Override
        public void run() {

            //简历进度临时文件,其实现在还没创建,当往里面写东西的时候才创建
            File progressFile = new File(Environment.getExternalStorageDirectory(), threadId + ".txt");
            //判断临时文件是够存在,没存在表示已经下载过,但还没有下载完事

            try {
                if (progressFile.exists()) {

                    FileInputStream inputStream = new FileInputStream(progressFile);
                    BufferedReader bf = new BufferedReader(new InputStreamReader(inputStream));
                    //从进度临时文件中读取上一次下载的总进度,然后与原本的开始位置相加。得到新的开始位置
                    lastProgress = Integer.valueOf(bf.readLine());
                    startIndex += lastProgress;

                    currentProgress += lastProgress;

                    bf.close();
                    inputStream.close();

                }

                //请求数据
                URL url = new URL(path);
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod("GET");
                connection.setReadTimeout(8000);
                connection.setConnectTimeout(3000);

                //设置本次http请求所请求的数据的区间(“这是需要服务器那边支持断点”),格式需要这样写,不能写错
                connection.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);

                if (connection.getResponseCode() == 206) {
                    //此时流中只有大概1/3原数据,因为开启了3个子线程分别下载各自的区间内容
                    InputStream inputStream = connection.getInputStream();
                    File file = new File(Environment.getExternalStorageDirectory(), fileName);
                    RandomAccessFile raf = new RandomAccessFile(file, "rwd");
                    //把文件的写入位置移动至startIndex
                    raf.seek(startIndex);

                    byte[] b = new byte[1024 * 8];
                    int len = 0;
                    int total = lastProgress;
                    while ((len = inputStream.read()) != -1) {
                        raf.write(b, 0, len);
                        total += len;

                        Log.e("DownLoadThread", "线程" + threadId + "下载了" + total);
                        //生成一个专门用来记录下载进度的临时文件
                        RandomAccessFile progressRaf = new RandomAccessFile(progressFile, "rwd");
                        //每次读取流里面的数据之后,同步把当前线程下载的总进度写入进度临时文件中
                        progressRaf.write((total + "").getBytes());
                        progressRaf.close();

                        currentProgress += len;
                        AchieveWatched.getInstance().notifyWatcher(CODE_COUNT_PROGRESS, (int) ((((float) currentProgress / totalProgress)) * 100));
                    }
                    raf.close();
                    //每完成一个线程就+1
                    finishedThread++;
                    //等标志位等于线程数的时候就说明线程全部完成了
                    if (finishedThread == threadcount) {
                        for (int i = 0; i < finishedThread; i++) {
                            //将生成的临时文件删除
                            File f = new File(Environment.getExternalStorageDirectory(), i + ".txt");
                            f.delete();
                            AchieveWatched.getInstance().clear();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public interface DownCount {
        void count(int count);
    }
}

6.使用方法

new MultiThreadUtils().startDownload(path, fileName, THREADCOUNT, new MultiThreadUtils.DownCount() {
                    @Override
                    public void count(int count) {
                        pb.setProgress(count);
                        Log.e("aaa", count + "%");
                    }
                });

7.源码下载地址

源码下载地址:https://github.com/zxp19920626/MultiThreadUtils
参考博客:http://blog.csdn.net/qq_22063697/article/details/51568066

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值