更新時間:2020-11-25 17:47:39 來源:動力節(jié)點 瀏覽1507次
說到多線程下載,我們可以把線程理解為下載的通道,一個線程就是文件下載的一個通道,多線程就是同時打開了多個通道對文件進行下載。當(dāng)服務(wù)器提供下載服務(wù)時,用戶之間共享帶寬,在優(yōu)先級相同的情況下,總服務(wù)器會對總下載線程進行平均分配。我們平時用的許多下載器下都是多線程下載。本文我們就來看看多線程下載的完整流程。
1.入口DownLoadManager.download()
/**
*
* @param request 請求實體參數(shù)Entity
* @param tag 下載地址
* @param callBack 返回給調(diào)用的CollBack
*/
public void download(DownloadRequest request, String tag, CallBack callBack) {
final String key = createKey(tag);
if (check(key)) {
// 請求的響應(yīng) 需要狀態(tài)傳遞類 以及對應(yīng)的回調(diào)
DownloadResponse response = new DownloadResponseImpl(mDelivery, callBack);
// 下載器 需要線程池 數(shù)據(jù)庫管理者 對應(yīng)的url key值 之后回調(diào)給自己
Downloader downloader = new DownloaderImpl(request, response,
mExecutorService, mDBManager, key, mConfig, this);
mDownloaderMap.put(key, downloader);
//開始下載
downloader.start();
}
}
DownloadResponseImpl下載響應(yīng)需要把本身的下載事件插入給調(diào)用者,由于下載是在子線程里面的,所以專門搞了一個下載狀態(tài)的傳遞類
DownLoaderImpl下載器需要的參數(shù)就比較多了,請求實體,對應(yīng)的下載響應(yīng),線程池,數(shù)據(jù)庫管理器,url的哈希值,對應(yīng)的配置,還有下載的一部分
加入進LinkedHashMap做一個有序的存儲
之后調(diào)用下載器的start方法。
2.開始下載開始
@Override
public void start() {
//修改為Started狀態(tài)
mStatus = DownloadStatus.STATUS_STARTED;
//CallBack 回調(diào)給調(diào)用者
mResponse.onStarted();
// 連接獲取是否支持多線程下載
connect();
}
/**
* 執(zhí)行連接任務(wù)
*/
private void connect() {
mConnectTask = new ConnectTaskImpl(mRequest.getUri(), this);
mExecutor.execute(mConnectTask);
}
在正式下載之前需要確定后臺是否支持?jǐn)帱c下載,所以才有先執(zhí)行這個ConnectTaskImpl連接任務(wù)。
3.ConnectTaskImpl連接任務(wù)
@Override
public void run() {
// 設(shè)置為后臺線程
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//修改連接中狀態(tài)
mStatus = DownloadStatus.STATUS_CONNECTING;
//回調(diào)給調(diào)用者
mOnConnectListener.onConnecting();
try {
//執(zhí)行連接方法
executeConnection();
} catch (DownloadException e) {
handleDownloadException(e);
}
}
/**
*
* @throws DownloadException
*/
private void executeConnection() throws DownloadException {
mStartTime = System.currentTimeMillis();
HttpURLConnection httpConnection = null;
final URL url;
try {
url = new URL(mUri);
} catch (MalformedURLException e) {
throw new DownloadException(DownloadStatus.STATUS_FAILED, "Bad url.", e);
}
try {
httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setConnectTimeout(Constants.HTTP.CONNECT_TIME_OUT);
httpConnection.setReadTimeout(Constants.HTTP.READ_TIME_OUT);
httpConnection.setRequestMethod(Constants.HTTP.GET);
httpConnection.setRequestProperty("Range", "bytes=" + 0 + "-");
final int responseCode = httpConnection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
//后臺不支持?jǐn)帱c下載,啟用單線程下載
parseResponse(httpConnection, false);
} else if (responseCode == HttpURLConnection.HTTP_PARTIAL) {
//后臺支持?jǐn)帱c下載,啟用多線程下載
parseResponse(httpConnection, true);
} else {
throw new DownloadException(DownloadStatus.STATUS_FAILED,
"UnSupported response code:" + responseCode);
}
} catch (ProtocolException e) {
throw new DownloadException(DownloadStatus.STATUS_FAILED, "Protocol error", e);
} catch (IOException e) {
throw new DownloadException(DownloadStatus.STATUS_FAILED, "IO error", e);
} finally {
if (httpConnection != null) {
httpConnection.disconnect();
}
}
}
private void parseResponse(HttpURLConnection httpConnection, boolean isAcceptRanges)
throws DownloadException {
final long length;
//header獲取length
String contentLength = httpConnection.getHeaderField("Content-Length");
if (TextUtils.isEmpty(contentLength) || contentLength.equals("0") || contentLength
.equals("-1")) {
//判斷后臺給你length,為null 0,-1,從連接中獲取
length = httpConnection.getContentLength();
} else {
//直接轉(zhuǎn)化
length = Long.parseLong(contentLength);
}
if (length <= 0) {
//拋出異常數(shù)據(jù)
throw new DownloadException(DownloadStatus.STATUS_FAILED, "length <= 0");
}
//判斷是否取消和暫停
checkCanceledOrPaused();
//Successful
mStatus = DownloadStatus.STATUS_CONNECTED;
//獲取時間差
final long timeDelta = System.currentTimeMillis() - mStartTime;
//回調(diào)給調(diào)用者
mOnConnectListener.onConnected(timeDelta, length, isAcceptRanges);
}
private void checkCanceledOrPaused() throws DownloadException {
if (isCanceled()) {
// cancel
throw new DownloadException(DownloadStatus.STATUS_CANCELED, "Connection Canceled!");
} else if (isPaused()) {
// paused
throw new DownloadException(DownloadStatus.STATUS_PAUSED, "Connection Paused!");
}
}
//統(tǒng)一執(zhí)行對應(yīng)的異常信息
private void handleDownloadException(DownloadException e) {
switch (e.getErrorCode()) {
case DownloadStatus.STATUS_FAILED:
synchronized (mOnConnectListener) {
mStatus = DownloadStatus.STATUS_FAILED;
mOnConnectListener.onConnectFailed(e);
}
break;
case DownloadStatus.STATUS_PAUSED:
synchronized (mOnConnectListener) {
mStatus = DownloadStatus.STATUS_PAUSED;
mOnConnectListener.onConnectPaused();
}
break;
case DownloadStatus.STATUS_CANCELED:
synchronized (mOnConnectListener) {
mStatus = DownloadStatus.STATUS_CANCELED;
mOnConnectListener.onConnectCanceled();
}
break;
default:
throw new IllegalArgumentException("Unknown state");
}
}
HttpURLConnection.HTTP_OK不支持?jǐn)帱c下載使用單線程下載
HttpURLConnection.HTTP_PARTIAL支持?jǐn)帱c下載使用多線程下載
如果成功就會發(fā)生到OnConnectListener.onConnected(timeDelta,length,isAcceptRanges)方法中。
4.查看下載器的onConnected()
@Override
public void onConnected(long time, long length, boolean isAcceptRanges) {
if (mConnectTask.isCanceled()) {
//連接取消
onConnectCanceled();
} else {
mStatus = DownloadStatus.STATUS_CONNECTED;
//回調(diào)給你響應(yīng)連接成功狀態(tài)
mResponse.onConnected(time, length, isAcceptRanges);
mDownloadInfo.setAcceptRanges(isAcceptRanges);
mDownloadInfo.setLength(length);
//真正開始下載
download(length, isAcceptRanges);
}
}
@Override
public void onConnectCanceled() {
deleteFromDB();
deleteFile();
mStatus = DownloadStatus.STATUS_CANCELED;
mResponse.onConnectCanceled();
onDestroy();
}
@Override
public void onDestroy() {
// trigger the onDestroy callback tell download manager
mListener.onDestroyed(mTag, this);
}
根據(jù)狀態(tài)來處理,isCanceled()刪除數(shù)據(jù)庫里面的數(shù)據(jù),刪除文件,更改為取消狀態(tài)狀態(tài)
未取消,進去下載。
5.下載文件下載方法
/**
* 下載開始
* @param length 設(shè)置下載的長度
* @param acceptRanges 是否支持?jǐn)帱c下載
*/
private void download(long length, boolean acceptRanges) {
mStatus = DownloadStatus.STATUS_PROGRESS;
initDownloadTasks(length, acceptRanges);
//開始下載任務(wù)
for (DownloadTask downloadTask : mDownloadTasks) {
mExecutor.execute(downloadTask);
}
}
/**
* 初始化下載任務(wù)
* @param length
* @param acceptRanges
*/
private void initDownloadTasks(long length, boolean acceptRanges) {
mDownloadTasks.clear();
if (acceptRanges) {
List<ThreadInfo> threadInfos = getMultiThreadInfos(length);
// init finished
int finished = 0;
for (ThreadInfo threadInfo : threadInfos) {
finished += threadInfo.getFinished();
}
mDownloadInfo.setFinished(finished);
for (ThreadInfo info : threadInfos) {
//開始多線程下載
mDownloadTasks.add(new MultiDownloadTask(mDownloadInfo, info, mDBManager, this));
}
} else {
//單線程下載不需要保存進度信息
ThreadInfo info = getSingleThreadInfo();
mDownloadTasks.add(new SingleDownloadTask(mDownloadInfo, info, this));
}
}
//TODO
private List<ThreadInfo> getMultiThreadInfos(long length) {
// init threadInfo from db
final List<ThreadInfo> threadInfos = mDBManager.getThreadInfos(mTag);
if (threadInfos.isEmpty()) {
final int threadNum = mConfig.getThreadNum();
for (int i = 0; i < threadNum; i++) {
// calculate average
final long average = length / threadNum;
final long start = average * i;
final long end;
if (i == threadNum - 1) {
end = length;
} else {
end = start + average - 1;
}
ThreadInfo threadInfo = new ThreadInfo(i, mTag, mRequest.getUri(), start, end, 0);
threadInfos.add(threadInfo);
}
}
return threadInfos;
}
//單線程數(shù)據(jù)
private ThreadInfo getSingleThreadInfo() {
ThreadInfo threadInfo = new ThreadInfo(0, mTag, mRequest.getUri(), 0);
return threadInfo;
}
根據(jù)已連接返回的數(shù)據(jù)判斷是否支持?jǐn)帱c下載,支持acceptRanges就調(diào)用getMultiThreadInfos來組裝多線程下載數(shù)據(jù),多線程需要初始化下載的進度信息,二單線程getSingleThreadInfo自己組裝一個簡單的就可以可以了。
6.執(zhí)行DownloadTaskImpl
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// 插入數(shù)據(jù)庫
insertIntoDB(mThreadInfo);
try {
mStatus = DownloadStatus.STATUS_PROGRESS;
executeDownload();
//根據(jù)回調(diào)對象,加鎖
synchronized (mOnDownloadListener) {
//沒出異常就代表下載完成了
mStatus = DownloadStatus.STATUS_COMPLETED;
mOnDownloadListener.onDownloadCompleted();
}
} catch (DownloadException e) {
handleDownloadException(e);
}
}
/**
* 開始下載數(shù)據(jù)
*/
private void executeDownload() throws DownloadException {
final URL url;
try {
url = new URL(mThreadInfo.getUri());
} catch (MalformedURLException e) {
throw new DownloadException(DownloadStatus.STATUS_FAILED, "Bad url.", e);
}
HttpURLConnection httpConnection = null;
try {
//設(shè)置http連接信息
httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setConnectTimeout(HTTP.CONNECT_TIME_OUT);
httpConnection.setReadTimeout(HTTP.READ_TIME_OUT);
httpConnection.setRequestMethod(HTTP.GET);
//設(shè)置header數(shù)據(jù),斷點下載設(shè)置關(guān)鍵
setHttpHeader(getHttpHeaders(mThreadInfo), httpConnection);
final int responseCode = httpConnection.getResponseCode();
if (responseCode == getResponseCode()) {
//下載數(shù)據(jù)
transferData(httpConnection);
} else {
throw new DownloadException(DownloadStatus.STATUS_FAILED,
"UnSupported response code:" + responseCode);
}
} catch (ProtocolException e) {
throw new DownloadException(DownloadStatus.STATUS_FAILED, "Protocol error", e);
} catch (IOException e) {
throw new DownloadException(DownloadStatus.STATUS_FAILED, "IO error", e);
} finally {
if (httpConnection != null) {
httpConnection.disconnect();
}
}
}
/**
* 設(shè)置header數(shù)據(jù)
*
* @param headers header元數(shù)據(jù)
*/
private void setHttpHeader(Map<String, String> headers, URLConnection connection) {
if (headers != null) {
for (String key : headers.keySet()) {
connection.setRequestProperty(key, headers.get(key));
}
}
}
/**
* 下載數(shù)據(jù)
*/
private void transferData(HttpURLConnection httpConnection) throws DownloadException {
InputStream inputStream = null;
RandomAccessFile raf = null;
try {
try {
inputStream = httpConnection.getInputStream();
} catch (IOException e) {
throw new DownloadException(DownloadStatus.STATUS_FAILED, "http get inputStream error", e);
}
//獲取下載的偏移量
final long offset = mThreadInfo.getStart() + mThreadInfo.getFinished();
try {
//設(shè)置偏移量
raf = getFile(mDownloadInfo.getDir(), mDownloadInfo.getName(), offset);
} catch (IOException e) {
throw new DownloadException(DownloadStatus.STATUS_FAILED, "File error", e);
}
//開始寫入數(shù)據(jù)
transferData(inputStream, raf);
} finally {
try {
IOCloseUtils.close(inputStream);
IOCloseUtils.close(raf);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 寫入數(shù)據(jù)
*/
private void transferData(InputStream inputStream, RandomAccessFile raf)
throws DownloadException {
final byte[] buffer = new byte[1024 * 8];
while (true) {
checkPausedOrCanceled();
int len = -1;
try {
len = inputStream.read(buffer);
if (len == -1) {
break;
}
raf.write(buffer, 0, len);
//設(shè)置下載的信息
mThreadInfo.setFinished(mThreadInfo.getFinished() + len);
synchronized (mOnDownloadListener) {
mDownloadInfo.setFinished(mDownloadInfo.getFinished() + len);
//回調(diào)進度
mOnDownloadListener
.onDownloadProgress(mDownloadInfo.getFinished(), mDownloadInfo.getLength());
}
} catch (IOException e) {
//更新數(shù)據(jù)庫
updateDB(mThreadInfo);
throw new DownloadException(DownloadStatus.STATUS_FAILED, e);
}
}
}
斷點下載的關(guān)鍵是在header頭信息里面添加了已經(jīng)下載的長度,下載數(shù)據(jù)也是從下載的長度點開始寫入數(shù)據(jù),寫入數(shù)據(jù),每個線程在對應(yīng)的片段里面下載對應(yīng)的數(shù)據(jù),后續(xù)使用RandomAccessFile組裝起來,合成一個文件。
以上就是整個多線程下載的完整流程,多線程下載的優(yōu)勢也是一覽無余。在同等的網(wǎng)絡(luò)傳輸速度下,多線程下載還是要比單線程下載更加高效的。本站的Java多線程教程中對此有詳細的講解,希望喜歡刨根問底的小伙伴可以找到自己滿意的答案。
初級 202925
初級 203221
初級 202629
初級 203743