此方法实现的下载,下载速度达到网速上限十几兆/S。
先讲大体实现思路,再讲其中各种导致下载速度上不去的坑。
原理:下载的时候多个线程并发可以占用服务器端更多资源,从而加快下载速度。
1. 请求下载链接地址,获取getContentLength,也就是文件总大小。
public boolean initDownLoadFileSize() {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(downLoadUrl)
.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.connect();
int responseCode = conn.getResponseCode();
if (responseCode == 200) {
fileSize = conn.getContentLength();
return true;
}
} catch (IOException e) {
}
return false;
}
2. 根据文件总大小,决定分几个子线程块去下载。
private void convertChunks(){
int MaximumUserCHUNKS = chunks/2;
chunks = 1;
for (int f=1 ; f <=MaximumUserCHUNKS ; f++)
if (fileSize > MegaByte*f)
chunks = f*2;
data = FileDB.getInstance().getFileDownChunkLog(this.package_name);
if (data.size() == chunks) {
for (int i = 0; i < chunks; i++) {
alreadyDownSize += data.get(i + 1);
}
} else {
data.clear();
for (int i = 0; i < chunks; i++) {
data.put(i + 1, 0);// 初始化每条线程已经下载的数据长度为0
}
FileDB.getInstance().deleteFileDownChunksLog(this.package_name);
FileDB.getInstance().saveFileDownChunkLog(this.package_name, data);
}
workList = new DownloadThread[chunks];
}
这一步骤核心:使用数据库维护各线程的下载进度,从而实现断点续传的功能。下载之前先读取数据库,查询是否有未完成的记录,有就继续下载,没有则创建新记录插入数据库
3. 根据分好的块开启子线程开始下载。
void startDownloadChunk(){
InputStream inStream = null;
RandomAccessFile threadfile = null;
try {
HttpURLConnection http = (HttpURLConnection) new URL(downUrl)
.openConnection();
http.setConnectTimeout( 10 * 1000 );
http.setReadTimeout( 10 * 1000 );
int startPos = block * (threadId - 1) + downLength;
int endPos;
if (threadId == downloader.getThreadNum()) {
endPos = downloader.getFileSize();
} else {
endPos = block * threadId - 1;
}
http.setRequestProperty("Range", "bytes=" + startPos + "-"
+ endPos);
inStream = http.getInputStream();
byte[] buffer = new byte[1024];
int offset;
threadfile = new RandomAccessFile(this.saveFile, "rw");
threadfile.seek(startPos);
while ( (offset = inStream.read(buffer)) > 0 ) {
threadfile.write(buffer, 0, offset);
downLength += offset;
downloader.append(offset);
if (isPause) {
downloader.Pause();
return;
}
}
downloader.checkDownloadFinish(this);
} catch (Exception e) {
LogUtil.i(e.toString());
if (tryTime < MAX_RETRY_TIMES) {
tryTime++;
startDownloadChunk();
}else {
downloader.Pause();
MyDownloadManager.getInstance().downFail(downloader, "下载失败,请重试",
true);
}
} finally {
downloader.update(threadId, downLength);
try {
if (threadfile != null)
threadfile.close();
if (inStream != null)
inStream.close();
} catch (IOException e) {
}
}
}
这一块核心主要是两个
一: 设置http请求的文件长度,http.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);
二:使用RandomAccessFile来实现多线程写入同一文件。
=======================================分割线=============================================
大体实现思路就是如上文所述,是不是感觉没几步,很简单? 0.0
但其实真正做起来还有很多细节,很多坑在。接下来,作为一个老司机,就谈谈我踩过的一些坑。
1.第一个问题就是分块,到底怎么分,分几个块合适。
博主最先开始从单线程下载改成多线程下载时,分的子线程都是2个...或者3个.... 改完后对比,差别真的不大....很尴尬,现在想想,当初太小气了。优化后的分块逻辑文章上半部分已贴,16子线程上限。
2.第二个问题是RandomAccessFile的创建。
网上帖子大多数是这么创建的:new RandomAccessFile(file, "rws");
rws模式下文件的写入是同步的,不是异步的,会大大降低多线程写入文件的速度哦。
所以,正确的姿势是这样子的: new RandomAccessFile(file, "rw");
3.第三个问题是各子线程下载进度的保存问题。
有些同学为了实现断点续传这个功能,跟博主年轻的时候一样,想当然的在写入文件时更新数据库。navie
频繁读写数据库同样也是降低下载速度的因素。
优雅的做法是在异常时才去更新保存进度。或者设置一个阀值。