断点多线程下载的几个关键点:①:得到要下载的文件大小后,均分给几个线程。②:使用RandomAccessFile类进行读写,可以指定开始写入的位置。③:数据库保存下载信息,下一次继续下载的时候从数据库取出数据,然后从上次下载结束的地方开始。
这里我使用了FinalDb的数据库框架,同时在内存中存储了一份所有线程的下载信息,负责时时更新和查询下载进度。我测试用的是百度云网盘,文件大小50M左右。注意,线程中指定了开始结束下载位置的网络请求成功的返回码是206,并不是200。
效果图:
线程类:线程类负责具体的下载,线程的下载信息被存储到了数据库。线程开始下载时,根据线程ID查询自己的存储信息,然后开始从指定的位置下载和写入文件。完毕后根据自己的当前下载结果设置自己当前的下载状态。时时的下载进度存储只存储到了内存,只在本次下载结束才存储到数据库。
public class DownloadThread extends Thread {
/**
* 数据库操作工具
*/
private FinalDb finalDb;
/**
* 下载状态:未开始
*/
public static final int STATE_READY = 1;
/**
* 下载状态:下载中
*/
public static final int STATE_LOADING = 2;
/**
* 下载状态:下载暂停中
*/
public static final int STATE_PAUSING = 3;
/**
* 下载状态:下载完成
*/
public static final int STATE_FINISH = 4;
/**
* 下载状态
*/
public int downloadState;
/**
* 线程ID
*/
private int threadID;
/**
* 要下载的URL路径
*/
private String url;
/**
* 本线程要下载的文件
*/
public RandomAccessFile file;
/**
* 构造器
*/
public DownloadThread(Context context, int threadID, String downloadUrl, RandomAccessFile randomAccessFile) {
this.threadID = threadID;
this.url = downloadUrl;
this.file = randomAccessFile;
finalDb = DBUtil.getFinalDb(context);
}
@Override
public void run() {
//数据库查询本线程下载进度
List list = finalDb.findAllByWhere(ThreadDownloadInfoBean.class, "threadID='" + threadID + "'");
//下载信息存放到内存
if (list.get(0) != null) {
MapUtil.map.put(threadID, list.get(0));
}
//取出实体类
ThreadDownloadInfoBean bean = MapUtil.map.get(threadID);
Utils.Print("bean:" + bean.toString());
InputStream is;
HttpURLConnection conn;
try {
Utils.Print("线程" + threadID + "开始连接");
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestMethod("GET");
//设置下载开始和结束的位置
conn.setRequestProperty("Range", "bytes=" + (bean.startDownloadPosition + bean.downloadedSize) + "-" + bean.endDownloadPosition);
conn.connect();
if (conn.getResponseCode() == 206) {
//更改下载状态
downloadState = STATE_LOADING;
bean.downloadState = STATE_LOADING;
Utils.Print("线程" + threadID + "连接成功");
is = conn.getInputStream();
// 1K的数据缓冲
byte[] bs = new byte[1024];
// 读取到的数据长度
int len;
//从指定的位置开始下载
file.seek(bean.startDownloadPosition);
// 循环读取,当已经下载的大小达到了指定的本线程负责的大小时跳出循环,线程之间负责的文件首尾有重合的话没有影响,因为写入的内容时相同的
while ((len = is.read(bs)) != -1) {
//不用在这个循环里面更新数据库
file.write(bs, 0, len);
//时时更新内存中的已下载大小信息
bean.downloadedSize += len;
//如果调用者暂停下载,则跳出结束方法
if (downloadState == STATE_PAUSING) {
Utils.Print("线程" + threadID + "暂停下载");
break;
}
}
is.close();
file.close();
} else {
Utils.Print("线程" + threadID + "连接失败");
}
conn.disconnect();
//如果这个线程已经下载完了自己负责的部分就修改下载状态
if (bean.downloadedSize >= bean.downloadTotalSize) {
bean.downloadState = STATE_FINISH;
} else {
bean.downloadState = STATE_PAUSING;
}
//内存中信息更新至数据库
finalDb.update(bean, "threadID='" + bean.threadID + "'");
} catch (IOException e) {
Utils.Print("线程" + threadID + "IO异常");
e.printStackTrace();
}
}
}