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

Android实现下载工具的简单代码

程序员文章站 2023-11-30 17:54:46
下载应该是每个app都必须的一项功能,不采用第三方框架的话,就需要我们自己去实现下载工具了。如果我们自己实现可以怎么做呢? 首先如果服务器文件支持断点续传,则我们需要实现...

下载应该是每个app都必须的一项功能,不采用第三方框架的话,就需要我们自己去实现下载工具了。如果我们自己实现可以怎么做呢?

首先如果服务器文件支持断点续传,则我们需要实现的主要功能点如下:

多线程、断点续传下载
下载管理:开始、暂停、继续、取消、重新开始

如果服务器文件不支持断点续传,则只能进行普通的单线程下载,而且不能暂停、继续。当然一般情况服务器文件都应该支持断点续传吧!

下边分别是单个任务下载、多任务列表下载、以及service下载的效果图:

Android实现下载工具的简单代码

single_task

Android实现下载工具的简单代码

task_manage

Android实现下载工具的简单代码

service_task

基本实现原理:

接下来看看具体的实现原理,由于我们的下载是基于okhttp实现的,首先我们需要一个okhttpmanager类,进行最基本的网络请求封装:

public class okhttpmanager {
  ............省略..............
  /**
   * 异步(根据断点请求)
   *
   * @param url
   * @param start
   * @param end
   * @param callback
   * @return
   */
  public call initrequest(string url, long start, long end, final callback callback) {
    request request = new request.builder()
        .url(url)
        .header("range", "bytes=" + start + "-" + end)
        .build();

    call call = builder.build().newcall(request);
    call.enqueue(callback);

    return call;
  }

  /**
   * 同步请求
   *
   * @param url
   * @return
   * @throws ioexception
   */
  public response initrequest(string url) throws ioexception {
    request request = new request.builder()
        .url(url)
        .header("range", "bytes=0-")
        .build();

    return builder.build().newcall(request).execute();
  }

  /**
   * 文件存在的情况下可判断服务端文件是否已经更改
   *
   * @param url
   * @param lastmodify
   * @return
   * @throws ioexception
   */
  public response initrequest(string url, string lastmodify) throws ioexception {
    request request = new request.builder()
        .url(url)
        .header("range", "bytes=0-")
        .header("if-range", lastmodify)
        .build();

    return builder.build().newcall(request).execute();
  }

  /**
   * https请求时初始化证书
   *
   * @param certificates
   * @return
   */
  public void setcertificates(inputstream... certificates) {
    try {
      certificatefactory certificatefactory = certificatefactory.getinstance("x.509");
      keystore keystore = keystore.getinstance(keystore.getdefaulttype());
      keystore.load(null);
      int index = 0;
      for (inputstream certificate : certificates) {
        string certificatealias = integer.tostring(index++);
        keystore.setcertificateentry(certificatealias, certificatefactory.generatecertificate(certificate));
        try {
          if (certificate != null)
            certificate.close();
        } catch (ioexception e) {
        }
      }

      sslcontext sslcontext = sslcontext.getinstance("tls");
      trustmanagerfactory trustmanagerfactory = trustmanagerfactory.getinstance(trustmanagerfactory.getdefaultalgorithm());

      trustmanagerfactory.init(keystore);
      sslcontext.init(null, trustmanagerfactory.gettrustmanagers(), new securerandom());

      builder.sslsocketfactory(sslcontext.getsocketfactory());

    } catch (exception e) {
      e.printstacktrace();
    }
  }
}

这个类里包含了基本的超时配置、根据断点信息发起异步请求、校验服务器文件是否有更新、https证书配置等。这样网络请求部分就有了。

接下来,我们还需要数据库的支持,以便记录下载文件的基本信息,这里我们使用sqlite,只有一张表:

/**
   * download_info表建表语句
   */
  public static final string create_download_info = "create table download_info ("
      + "id integer primary key autoincrement, "
      + "url text, "
      + "path text, "
      + "name text, "
      + "child_task_count integer, "
      + "current_length integer, "
      + "total_length integer, "
      + "percentage real, "
      + "last_modify text, "
      + "date text)";

当然还有对应表的增删改查工具类,具体的可参考源码。

由于需要下载管理,所以线程池也是必不可少的,这样可以避免过多的创建子线程,达到复用的目的,当然线程池的大小可以根据需求进行配置,主要代码如下:

public class threadpool {
  //可同时下载的任务数(核心线程数)
  private int core_pool_size = 3;
  //缓存队列的大小(最大线程数)
  private int max_pool_size = 20;
  //非核心线程闲置的超时时间(秒),如果超时则会被回收
  private long keep_alive = 10l;

  private threadpoolexecutor thread_pool_executor;

  private threadfactory sthreadfactory = new threadfactory() {
    private final atomicinteger mcount = new atomicinteger();

    @override
    public thread newthread(@nonnull runnable runnable) {
      return new thread(runnable, "download_task#" + mcount.getandincrement());
    }
  };

  ...................省略................

  public void setcorepoolsize(int corepoolsize) {
    if (corepoolsize == 0) {
      return;
    }
    core_pool_size = corepoolsize;
  }

  public void setmaxpoolsize(int maxpoolsize) {
    if (maxpoolsize == 0) {
      return;
    }
    max_pool_size = maxpoolsize;
  }

  public int getcorepoolsize() {
    return core_pool_size;
  }

  public int getmaxpoolsize() {
    return max_pool_size;
  }

  public threadpoolexecutor getthreadpoolexecutor() {
    if (thread_pool_executor == null) {
      thread_pool_executor = new threadpoolexecutor(
          core_pool_size, max_pool_size,
          keep_alive, timeunit.seconds,
          new linkedblockingdeque<runnable>(),
          sthreadfactory);
    }
    return thread_pool_executor;
  }
}

接下来就是我们核心的下载类filetask了,它实现了runnable接口,这样就能在线程池中执行,首先看下run()方法的逻辑:

@override
  public void run() {
    try {
      file savefile = new file(path, name);
      file tempfile = new file(path, name + ".temp");
      downloaddata data = db.getinstance(context).getdata(url);
      if (utils.isfileexists(savefile) && utils.isfileexists(tempfile) && data != null) {
        response response = okhttpmanager.getinstance().initrequest(url, data.getlastmodify());
        if (response != null && response.issuccessful() && utils.isnotserverfilechanged(response)) {
          temp_file_total_size = each_temp_size * data.getchildtaskcount();
          onstart(data.gettotallength(), data.getcurrentlength(), "", true);
        } else {
          preparerangefile(response);
        }
        saverangefile();
      } else {
        response response = okhttpmanager.getinstance().initrequest(url);
        if (response != null && response.issuccessful()) {
          if (utils.issupportrange(response)) {
            preparerangefile(response);
            saverangefile();
          } else {
            savecommonfile(response);
          }
        }
      }
    } catch (ioexception e) {
      onerror(e.tostring());
    }
  }

如果下载的目标文件、记录断点的临时文件、数据库记录都存在,则我们先判断服务器文件是否有更新,如果没有更新则根据之前的记录直接开始下载,否则需要先进行断点下载前的准备。如果记录文件不全部存在则需要先判断是否支持断点续传,如果支持则按照断点续传的流程进行,否则采用普通下载。

首先看下preparerangefile()方法,在这里进行断点续传的准备工作:

private void preparerangefile(response response) {
   .................省略.................
    try {
      file savefile = utils.createfile(path, name);
      file tempfile = utils.createfile(path, name + ".temp");

      long filelength = response.body().contentlength();
      onstart(filelength, 0, utils.getlastmodify(response), true);

      db.getinstance(context).deletedata(url);
      utils.deletefile(savefile, tempfile);

      saverandomaccessfile = new randomaccessfile(savefile, "rws");
      saverandomaccessfile.setlength(filelength);

      temprandomaccessfile = new randomaccessfile(tempfile, "rws");
      temprandomaccessfile.setlength(temp_file_total_size);
      tempchannel = temprandomaccessfile.getchannel();
      mappedbytebuffer buffer = tempchannel.map(read_write, 0, temp_file_total_size);

      long start;
      long end;
      int eachsize = (int) (filelength / childtaskcount);
      for (int i = 0; i < childtaskcount; i++) {
        if (i == childtaskcount - 1) {
          start = i * eachsize;
          end = filelength - 1;
        } else {
          start = i * eachsize;
          end = (i + 1) * eachsize - 1;
        }
        buffer.putlong(start);
        buffer.putlong(end);
      }
    } catch (exception e) {
      onerror(e.tostring());
    } finally {
      .............省略............
    }
  }

首先是清除历史记录,创建新的目标文件和临时文件,childtaskcount代表文件需要通过几个子任务去下载,这样就可以得到每个子任务需要下载的任务大小,进而得到具体的断点信息并记录到临时文件中。文件下载我们采用mappedbytebuffer 类,相比randomaccessfile 更加的高效。同时执行onstart()方法将代表下载的准备阶段,具体细节后面会说到。

接下来看saverangefile()方法:

private void saverangefile() {

     .................省略..............

    for (int i = 0; i < childtaskcount; i++) {
      final int tempi = i;
      call call = okhttpmanager.getinstance().initrequest(url, range.start[i], range.end[i], new callback() {
        @override
        public void onfailure(call call, ioexception e) {
          onerror(e.tostring());
        }

        @override
        public void onresponse(call call, response response) throws ioexception {
          startsaverangefile(response, tempi, range, savefile, tempfile);
        }
      });
      calllist.add(call);
    }
    .................省略..............
  }

就是根据临时文件保存的断点信息发起childtaskcount数量的异步请求,如果响应成功则通过startsaverangefile()方法分段保存文件:

private void startsaverangefile(response response, int index, ranges range, file savefile, file tempfile) {
   .................省略..............
    try {
      saverandomaccessfile = new randomaccessfile(savefile, "rws");
      savechannel = saverandomaccessfile.getchannel();
      mappedbytebuffer savebuffer = savechannel.map(read_write, range.start[index], range.end[index] - range.start[index] + 1);

      temprandomaccessfile = new randomaccessfile(tempfile, "rws");
      tempchannel = temprandomaccessfile.getchannel();
      mappedbytebuffer tempbuffer = tempchannel.map(read_write, 0, temp_file_total_size);

      inputstream = response.body().bytestream();
      int len;
      byte[] buffer = new byte[buffer_size];

      while ((len = inputstream.read(buffer)) != -1) {
        //取消
        if (is_cancel) {
          handler.sendemptymessage(cancel);
          calllist.get(index).cancel();
          break;
        }

        savebuffer.put(buffer, 0, len);
        tempbuffer.putlong(index * each_temp_size, tempbuffer.getlong(index * each_temp_size) + len);
        onprogress(len);

        //退出保存记录
        if (is_destroy) {
          handler.sendemptymessage(destroy);
          calllist.get(index).cancel();
          break;
        }
        //暂停
        if (is_pause) {
          handler.sendemptymessage(pause);
          calllist.get(index).cancel();
          break;
        }
      }
      addcount();
    } catch (exception e) {
      onerror(e.tostring());
    } finally {
      .................省略..............
    }

在while循环中进行目前文件的写入和将当前下载到的位置保存到临时文件:

 savebuffer.put(buffer, 0, len);
 tempbuffer.putlong(index * each_temp_size, tempbuffer.getlong(index * each_temp_size) + len);

同时调用onprogress()方法将进度发送出去,其中取消、退出保存记录、暂停需要中断while循环。

因为下载是在子线程进行的,但我们一般需要在ui线程根据下载状态来更新ui,所以我们通过handler将下载过程的状态数据发送到ui线程:即调用handler.sendemptymessage()方法。

最后filetask类还有一个savecommonfile()方法,即进行不支持断点续传的普通下载。

前边我们提到了通过handler将下载过程的状态数据发送到ui线程,接下看下progresshandler类基本的处理:

private handler mhandler = new handler() {
    @override
    public void handlemessage(message msg) {
      super.handlemessage(msg);
      switch (mcurrentstate) {
        case start:
          break;
        case progress:
          break;
        case cancel:
          break;
        case pause:
          break;
        case finish:
          break;
        case destroy:
          break;
        case error:
          break;
      }
    }
  };

在handlemessage()方法中,我们根据当前的下载状态进行相应的操作。
如果是start则需要将下载数据插入数据库,执行初始化回调等;如果是progress则执行下载进度回调;如果是cancel则删除目标文件、临时文件、数据库记录并执行对应回调等;如果是pause则更新数据库文件记录并执行暂停的回调等;如果是finish则删除临时文件和数据库记录并执行完成的回调;如果是destroy则代表直接在activity中下载,退出activity则会更新数据库记录;最后的error则对应出错的情况。具体的细节可参考源码。

最后在downloadmanger类里使用线程池执行下载操作:

threadpool.getinstance().getthreadpoolexecutor().execute(filetask);

 //如果正在下载的任务数量等于线程池的核心线程数,则新添加的任务处于等待状态
    if (threadpool.getinstance().getthreadpoolexecutor().getactivecount() == threadpool.getinstance().getcorepoolsize()) {
      downloadcallback.onwait();
    }

以及判断新添加的任务是否处于等待的状态,方便在ui层处理。到这里核心的实现原理就完了,更多的细节可以参考源码。

如何使用:

downloadmanger是个单例类,在这里封装在了具体的使用操作,我们可以根据url进行下载的开始、暂停、继续、取消、重新开始、线程池配置、https证书配置、查询数据的记录数据、获得当前某个下载状态的数据:

开始一个下载任务我们可以通过三种方式来进行:
1、通过downloadmanager类的start(downloaddata downloaddata, downloadcallback downloadcallback)方法,data可以设置url、保存路径、文件名、子任务数量:
2、先执行downloadmanager类的setondownloadcallback(downloaddata downloaddata, downloadcallback downloadcallback)方法,绑定data和callback,再执行start(string url)方法。

3、链式调用,需要通过dutil类来进行:例如

dutil.init(mcontext)
        .url(url)
        .path(environment.getexternalstoragedirectory() + "/dutil/")
        .name(name.xxx)
        .childtaskcount(3)
        .build()
        .start(callback);

start()方法会返回downloadmanager类的实例,如果你不关心返回值,使用downloadmanger.getinstance(context)同样可以得到downloadmanager类的实例,以便进行后续的暂停、继续、取消等操作。

关于callback可以使用downloadcallback接口实现完整的回调:

new downloadcallback() {
          //开始
          @override
          public void onstart(long currentsize, long totalsize, float progress) {
          }
          //下载中
          @override
          public void onprogress(long currentsize, long totalsize, float progress) { 
          }
          //暂停
          @override
          public void onpause() {
          }
          //取消
          @override
          public void oncancel() {
          }
          //下载完成
          @override
          public void onfinish(file file) { 
          }
          //等待
          @override
          public void onwait() {
          }
          //下载出错
          @override
          public void onerror(string error) {
          }
        }

也可以使用simpledownloadcallback接口只实现需要的回调方法。

暂停下载中的任务:pause(string url)

继续暂停的任务:resume(string url)
     ps:不支持断点续传的文件无法进行暂停和继续操作。

取消任务:cancel(string url),可以取消下载中、或暂停的任务。

重新开始下载:restart(string url),暂停、下载中、已取消、已完成的任务均可重新开始下载。
下载数据保存:destroy(string url)、destroy(string... urls),如在activity中直接下载,直接退出时可在ondestroy()方法中调用,以保存数据。
配置线程池:settaskpoolsize(int corepoolsize, int maxpoolsize),设置核心线程数以及总线程数。
配置okhttp证书:setcertificates(inputstream... certificates)
在数据库查询单个数据downloaddata getdbdata(string url),查询全部数据:list<downloaddata> getalldbdata()
ps:数据库不保存已下载完成的数据
获得下载队列中的某个文件数据:downloaddata getcurrentdata(string url)
到这里基本的就介绍完了,更多的细节和具体的使用都在demo中,不合理的地方还请多多指教哦。

github地址:https://github.com/othershe/dutil

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