Android 多线程断点下载
概念
多线程断点下载:意思是把一个下载文件分成多个,然后分配每个线程去下载分段,当每个线程下载完成一段时候,存储他的下载量,如果当网络不好,或者断开连接失败,那么下次从下载量开始地方下载,而不用重新下载。
技术难点
1、线程分配下载
URL url = new URL(downUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setReadTimeout(10 * 1000);
connection.setRequestMethod("GET");
int code = connection.getResponseCode();
if (code == 200) {
int fileLength = connection.getContentLength();
RandomAccessFile randomFile = new RandomAccessFile(new File(storePath), "rw");
randomFile.setLength(fileLength);
randomFile.close();
blockSize = fileLength / threadCount;//得到分段大小
for (int i = 0; i < threadCount; i++) {
int startBlock = i * blockSize;
int endBlock = (i + 1) * blockSize - 1;
if (i == threadCount - 1) {//如果是最后一个线程,下载完
endBlock = fileLength - 1;
}
//下载逻辑.....
}
}
2、这里主要是在网络连接时候,分段的读写和分段的写入
HttpURLConnection
.setRequestProperty("Range", "bytes=" + startBlock + "-" + endBlock);
startBlock是开始的下载点,endBlock是下载结束的点。
3、存储分段的文件下载量
我在这里使用文件的方式存储,你也可以使用其他方式,只有能持久化,就ok,而且这里使用了线程多个文件,你也可以使用单个文件存储,按照行来存储。
File file = new File(storePath.substring(0, storePath.lastIndexOf("/")), version + "_" + threadId + ".txt");
RandomAccessFile downLoadAss = null;
if (file != null && file.exists()) {
downLoadAss = new RandomAccessFile(file, "rwd");
String lastPositon = downLoadAss.readLine();
if (null == lastPositon || "".equals(lastPositon)) {
this.startBlock = startBlock;
} else {
if (lastPositon != null && !"".equals(lastPositon)) {
startBlock = Integer.parseInt(lastPositon) - 1;
}
}
} else {
downLoadAss = new RandomAccessFile(file, "rwd");
}
....
while ((length = input.read(bytes)) != -1) {
randomAccessFile.write(bytes, 0, length);
total += length;
downLoadAss.seek(0);
downLoadAss.write(String.valueOf(startBlock + total).getBytes("UTF-8"));
}
存储下载量的文件,如果里面有值,读取出来,然后重新设置起始点,然后在写入文件时候,写入读取的数据量。
具体代码实现
public class MutilDownHelper {
private static int blockSize;
private int currentRunThreadCount ;
/**
*
* @param downUrl 下载地址
* @param storePath 存储地址
* @param threadCount 线程池大小
* @param version 下载版本
* @return
*/
public int load(String downUrl, String storePath, int threadCount, String version) {
HttpURLConnection connection = null;
currentRunThreadCount = threadCount;
try {
URL url = new URL(downUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setReadTimeout(10 * 1000);
connection.setRequestMethod("GET");
int code = connection.getResponseCode();
if (code == 200) {
int fileLength = connection.getContentLength();
RandomAccessFile randomFile = new RandomAccessFile(new File(storePath), "rw");
randomFile.setLength(fileLength);
randomFile.close();
blockSize = fileLength / threadCount;
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()+1);
List downLoadThreads = new ArrayList<>();
for (int i = 0; i < threadCount; i++) {
int startBlock = i * blockSize;
int endBlock = (i + 1) * blockSize - 1;
if (i == threadCount - 1) {//如果是最后一个线程,下载完
endBlock = fileLength - 1;
}
downLoadThreads.add(new DownLoadThread(i, startBlock, endBlock, downUrl, storePath, version));
}
try {
List> futures = executorService.invokeAll(downLoadThreads);
for (Future future : futures) {
if (future.get() == 1) {//这里会等待 阻塞线程 1是成功的标识
currentRunThreadCount = currentRunThreadCount - 1;//还没有完成的进程
}
}
if (currentRunThreadCount == 0) {
return 1;
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
} catch (MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if (connection != null) {
connection.disconnect();
}
}
return 0;
}
public static class DownLoadThread implements Callable {
private int threadId;
private int startBlock;
private int endBlock;
private String downUrl;
private String storePath;
private String version;//解决下载中断,不同版本的切换问题
public DownLoadThread(int i, int startBlock, int endBlock, String url, String storePath, String version) {
this.threadId = i;
this.startBlock = startBlock;
this.endBlock = endBlock;
this.downUrl = url;
this.storePath = storePath;
this.version = version;
}
@Override
public Integer call() {
try {
URL url = new URL(downUrl);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
con.setConnectTimeout(10 * 1000);
File file = new File(storePath.substring(0, storePath.lastIndexOf("/")), version + "_" + threadId + ".txt");
RandomAccessFile downLoadAss = null;
if (file != null && file.exists()) {
downLoadAss = new RandomAccessFile(file, "rwd");
String lastPositon = downLoadAss.readLine();
if (null == lastPositon || "".equals(lastPositon)) {
this.startBlock = startBlock;
} else {
if (lastPositon != null && !"".equals(lastPositon)) {
startBlock = Integer.parseInt(lastPositon) - 1;
}
}
} else {
downLoadAss = new RandomAccessFile(file, "rwd");
}
con.setRequestProperty("Range", "bytes=" + startBlock + "-" + endBlock);
if (con.getResponseCode() == 206) {//请求部分成果
InputStream input = con.getInputStream();
RandomAccessFile randomAccessFile = new RandomAccessFile(new File(storePath), "rwd");
randomAccessFile.seek(startBlock);
byte[] bytes = new byte[1024 * 4];
int length = -1;
int total = 0;
while ((length = input.read(bytes)) != -1) {
randomAccessFile.write(bytes, 0, length);
total += length;
downLoadAss.seek(0);
downLoadAss.write(String.valueOf(startBlock + total).getBytes("UTF-8"));
}
downLoadAss.close();
randomAccessFile.close();
input.close();
Log.e("show", "线程" + threadId + "下载关闭");
File f = new File(storePath.substring(0, storePath.lastIndexOf("/")), version + "_" + threadId + ".txt");
f.delete();//删除记录下载的文件
return 1;
}
} catch (MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return 0;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return 0;
}
return 1;
}
}
这里我的场景是等这个线程分段下载完成后需要判断这些线程都写完成了,所以使用了Callable,然后在Future里面去判断是否都完成了,这也是一个比较难的点。当然如果你不需要这些判断,你也可以把calable改写成一个runable。这样实现也没有问题,我这样有一个好处,是我有返回值,判断我这个下载是否完成了,还是失败了。