1.原理
斷點(diǎn)續(xù)傳的理解可以分為兩部分:一部分是斷點(diǎn),一部分是續(xù)傳。斷點(diǎn)的由來是在下載的過程中,將一個(gè)下載文件分成了多個(gè)部分。同時(shí)進(jìn)行多個(gè)部分一起的下載,當(dāng)某個(gè)時(shí)間點(diǎn),任務(wù)被暫停了。此時(shí)下載暫停的位置就是斷點(diǎn)了。續(xù)傳就是一個(gè)未完成的任務(wù)再次開始時(shí),會從上次斷點(diǎn)繼續(xù)傳送。
使用多線程斷點(diǎn)續(xù)傳的時(shí)候,將下載后上傳任務(wù)(一個(gè)文件或者一個(gè)壓縮包)人為的劃分為幾個(gè)部分。每一部分采用一個(gè)線程進(jìn)行上傳或下載,多個(gè)線程并發(fā)可以占用服務(wù)器更多資源,從而加快下載速度。在下載(或上傳)過程中,如果網(wǎng)絡(luò)故障,點(diǎn)量不足等原因?qū)е孪螺d中斷,就需要用到斷點(diǎn)續(xù)傳功能。下次啟動時(shí),可以從記錄位置(已下載的部分)開始,繼續(xù)下載沒有下載的部分,避免重復(fù)部分的下載。斷點(diǎn)續(xù)傳的實(shí)質(zhì)就是能記錄上一次已下載完成的位置
斷點(diǎn)續(xù)傳的過程
1 斷點(diǎn)續(xù)傳需要在下載過程中記錄每條線程的下載進(jìn)度;
2 每次下載開始之前先讀取數(shù)據(jù)庫,查詢是否有未完成的記錄,有就繼續(xù)下載,沒有則創(chuàng)建記錄插入數(shù)據(jù)庫
3 在每次向文件中寫入數(shù)據(jù)之后,在數(shù)據(jù)庫中更新下載進(jìn)度
4 下載完成之后刪除數(shù)據(jù)庫中的進(jìn)度
2.代碼
package com.zhouzhuo.multitaskdownload;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.ProgressBar;
import java.io.File;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
/**
* Created by zhouzhuo on 2018/1/17.
*/
public class MainActivity extends Activity {
private String path = "http://117.169.69.238/mp3.9ku.com/m4a/186947.m4a";
private ProgressBar progressBar;
private String downloadPath = Environment.getDownloadCacheDirectory()
+ File.separator+"download";
private String targetFilePath="/"; //下載文件存放目錄
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
new MuchThreadDown(path, downloadPath, 3).downLoad();
} catch (Exception e) {
e.printStackTrace();
}
}
public class MuchThreadDown {
private String targetFilePath = "/";//下載文件存放目錄
private int threadCount = 3;//線程數(shù)量
private Map<Integer,ProgressBar> progressBarMap;//鍵 線程iD,值 進(jìn)度條控件
private Handler handler;
private String path;
/**
*
* @param path 要下載文件的網(wǎng)絡(luò)路徑
* @param targetFilePath 保存下載文件的目錄
* @param threadCount 開啟的線程數(shù)量,默認(rèn)為3
*/
public MuchThreadDown(String path,String targetFilePath,int threadCount){
this.path = path;
this.targetFilePath = targetFilePath;
this.threadCount = threadCount;
}
public Map<Integer,ProgressBar> getProgressBarMap(){
return progressBarMap;
}
public Handler getHandler(){
return handler;
}
public void setHandler(Handler handler){
this.handler = handler;
}
public void setProgressBarMap(Map<Integer,ProgressBar> progressBarMap){
this.progressBarMap = progressBarMap;
}
//下載文件
public void downLoad() throws Exception{
//連接資源
URL url = new URL(path);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000);
int code = connection.getResponseCode();
if(code == 200){
//獲取資源大小
int connectionLength = connection.getContentLength();
//在本地創(chuàng)建一個(gè)與資源文件同樣大小的文件來占位
RandomAccessFile randomAccessFile = new RandomAccessFile(new File(targetFilePath,getFileName(url)),"rw");
randomAccessFile.setLength(connectionLength);
//將下載任務(wù)分配給每個(gè)線程
//計(jì)算每個(gè)線程理論上下載的數(shù)量
int blockSize = connectionLength/threadCount;
//為每個(gè)線程分配任務(wù)
for (int threadId=0;threadId<threadCount;threadId++){
//線程開始下載的位置
int startIndex = blockSize*threadId;
//線程結(jié)束下載的位置
int endIndex = blockSize*(threadId+1)-1;
//如果是最后一個(gè)線程,將剩下的下載任務(wù)全部交給這個(gè)線程
if(threadId == threadCount -1){
endIndex = connectionLength - 1;
}
//TODO
if(progressBarMap !=null){
new DownloadThread(threadId,startIndex,endIndex,progressBarMap.get(threadId),handler);
}else {
new DownloadThread(threadId,startIndex,endIndex,null,handler).start();
}
}
}
}
}
private class DownloadThread extends Thread{
private int threadId;
private int startIndex;
private int endIndex;
private ProgressBar progressBar;
private int currentThreadTotal;//當(dāng)前線程下載文件的總大小
private Handler handler;
public DownloadThread(int threadId,
int startIndex,
int endIndex,
ProgressBar progressBar,
Handler handler){
this.threadId = threadId;
this.startIndex = startIndex;
this.endIndex = endIndex;
this.currentThreadTotal = endIndex -startIndex+1;
this.progressBar = progressBar;
this.handler = handler;
}
@Override
public void run() {
super.run();
Log.d("zhouzhuo","線程"+threadId+"開始下載");
//分段請求網(wǎng)絡(luò),分段將文件保存到本地
try {
URL url = new URL(path);
//加載下載文件的位置
File downThreadFile = new File(targetFilePath,getFileName(url)+"_downThread_"+threadId+".dt");
RandomAccessFile downThreadStream ;
if(downThreadFile.exists()){//
downThreadStream = new RandomAccessFile(downThreadFile,"rwd");
String startIndex_str = downThreadStream.readLine();
//設(shè)置下載起點(diǎn)
this.startIndex = Integer.parseInt(startIndex_str);
}else {
downThreadStream = new RandomAccessFile(downThreadFile,"rwd");
}
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000);
//設(shè)置分段下載的頭信息。Range:做分段數(shù)據(jù)請求用的。格式:Range types = 0-1024 或者 bytes:0-1024
connection.setRequestProperty("Range","bytes="+startIndex+"-"+endIndex);
Log.d("zhouzhuo","線程_"+threadId+"的下載起點(diǎn)是:"+startIndex+" 下載終點(diǎn)是:"+endIndex);
if(connection.getResponseCode() == 206){
//200:請求全部資源成功
//206: 代表部分資源請求成功
//獲取流
InputStream inputStream = connection.getInputStream();
//獲取前面已創(chuàng)建的文件
RandomAccessFile randomAccessFile =new RandomAccessFile(
new File(targetFilePath,getFileName(url)),"rw"
);
//文件寫入的開始位置
randomAccessFile.seek(startIndex);
//將網(wǎng)絡(luò)流中的文件寫入本地
byte[] buffer = new byte[1024*100];
int length = -1;
//記錄本次下載文件的大小
int total = 0;
boolean flag = false;
if(progressBar !=null){
flag = true;
progressBar.setMax(currentThreadTotal);
}
while ((length = inputStream.read(buffer))>0){
randomAccessFile.write(buffer,0,length);
total += length;
//將當(dāng)前下載到位置保持到文件中
int currentThreadPosition = startIndex +total;//當(dāng)前文件下載位置
downThreadStream.seek(0);
downThreadStream.write(((currentThreadPosition)+"").getBytes("UTF-8"));
//設(shè)置進(jìn)度條
if(flag){
progressBar.setProgress(currentThreadTotal-(endIndex-currentThreadPosition));
}
}
if(handler !=null){
Message message = Message.obtain();
message.what = 66;
message.obj = 1;
handler.sendMessage(message);
}
downThreadStream.close();
inputStream.close();
randomAccessFile.close();
//刪除臨時(shí)文件
cleanTemp(downThreadFile);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//獲取下載文件的名稱
private String getFileName(URL url) {
String filename = url.getFile();
return filename.substring(filename.lastIndexOf("/")+1);
}
//刪除線程產(chǎn)生的臨時(shí)文件
private synchronized void cleanTemp(File file){
file.delete();
}
}
3.代碼分析
首先根據(jù)下載的地址獲取連接流,得到下載文件的大小。然后根據(jù)下載線程的個(gè)數(shù)把文件拆分成若干斷,交給各個(gè)線程去下載。
每一個(gè)下載線程里面都有下載的起始和結(jié)束位置。
通過connection.setRequestProperty("Range","bytes="+startIndex+"-"+endIndex);設(shè)置分段請求。然后通過RandomAccessFile seek移動位置,然后在相應(yīng)位置RandomAccessFileWrite()寫入文件。