Android 多線程斷點(diǎn)下載
概念
多線程斷點(diǎn)下載:意思是把一個下載文件分成多個置森,然后分配每個線程去下載分段,當(dāng)每個線程下載完成一段時候帐我,存儲他的下載量,如果當(dāng)網(wǎng)絡(luò)不好愧膀,或者斷開連接失敗拦键,那么下次從下載量開始地方下載,而不用重新下載檩淋。
技術(shù)難點(diǎn)
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蟀悦、這里主要是在網(wǎng)絡(luò)連接時候媚朦,分段的讀寫和分段的寫入
HttpURLConnection
.setRequestProperty("Range", "bytes=" + startBlock + "-" + endBlock);
startBlock是開始的下載點(diǎn),endBlock是下載結(jié)束的點(diǎn)熬芜。
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"));
}
存儲下載量的文件季俩,如果里面有值,讀取出來梅掠,然后重新設(shè)置起始點(diǎn),然后在寫入文件時候酌住,寫入讀取的數(shù)據(jù)量。
具體代碼實現(xiàn)
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<DownLoadThread> 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<Future<Integer>> futures = executorService.invokeAll(downLoadThreads);
for (Future<Integer> future : futures) {
if (future.get() == 1) {//這里會等待 阻塞線程 1是成功的標(biāo)識
currentRunThreadCount = currentRunThreadCount - 1;//還沒有完成的進(jìn)程
}
}
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<Integer> {
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 + "下載關(guān)閉");
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里面去判斷是否都完成了颅眶,這也是一個比較難的點(diǎn)啊易。當(dāng)然如果你不需要這些判斷,你也可以把calable改寫成一個runable逞带。這樣實現(xiàn)也沒有問題欺矫,我這樣有一個好處,是我有返回值展氓,判斷我這個下載是否完成了穆趴,還是失敗了。