欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android多线程断点续传下载功能实现代码

程序员文章站 2023-12-11 11:41:46
原理 其实断点续传的原理很简单,从字面上理解,所谓断点续传就是从停止的地方重新下载。 断点:线程停止的位置。 续传:从停止的位置重新下载。 用代码解析就是...

原理

其实断点续传的原理很简单,从字面上理解,所谓断点续传就是从停止的地方重新下载。
断点:线程停止的位置。
续传:从停止的位置重新下载。

用代码解析就是:
断点:当前线程已经下载完成的数据长度。
续传:向服务器请求上次线程停止位置之后的数据。

原理知道了,功能实现起来也简单。每当线程停止时就把已下载的数据长度写入记录文件,当重新下载时,从记录文件读取已经下载了的长度。而这个长度就是所需要的断点。

续传的实现也简单,可以通过设置网络请求参数,请求服务器从指定的位置开始读取数据。
而要实现这两个功能只需要使用到httpurlconnection里面的setrequestproperty方法便可以实现.

public void setrequestproperty(string field, string newvalue)

如下所示,便是向服务器请求500-1000之间的500个byte:

conn.setrequestproperty("range", "bytes=" + 500 + "-" + 1000);

以上只是续传的一部分需求,当我们获取到下载数据时,还需要将数据写入文件,而普通发file对象并不提供从指定位置写入数据的功能,这个时候,就需要使用到randomaccessfile来实现从指定位置给文件写入数据的功能。

public void seek(long offset)

如下所示,便是从文件的的第100个byte后开始写入数据。

rafile.seek(100);

而开始写入数据时还需要用到randomaccessfile里面的另外一个方法

public void write(byte[] buffer, int byteoffset, int bytecount)

该方法的使用和outputstream的write的使用一模一样…

以上便是断点续传的原理。

多线程断点续传

而多线程断点续传便是在单线程的断点续传上延伸的,而多线程断点续传是把整个文件分割成几个部分,每个部分由一条线程执行下载,而每一条下载线程都要实现断点续传功能。
为了实现文件分割功能,我们需要使用到httpurlconnection的另外一个方法:

public int getcontentlength()

当请求成功时,可以通过该方法获取到文件的总长度。

每一条线程下载大小 = filelength / thread_num

如下图所示,描述的便是多线程的下载模型:

Android多线程断点续传下载功能实现代码

在多线程断点续传下载中,有一点需要特别注意:

由于文件是分成多个部分是被不同的线程的同时下载的,这就需要,每一条线程都分别需要有一个断点记录,和一个线程完成状态的记录;

如下图所示:

Android多线程断点续传下载功能实现代码

只有所有线程的下载状态都处于完成状态时,才能表示文件已经下载完成。
实现记录的方法多种多样,我这里采用的是jdk自带的properties类来记录下载参数。

断点续传结构

通过原理的了解,便可以很快的设计出断点续传工具类的基本结构图

Android多线程断点续传下载功能实现代码

idownloadlistener.java

  package com.arialyy.frame.http.inf;
  import java.net.httpurlconnection;

  /** 
   * 在这里面编写你的业务逻辑
   */
  public interface idownloadlistener {
    /**
     * 取消下载
     */
    public void oncancel();

    /**
     * 下载失败
     */
    public void onfail();

    /**
     * 下载预处理,可通过httpurlconnection获取文件长度
     */
    public void onpredownload(httpurlconnection connection);

    /**
     * 下载监听
     */
    public void onprogress(long currentlocation);

    /**
     * 单一线程的结束位置
     */
    public void onchildcomplete(long finishlocation);

    /**
     * 开始
     */
    public void onstart(long startlocation);

    /**
     * 子程恢复下载的位置
     */
    public void onchildresume(long resumelocation);

    /**
     * 恢复位置
     */
    public void onresume(long resumelocation);

    /**
     * 停止
     */
    public void onstop(long stoplocation);

    /**
     * 下载完成
     */
    public void oncomplete();
  }

该类是下载监听接口

downloadlistener.java

import java.net.httpurlconnection;

/**
 * 下载监听
 */
public class downloadlistener implements idownloadlistener {

  @override
  public void onresume(long resumelocation) {

  }

  @override
  public void oncancel() {

  }

  @override
  public void onfail() {

  }

  @override
  public void onpredownload(httpurlconnection connection) {

  }

  @override
  public void onprogress(long currentlocation) {

  }

  @override
  public void onchildcomplete(long finishlocation) {

  }

  @override
  public void onstart(long startlocation) {

  }

  @override
  public void onchildresume(long resumelocation) {

  }

  @override
  public void onstop(long stoplocation) {

  }

  @override
  public void oncomplete() {

  }
}

下载参数实体

 /**
   * 子线程下载信息类
   */
  private class downloadentity {
    //文件总长度
    long filesize;
    //下载链接
    string downloadurl;
    //线程id
    int threadid;
    //起始下载位置
    long startlocation;
    //结束下载的文章
    long endlocation;
    //下载文件
    file tempfile;
    context context;

    public downloadentity(context context, long filesize, string downloadurl, file file, int threadid, long startlocation, long endlocation) {
      this.filesize = filesize;
      this.downloadurl = downloadurl;
      this.tempfile = file;
      this.threadid = threadid;
      this.startlocation = startlocation;
      this.endlocation = endlocation;
      this.context = context;
    }
  }

该类是下载信息配置类,每一条子线程的下载都需要一个下载实体来配置下载信息。

下载任务线程

  /**
   * 多线程下载任务类
   */
  private class downloadtask implements runnable {
    private static final string tag = "downloadtask";
    private downloadentity dentity;
    private string configfpath;

    public downloadtask(downloadentity downloadinfo) {
      this.dentity = downloadinfo;
      configfpath = dentity.context.getfilesdir().getpath() + "/temp/" + dentity.tempfile.getname() + ".properties";
    }

    @override
    public void run() {
      try {
        l.d(tag, "线程_" + dentity.threadid + "_正在下载【" + "开始位置 : " + dentity.startlocation + ",结束位置:" + dentity.endlocation + "】");
        url url = new url(dentity.downloadurl);
        httpurlconnection conn = (httpurlconnection) url.openconnection();
        //在头里面请求下载开始位置和结束位置
        conn.setrequestproperty("range", "bytes=" + dentity.startlocation + "-" + dentity.endlocation);
        conn.setrequestmethod("get");
        conn.setrequestproperty("charset", "utf-8");
        conn.setconnecttimeout(time_out);
        conn.setrequestproperty("user-agent", "mozilla/4.0 (compatible; msie 8.0; windows nt 5.2; trident/4.0; .net clr 1.1.4322; .net clr 2.0.50727; .net clr 3.0.04506.30; .net clr 3.0.4506.2152; .net clr 3.5.30729)");
        conn.setrequestproperty("accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*");
        conn.setreadtimeout(2000); //设置读取流的等待时间,必须设置该参数
        inputstream is = conn.getinputstream();
        //创建可设置位置的文件
        randomaccessfile file = new randomaccessfile(dentity.tempfile, "rwd");
        //设置每条线程写入文件的位置
        file.seek(dentity.startlocation);
        byte[] buffer = new byte[1024];
        int len;
        //当前子线程的下载位置
        long currentlocation = dentity.startlocation;
        while ((len = is.read(buffer)) != -1) {
          if (iscancel) {
            l.d(tag, "++++++++++ thread_" + dentity.threadid + "_cancel ++++++++++");
            break;
          }

          if (isstop) {
            break;
          }

          //把下载数据数据写入文件
          file.write(buffer, 0, len);
          synchronized (downloadutil.this) {
            mcurrentlocation += len;
            mlistener.onprogress(mcurrentlocation);
          }
          currentlocation += len;
        }
        file.close();
        is.close();

        if (iscancel) {
          synchronized (downloadutil.this) {
            mcancelnum++;
            if (mcancelnum == thread_num) {
              file configfile = new file(configfpath);
              if (configfile.exists()) {
                configfile.delete();
              }

              if (dentity.tempfile.exists()) {
                dentity.tempfile.delete();
              }
              l.d(tag, "++++++++++++++++ oncancel +++++++++++++++++");
              isdownloading = false;
              mlistener.oncancel();
              system.gc();
            }
          }
          return;
        }

        //停止状态不需要删除记录文件
        if (isstop) {
          synchronized (downloadutil.this) {
            mstopnum++;
            string location = string.valueof(currentlocation);
            l.i(tag, "thread_" + dentity.threadid + "_stop, stop location ==> " + currentlocation);
            writeconfig(dentity.tempfile.getname() + "_record_" + dentity.threadid, location);
            if (mstopnum == thread_num) {
              l.d(tag, "++++++++++++++++ onstop +++++++++++++++++");
              isdownloading = false;
              mlistener.onstop(mcurrentlocation);
              system.gc();
            }
          }
          return;
        }

        l.i(tag, "线程【" + dentity.threadid + "】下载完毕");
        writeconfig(dentity.tempfile.getname() + "_state_" + dentity.threadid, 1 + "");
        mlistener.onchildcomplete(dentity.endlocation);
        mcompletethreadnum++;
        if (mcompletethreadnum == thread_num) {
          file configfile = new file(configfpath);
          if (configfile.exists()) {
            configfile.delete();
          }
          mlistener.oncomplete();
          isdownloading = false;
          system.gc();
        }
      } catch (malformedurlexception e) {
        e.printstacktrace();
        isdownloading = false;
        mlistener.onfail();
      } catch (ioexception e) {
        fl.e(this, "下载失败【" + dentity.downloadurl + "】" + fl.getprintexception(e));
        isdownloading = false;
        mlistener.onfail();
      } catch (exception e) {
        fl.e(this, "获取流失败" + fl.getprintexception(e));
        isdownloading = false;
        mlistener.onfail();
      }
    }

这个是每条下载子线程的下载任务类,子线程通过下载实体对每一条线程进行下载配置,由于在多断点续传的概念里,停止表示的是暂停状态,而恢复表示的是线程从记录的断点重新进行下载,所以,线程处于停止状态时是不能删除记录文件的。

下载入口

  /**
   * 多线程断点续传下载文件,暂停和继续
   *
   * @param context     必须添加该参数,不能使用全局变量的context
   * @param downloadurl   下载路径
   * @param filepath     保存路径
   * @param downloadlistener 下载进度监听 {@link downloadlistener}
   */
  public void download(final context context, @nonnull final string downloadurl, @nonnull final string filepath,
             @nonnull final downloadlistener downloadlistener) {
    isdownloading = true;
    mcurrentlocation = 0;
    isstop = false;
    iscancel = false;
    mcancelnum = 0;
    mstopnum = 0;
    final file dfile = new file(filepath);
    //读取已完成的线程数
    final file configfile = new file(context.getfilesdir().getpath() + "/temp/" + dfile.getname() + ".properties");
    try {
      if (!configfile.exists()) { //记录文件被删除,则重新下载
        newtask = true;
        fileutil.createfile(configfile.getpath());
      } else {
        newtask = false;
      }
    } catch (exception e) {
      e.printstacktrace();
      mlistener.onfail();
      return;
    }
    newtask = !dfile.exists();
    new thread(new runnable() {
      @override
      public void run() {
        try {
          mlistener = downloadlistener;
          url url = new url(downloadurl);
          httpurlconnection conn = (httpurlconnection) url.openconnection();
          conn.setrequestmethod("get");
          conn.setrequestproperty("charset", "utf-8");
          conn.setconnecttimeout(time_out);
          conn.setrequestproperty("user-agent", "mozilla/4.0 (compatible; msie 8.0; windows nt 5.2; trident/4.0; .net clr 1.1.4322; .net clr 2.0.50727; .net clr 3.0.04506.30; .net clr 3.0.4506.2152; .net clr 3.5.30729)");
          conn.setrequestproperty("accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*");
          conn.connect();
          int len = conn.getcontentlength();
          if (len < 0) { //网络被劫持时会出现这个问题
            mlistener.onfail();
            return;
          }
          int code = conn.getresponsecode();
          if (code == 200) {
            int filelength = conn.getcontentlength();
            //必须建一个文件
            fileutil.createfile(filepath);
            randomaccessfile file = new randomaccessfile(filepath, "rwd");
            //设置文件长度
            file.setlength(filelength);
            mlistener.onpredownload(conn);
            //分配每条线程的下载区间
            properties pro = null;
            pro = util.loadconfig(configfile);
            int blocksize = filelength / thread_num;
            sparsearray<thread> tasks = new sparsearray<>();
            for (int i = 0; i < thread_num; i++) {
              long startl = i * blocksize, endl = (i + 1) * blocksize;
              object state = pro.getproperty(dfile.getname() + "_state_" + i);
              if (state != null && integer.parseint(state + "") == 1) { //该线程已经完成
                mcurrentlocation += endl - startl;
                l.d(tag, "++++++++++ 线程_" + i + "_已经下载完成 ++++++++++");
                mcompletethreadnum++;
                if (mcompletethreadnum == thread_num) {
                  if (configfile.exists()) {
                    configfile.delete();
                  }
                  mlistener.oncomplete();
                  isdownloading = false;
                  system.gc();
                  return;
                }
                continue;
              }
              //分配下载位置
              object record = pro.getproperty(dfile.getname() + "_record_" + i);
              if (!newtask && record != null && long.parselong(record + "") > 0) {    //如果有记录,则恢复下载
                long r = long.parselong(record + "");
                mcurrentlocation += r - startl;
                l.d(tag, "++++++++++ 线程_" + i + "_恢复下载 ++++++++++");
                mlistener.onchildresume(r);
                startl = r;
              }
              if (i == (thread_num - 1)) {
                endl = filelength;//如果整个文件的大小不为线程个数的整数倍,则最后一个线程的结束位置即为文件的总长度
              }
              downloadentity entity = new downloadentity(context, filelength, downloadurl, dfile, i, startl, endl);
              downloadtask task = new downloadtask(entity);
              tasks.put(i, new thread(task));
            }
            if (mcurrentlocation > 0) {
              mlistener.onresume(mcurrentlocation);
            } else {
              mlistener.onstart(mcurrentlocation);
            }
            for (int i = 0, count = tasks.size(); i < count; i++) {
              thread task = tasks.get(i);
              if (task != null) {
                task.start();
              }
            }
          } else {
            fl.e(tag, "下载失败,返回码:" + code);
            isdownloading = false;
            system.gc();
            mlistener.onfail();
          }
        } catch (ioexception e) {
          fl.e(this, "下载失败【downloadurl:" + downloadurl + "】\n【filepath:" + filepath + "】" + fl.getprintexception(e));
          isdownloading = false;
          mlistener.onfail();
        }
      }
    }).start();
  }

其实也没啥好说的,注释已经很完整了,需要注意两点
1、恢复下载时:已下载的文件大小 = 该线程的上一次断点的位置 - 该线程起始下载位置;
2、为了保证下载文件的完整性,只要记录文件不存在就需要重新进行下载;

最终效果:

Android多线程断点续传下载功能实现代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。