Android多线程断点续传下载功能实现代码
原理
其实断点续传的原理很简单,从字面上理解,所谓断点续传就是从停止的地方重新下载。
断点:线程停止的位置。
续传:从停止的位置重新下载。
用代码解析就是:
断点:当前线程已经下载完成的数据长度。
续传:向服务器请求上次线程停止位置之后的数据。
原理知道了,功能实现起来也简单。每当线程停止时就把已下载的数据长度写入记录文件,当重新下载时,从记录文件读取已经下载了的长度。而这个长度就是所需要的断点。
续传的实现也简单,可以通过设置网络请求参数,请求服务器从指定的位置开始读取数据。
而要实现这两个功能只需要使用到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
如下图所示,描述的便是多线程的下载模型:
在多线程断点续传下载中,有一点需要特别注意:
由于文件是分成多个部分是被不同的线程的同时下载的,这就需要,每一条线程都分别需要有一个断点记录,和一个线程完成状态的记录;
如下图所示:
只有所有线程的下载状态都处于完成状态时,才能表示文件已经下载完成。
实现记录的方法多种多样,我这里采用的是jdk自带的properties类来记录下载参数。
断点续传结构
通过原理的了解,便可以很快的设计出断点续传工具类的基本结构图
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、为了保证下载文件的完整性,只要记录文件不存在就需要重新进行下载;
最终效果:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: JavaScript自定义文本框光标