android實現(xiàn)多線程下載文件(支持暫停、取消、斷點續(xù)傳)
多線程下載文件(支持暫停、取消、斷點續(xù)傳)
多線程同時下載文件即:在同一時間內(nèi)通過多個線程對同一個請求地址發(fā)起多個請求,將需要下載的數(shù)據(jù)分割成多個部分,同時下載,每個線程只負(fù)責(zé)下載其中的一部分,最后將每一個線程下載的部分組裝起來即可。
涉及的知識及問題
- 請求的數(shù)據(jù)如何分段
- 分段完成后如何下載和下載完成后如何組裝到一起
- 暫停下載和繼續(xù)下載的實現(xiàn)(wait()、notifyAll()、synchronized的使用)
- 取消下載和斷點續(xù)傳的實現(xiàn)
一、請求的數(shù)據(jù)如何分段
首先通過HttpURLConnection請求總文件大小,而后根據(jù)線程數(shù)計算每一個線程的下載量,在分配給每一個線程去下載
fileLength = conn.getContentLength(); //根據(jù)文件大小,先創(chuàng)建一個空文件 //“r“——以只讀方式打開。調(diào)用結(jié)果對象的任何 write 方法都將導(dǎo)致拋出 IOException。 //“rw“——打開以便讀取和寫入。如果該文件尚不存在,則嘗試創(chuàng)建該文件。 //“rws“—— 打開以便讀取和寫入,對于 “rw”,還要求對文件的內(nèi)容或元數(shù)據(jù)的每個更新都同步寫入到底層存儲設(shè)備。 //“rwd“——打開以便讀取和寫入,對于 “rw”,還要求對文件內(nèi)容的每個更新都同步寫入到底層存儲設(shè)備。 RandomAccessFile raf = new RandomAccessFile(filePath, "rwd"); raf.setLength(fileLength); raf.close(); //計算各個線程下載的數(shù)據(jù)段 int blockLength = fileLength / threadCount;
二、分段完成后如何下載和下載完成后如何組裝到一起
分段完成后給每一個線程的請求頭設(shè)置Range參數(shù),他允許客戶端只請求文件的一部分?jǐn)?shù)據(jù),每一個線程只請求下載相應(yīng)范圍內(nèi)的數(shù)據(jù),使用RandomAccessFile(可隨機讀寫的文件)寫入到同一個文件里即可組裝成目標(biāo)文件Range,是在 HTTP/1.1里新增的一個 header field,它允許客戶端實際上只請求文檔的一部分(范圍可以相互重疊)
Range的使用形式:
| 屬性 | 解釋 |
|---|---|
| bytes=0-499 | 表示頭500個字節(jié) |
| bytes=500-999 | 表示第二個500字節(jié) |
| bytes=-500 | 表示最后500個字節(jié) |
| bytes=500- | 表示500字節(jié)以后的范圍 |
| bytes=0-0,-1 | 第一個和最后一個字節(jié) |
HttpUrlConnection中設(shè)置請求頭
URL url = new URL(loadUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition);
conn.setConnectTimeout(5000);
//若請求頭加上Range這個參數(shù),則返回狀態(tài)碼為206,而不是200
if (conn.getResponseCode() == 206) {
InputStream is = conn.getInputStream();
RandomAccessFile raf = new RandomAccessFile(filePath, "rwd");
raf.seek(startPosition);//跳到指定位置開始寫數(shù)據(jù)
}
三、暫停下載和繼續(xù)下載的實現(xiàn)(wait()、notifyAll()、synchronized的使用)
關(guān)于synchronized只需記住一下五點:
- 當(dāng)兩個并發(fā)線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內(nèi)只能有一個線程得到執(zhí)行。另一個線程必須等待當(dāng)前線程執(zhí)行完這個代碼塊以后才能執(zhí)行該代碼塊。
- 然而,當(dāng)一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。
- 尤其關(guān)鍵的是,當(dāng)一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。
- 第三個例子同樣適用其它同步代碼塊。也就是說,當(dāng)一個線程訪問object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結(jié)果,其它線程對該object對象所有同步代碼部分的訪問都被暫時阻塞。
- 以上規(guī)則對其它對象鎖同樣適用.
protected void onPause() {
if (mThreads != null)
stateDownload = DOWNLOAD_PAUSE;
}
protected void onStart() {
if (mThreads != null)
synchronized (DOWNLOAD_PAUSE) {
stateDownload = DOWNLOAD_ING;
DOWNLOAD_PAUSE.notifyAll();
}
}
對于wait()、notify()、notifyAll()需要注意的是
- 調(diào)用任何對象的wait()方法時,都必須先獲得該對象的鎖,即調(diào)用的wait()方法必須得寫在synchronized(obj){…}之內(nèi)
- 當(dāng)調(diào)用對象的wait()方法后,該線程若想繼續(xù)執(zhí)行,必須得再次獲得該對象的鎖才可以
- 如果A1,A2,A3線程都在obj.wait(),則B調(diào)用object.notify()只能喚醒A1,A2,A3中的一個(具體哪一個由JVM決定)
- 當(dāng)B調(diào)用object.notify/notifyAll的時候,B正持有object鎖,因此,A1,A2,A3雖被喚醒,但是仍無法獲得object鎖直到B退出synchronized塊,釋放object鎖后,A1,A2,A3中的一個/全部才有機會獲得鎖繼續(xù)執(zhí)行
synchronized (DOWNLOAD_PAUSE) {
if (stateDownload.equals(DOWNLOAD_PAUSE)) {
DOWNLOAD_PAUSE.wait();
}
}
四、取消下載和斷點續(xù)傳的實現(xiàn)
取消下載即取消每個線程的執(zhí)行,不建議直接使用Thread.stop()方法,安全的取消線程即run方法執(zhí)行結(jié)束。只要控制住循環(huán),就可以讓run方法結(jié)束,也就是線程結(jié)束
while ((len = is.read(buffer)) != -1) {
//是否繼續(xù)下載
if (!isGoOn)
break;
}
斷點續(xù)傳即其實和重新下載是一樣的,不過文件的大小和每一個線程下載時的起始位置和結(jié)束位置都不是重新計算的。而是上次取消下載時,每一個線程保存的當(dāng)前位置和結(jié)束位置,讓每一個線程接著上次的地方繼續(xù)下載即可
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
//獲取上次取消下載的進(jìn)度,若沒有則返回0
currLength = sp.getInt(CURR_LENGTH, 0);
for (int i = 0; i < threadCount; i++) {
//開始位置,獲取上次取消下載的進(jìn)度,默認(rèn)返回i*blockLength,即第i個線程開始下載的位置
int startPosition = sp.getInt(SP_NAME + (i + 1), i * blockLength);
//結(jié)束位置,-1是為了防止上一個線程和下一個線程重復(fù)下載銜接處數(shù)據(jù)
int endPosition = (i + 1) * blockLength - 1;
//將最后一個線程結(jié)束位置擴大,防止文件下載不完全,大了不影響,小了文件失效
if ((i + 1) == threadCount)
endPosition = endPosition * 2;
mThreads[i] = new DownThread(i + 1, startPosition, endPosition);
mThreads[i].start();
}
網(wǎng)絡(luò)獲取和讀寫SD卡都需要添加相應(yīng)權(quán)限
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
下面貼上全部的代碼,里面有詳細(xì)的注釋DownLoadFile.Java
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Message;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* Created by tianzhao on 2017/2/21 09:25.
* 多線程下載文件
*/
public class DownLoadFile {
private static final String SP_NAME = "download_file";
private static final String CURR_LENGTH = "curr_length";
private static final int DEFAULT_THREAD_COUNT = 4;//默認(rèn)下載線程數(shù)
//以下為線程狀態(tài)
private static final String DOWNLOAD_INIT = "1";
private static final String DOWNLOAD_ING = "2";
private static final String DOWNLOAD_PAUSE = "3";
private Context mContext;
private String loadUrl;//網(wǎng)絡(luò)獲取的url
private String filePath;//下載到本地的path
private int threadCount = DEFAULT_THREAD_COUNT;//下載線程數(shù)
private int fileLength;//文件總大小
//使用volatile防止多線程不安全
private volatile int currLength;//當(dāng)前總共下載的大小
private volatile int runningThreadCount;//正在運行的線程數(shù)
private Thread[] mThreads;
private String stateDownload = DOWNLOAD_INIT;//當(dāng)前線程狀態(tài)
private DownLoadListener mDownLoadListener;
public void setOnDownLoadListener(DownLoadListener mDownLoadListener) {
this.mDownLoadListener = mDownLoadListener;
}
interface DownLoadListener {
//返回當(dāng)前下載進(jìn)度的百分比
void getProgress(int progress);
void onComplete();
void onFailure();
}
public DownLoadFile(Context mContext, String loadUrl, String filePath) {
this(mContext, loadUrl, filePath, DEFAULT_THREAD_COUNT, null);
}
public DownLoadFile(Context mContext, String loadUrl, String filePath, DownLoadListener mDownLoadListener) {
this(mContext, loadUrl, filePath, DEFAULT_THREAD_COUNT, mDownLoadListener);
}
public DownLoadFile(Context mContext, String loadUrl, String filePath, int threadCount) {
this(mContext, loadUrl, filePath, threadCount, null);
}
public DownLoadFile(Context mContext, String loadUrl, String filePath, int threadCount, DownLoadListener mDownLoadListener) {
this.mContext = mContext;
this.loadUrl = loadUrl;
this.filePath = filePath;
this.threadCount = threadCount;
runningThreadCount = 0;
this.mDownLoadListener = mDownLoadListener;
}
/**
* 開始下載
*/
protected void downLoad() {
//在線程中運行,防止anr
new Thread(new Runnable() {
@Override
public void run() {
try {
//初始化數(shù)據(jù)
if (mThreads == null)
mThreads = new Thread[threadCount];
//建立連接請求
URL url = new URL(loadUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
int code = conn.getResponseCode();//獲取返回碼
if (code == 200) {//請求成功,根據(jù)文件大小開始分多線程下載
fileLength = conn.getContentLength();
//根據(jù)文件大小,先創(chuàng)建一個空文件
//“r“——以只讀方式打開。調(diào)用結(jié)果對象的任何 write 方法都將導(dǎo)致拋出 IOException。
//“rw“——打開以便讀取和寫入。如果該文件尚不存在,則嘗試創(chuàng)建該文件。
//“rws“—— 打開以便讀取和寫入,對于 “rw”,還要求對文件的內(nèi)容或元數(shù)據(jù)的每個更新都同步寫入到底層存儲設(shè)備。
//“rwd“——打開以便讀取和寫入,對于 “rw”,還要求對文件內(nèi)容的每個更新都同步寫入到底層存儲設(shè)備。
RandomAccessFile raf = new RandomAccessFile(filePath, "rwd");
raf.setLength(fileLength);
raf.close();
//計算各個線程下載的數(shù)據(jù)段
int blockLength = fileLength / threadCount;
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
//獲取上次取消下載的進(jìn)度,若沒有則返回0
currLength = sp.getInt(CURR_LENGTH, 0);
for (int i = 0; i < threadCount; i++) {
//開始位置,獲取上次取消下載的進(jìn)度,默認(rèn)返回i*blockLength,即第i個線程開始下載的位置
int startPosition = sp.getInt(SP_NAME + (i + 1), i * blockLength);
//結(jié)束位置,-1是為了防止上一個線程和下一個線程重復(fù)下載銜接處數(shù)據(jù)
int endPosition = (i + 1) * blockLength - 1;
//將最后一個線程結(jié)束位置擴大,防止文件下載不完全,大了不影響,小了文件失效
if ((i + 1) == threadCount)
endPosition = endPosition * 2;
mThreads[i] = new DownThread(i + 1, startPosition, endPosition);
mThreads[i].start();
}
} else {
handler.sendEmptyMessage(FAILURE);
}
} catch (Exception e) {
e.printStackTrace();
handler.sendEmptyMessage(FAILURE);
}
}
}).start();
}
/**
* 取消下載
*/
protected void cancel() {
if (mThreads != null) {
//若線程處于等待狀態(tài),則while循環(huán)處于阻塞狀態(tài),無法跳出循環(huán),必須先喚醒線程,才能執(zhí)行取消任務(wù)
if (stateDownload.equals(DOWNLOAD_PAUSE))
onStart();
for (Thread dt : mThreads) {
((DownThread) dt).cancel();
}
}
}
/**
* 暫停下載
*/
protected void onPause() {
if (mThreads != null)
stateDownload = DOWNLOAD_PAUSE;
}
/**
* 繼續(xù)下載
*/
protected void onStart() {
if (mThreads != null)
synchronized (DOWNLOAD_PAUSE) {
stateDownload = DOWNLOAD_ING;
DOWNLOAD_PAUSE.notifyAll();
}
}
protected void onDestroy() {
if (mThreads != null)
mThreads = null;
}
private class DownThread extends Thread {
private boolean isGoOn = true;//是否繼續(xù)下載
private int threadId;
private int startPosition;//開始下載點
private int endPosition;//結(jié)束下載點
private int currPosition;//當(dāng)前線程的下載進(jìn)度
private DownThread(int threadId, int startPosition, int endPosition) {
this.threadId = threadId;
this.startPosition = startPosition;
currPosition = startPosition;
this.endPosition = endPosition;
runningThreadCount++;
}
@Override
public void run() {
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
try {
URL url = new URL(loadUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition);
conn.setConnectTimeout(5000);
//若請求頭加上Range這個參數(shù),則返回狀態(tài)碼為206,而不是200
if (conn.getResponseCode() == 206) {
InputStream is = conn.getInputStream();
RandomAccessFile raf = new RandomAccessFile(filePath, "rwd");
raf.seek(startPosition);//跳到指定位置開始寫數(shù)據(jù)
int len;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
//是否繼續(xù)下載
if (!isGoOn)
break;
//回調(diào)當(dāng)前進(jìn)度
if (mDownLoadListener != null) {
currLength += len;
int progress = (int) ((float) currLength / (float) fileLength * 100);
handler.sendEmptyMessage(progress);
}
raf.write(buffer, 0, len);
//寫完后將當(dāng)前指針后移,為取消下載時保存當(dāng)前進(jìn)度做準(zhǔn)備
currPosition += len;
synchronized (DOWNLOAD_PAUSE) {
if (stateDownload.equals(DOWNLOAD_PAUSE)) {
DOWNLOAD_PAUSE.wait();
}
}
}
is.close();
raf.close();
//線程計數(shù)器-1
runningThreadCount--;
//若取消下載,則直接返回
if (!isGoOn) {
//此處采用SharedPreferences保存每個線程的當(dāng)前進(jìn)度,和三個線程的總下載進(jìn)度
if (currPosition < endPosition) {
sp.edit().putInt(SP_NAME + threadId, currPosition).apply();
sp.edit().putInt(CURR_LENGTH, currLength).apply();
}
return;
}
if (runningThreadCount == 0) {
sp.edit().clear().apply();
handler.sendEmptyMessage(SUCCESS);
handler.sendEmptyMessage(100);
mThreads = null;
}
} else {
sp.edit().clear().apply();
handler.sendEmptyMessage(FAILURE);
}
} catch (Exception e) {
sp.edit().clear().apply();
e.printStackTrace();
handler.sendEmptyMessage(FAILURE);
}
}
public void cancel() {
isGoOn = false;
}
}
private final int SUCCESS = 0x00000101;
private final int FAILURE = 0x00000102;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (mDownLoadListener != null) {
if (msg.what == SUCCESS) {
mDownLoadListener.onComplete();
} else if (msg.what == FAILURE) {
mDownLoadListener.onFailure();
} else {
mDownLoadListener.getProgress(msg.what);
}
}
}
};
}
在MainActivity中的使用
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
DownLoadFile downLoadFile;
private String loadUrl = "http://gdown.baidu.com/data/wisegame/d2fbbc8e64990454/wangyiyunyinle_87.apk";
private String filePath = Environment.getExternalStorageDirectory()+"/"+"網(wǎng)易云音樂.apk";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView tvprogress = (TextView) findViewById(R.id.tv_progress);
downLoadFile = new DownLoadFile(this,loadUrl, filePath, 3);
downLoadFile.setOnDownLoadListener(new DownLoadFile.DownLoadListener() {
@Override
public void getProgress(int progress) {
tvprogress.setText("當(dāng)前進(jìn)度 :"+progress+" %");
}
@Override
public void onComplete() {
Toast.makeText(MainActivity.this,"下載完成",Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure() {
Toast.makeText(MainActivity.this,"下載失敗",Toast.LENGTH_SHORT).show();
}
});
findViewById(R.id.bt).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
downLoadFile.downLoad();
}
});
findViewById(R.id.bt_pause).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
downLoadFile.onPause();
}
});
findViewById(R.id.bt_start).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
downLoadFile.onStart();
}
});
findViewById(R.id.bt_cancel).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
downLoadFile.cancel();
}
});
}
@Override
protected void onDestroy() {
downLoadFile.onDestroy();
super.onDestroy();
}
}
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android FTP 多線程斷點續(xù)傳下載\上傳的實例
- Android多線程+單線程+斷點續(xù)傳+進(jìn)度條顯示下載功能
- Android多線程斷點續(xù)傳下載功能實現(xiàn)代碼
- Android多線程斷點續(xù)傳下載示例詳解
- Android 使用AsyncTask實現(xiàn)多任務(wù)多線程斷點續(xù)傳下載
- Android實現(xiàn)網(wǎng)絡(luò)多線程斷點續(xù)傳下載實例
- Android編程開發(fā)實現(xiàn)多線程斷點續(xù)傳下載器實例
- PC版與Android手機版帶斷點續(xù)傳的多線程下載
- Android 使用AsyncTask實現(xiàn)多線程斷點續(xù)傳
- android原生實現(xiàn)多線程斷點續(xù)傳功能
相關(guān)文章
Android開發(fā)中WebView的詳細(xì)使用方法和常見操作
這篇文章詳細(xì)介紹了Android中WebView組件的使用方法和常見操作,包括基本初始化、啟用JavaScript、處理頁面導(dǎo)航、與JavaScript交互、加載本地HTML內(nèi)容、文件上傳與下載、進(jìn)度條與加載指示、處理網(wǎng)頁錯誤以及安全性設(shè)置,需要的朋友可以參考下2024-11-11
Android編程實現(xiàn)popupwindow彈出后屏幕背景變成半透明效果
這篇文章主要介紹了Android編程實現(xiàn)popupwindow彈出后屏幕背景變成半透明效果,涉及Android設(shè)置getWindows透明度的方法,需要的朋友可以參考下2016-01-01
Android的OkHttp包中的HTTP攔截器Interceptor用法示例
攔截器是OkHttp處理HTTP請求方面所具有的一個強大特性,這里我們就來看一下Android的OkHttp包中的HTTP攔截器Interceptor用法示例,需要的朋友可以參考下2016-07-07
解決Android從相冊中獲取圖片出錯圖片卻無法裁剪問題的方法
這篇文章主要介紹了解決Android從相冊中獲取圖片出錯圖片卻無法裁剪問題的方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-01-01
如何通過Battery Historian分析Android APP耗電情況
Android 從兩個層面統(tǒng)計電量的消耗,分別為軟件排行榜及硬件排行榜。它們各有自己的耗電榜單,軟件排行榜為機器中每個 App 的耗電榜單,硬件排行榜則為各個硬件的耗電榜單。這兩個排行榜的統(tǒng)計是互為獨立,互不干擾的2021-06-06
解決Android MediaRecorder錄制視頻過短問題
本文主要介紹Android MediaRecorder,在使用MediaRecorder時經(jīng)常會遇到視頻錄制太短問題,這里提供解決問題的實例代碼以供大家參考2016-07-07
android TextView不用ScrollViewe也可以滾動的方法
這篇文章主要介紹了android TextView不用ScrollViewe也可以滾動的方法,很簡單實用的代碼,大家參考使用吧2013-11-11

