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

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

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

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

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

single_task

task_manage

service_task

PS:如果demo不能正常下载,请替换对应下载地址

基本实现原理:

接下来看看具体的实现原理,由于我们的下载是基于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();@Overridepublic 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()方法的逻辑:

@Overridepublic 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() {@Overridepublic void onFailure(Call call, IOException e) {onError(e.toString());}@Overridepublic 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() {@Overridepublic 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() {//开始@Overridepublic void onStart(long currentSize, long totalSize, float progress) {}//下载中@Overridepublic void onProgress(long currentSize, long totalSize, float progress) { }//暂停@Overridepublic void onPause() {}//取消@Overridepublic void onCancel() {}//下载完成@Overridepublic void onFinish(File file) { }//等待@Overridepublic void onWait() {}//下载出错@Overridepublic 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)

2017.4.7补充:如果下载过程中 进行清理后台等操作导致App进程被杀死,则下次会重新下载对应文件。

到这里基本的就介绍完了,更多的细节和具体的使用都在demo中,不合理的地方还请多多指教哦。

如果想了解文件上传可以看这里:Android 实现一个简单的文件上传工具

github地址:https://github.com/SheHuan/DUtil

Android 实现一个简单的文件下载工具相关推荐

  1. android实现计算器功能吗,利用Android实现一个简单的计算器功能

    利用Android实现一个简单的计算器功能 发布时间:2020-11-20 16:25:01 来源:亿速云 阅读:90 作者:Leah 今天就跟大家聊聊有关利用Android实现一个简单的计算器功能, ...

  2. 开始做一个简单的记账工具

    用VScode做一个简单的记账工具 内容保存在 https://leonchan0608.coding.net/public/bookkeeping/bookkeeping/git 下班学习,奋斗一年 ...

  3. 初识Android 制作一个简单的记账本

    初识Android 制作一个简单的记账本 主要功能 实现一个记账本页面 可以添加数据并更新到页面中 主要步骤 运行截图 主页面 点击红色按钮弹出添加页面 完成后自动更新到目录下 主要功能 实现一个记账 ...

  4. 大学慕课MOOC设计一个简单的计算工具

    题目: ‎编程题: ‎设计一个简单的四则运算工具,有一个标题为"计算"的窗口,窗口布局为FlowLayout.设计四个按钮,分别命名为"加"."减&q ...

  5. wsm-lucene 一个简单的Lucene工具类

    代码地址: https://gitee.com/shaojiepeng/wsm-lucene wsm-lucene 一个简单的Lucene工具类,通过注释的方式来配置构建索引的字段.提供新建索引.查找 ...

  6. python实现一个简单的ping工具

    继上一篇计算checksum校验和,本章通过socket套接字,struct字节打包成二进制,select返回套接字的文件描述符的结合,实现一个简单的ping工具. #!/usr/bin/python ...

  7. Android Compose——一个简单的Bilibili APP

    Bilibili移动端APP 简介 依赖 效果 登录 效果 WebView 自定义TobRow的Indicator大小 首页 推荐 LazyGridView使用Paging3 热门 排行榜 搜索 模糊 ...

  8. 使用Python 封装一个简单的Mysql工具类

    pymysql操作mysql,虽然简单,但每次都要链接数据库,获取游标,关闭游标,关闭链接.这些操作无技术含量,还要重复编写!!想一想不如封装一个DBUtil,来提高开发效率. 要编写工具类首先要把公 ...

  9. 用VS2010写一个简单的记账工具(一)基础设置

    工作中,需要购买一些东西,为了便于记账,自己写了一个简单的程序,仅供参考吧. 首先,因为没有内网环境,所以我使用的数据库是Excel,你可以用sql.mdb等.性质是一样的. 主要是说方法,所以界面就 ...

最新文章

  1. request.getparameter特殊字符变成?号问号_15 个不可不知的 Linux 特殊字符,你懂几个?...
  2. 程序员自购老板椅被HR搬去老板办公室:不能享受这么好的椅子
  3. Windows Pe 第三章 PE头文件(中)
  4. java语言怎样判断文件夹_JAVA语言之如何判断文件,判断文件夹是否存在的代码...
  5. Cerebro 插件之电影磁力搜索神器
  6. paip.调试js 查看元素事件以及事件断点
  7. golang笔记14--go 语言爬虫实战项目介绍
  8. 2021/12/5 XSS跨站原理及攻击手法
  9. 百度文库Android、Iphone客户端——下载、阅读无需财富值积分
  10. 批量重命名文件、图片、去除括号
  11. 百家讲坛 黄帝内经(第一部)
  12. 掌阅 兼容性测试 结果
  13. 捷普服务器群组防护系统,捷普入侵防御系统
  14. 通过powershell查询OU中被禁用的AD账号,并删除他们的所属组
  15. DSA-数据签名算法(理论)
  16. 一群人围成一圈从123报数,如果报到3就退出该圈中,直到最后一个人留下来!
  17. Redis持久化(少年一贯快马扬帆,道阻且长不转弯)
  18. iptables端口映射
  19. 520,看看AI大牛吴恩达和姚期智如何撩妹
  20. python编程的就业方向_2021年python编程就业方向是怎样的?

热门文章

  1. 前端pc端和移动端的区别?
  2. java 汉字转拼音工具_【转载】Java汉字转全拼工具
  3. 用函数编程计算反馈意见的平均数(Mean)、中位数(Median)和众数(Mode)
  4. 十年重生,LinuxQQ回归体验
  5. 80c51的c语言程序设计,80C51的C语言程序设计概述.ppt
  6. mdbook通过markdown制作电子书(代替gitbook)
  7. 【数据架构系列-04】趁着国家数据局来了,一起聊聊政府数据开放,未来道阻且长
  8. 三参数 S-N 曲线拟合及MATLAB程序
  9. 意派epub360 html 代码,意派Epub360丨不用代码,不用定制,你也可以自己制作DIY类H5啦!...
  10. 使用线性回归实现波士顿房价预测