NO IMAGE

多執行緒下載檔案(支援暫停、取消、斷點續傳)

多執行緒同時下載檔案即:在同一時間內通過多個執行緒對同一個請求地址發起多個請求,將需要下載的資料分割成多個部分,同時下載,每個執行緒只負責下載其中的一部分,最後將每一個執行緒下載的部分組裝起來即可。

涉及的知識及問題

  • 請求的資料如何分段
  • 分段完成後如何下載和下載完成後如何組裝到一起
  • 暫停下載和繼續下載的實現(wait()、notifyAll()、synchronized的使用)
  • 取消下載和斷點續傳的實現

一、請求的資料如何分段

首先通過HttpURLConnection請求總檔案大小,而後根據執行緒數計算每一個執行緒的下載量,在分配給每一個執行緒去下載

fileLength = conn.getContentLength();
//根據檔案大小,先建立一個空檔案
//“r“——以只讀方式開啟。呼叫結果物件的任何 write 方法都將導致丟擲 IOException。
//“rw“——開啟以便讀取和寫入。如果該檔案尚不存在,則嘗試建立該檔案。
//“rws“—— 開啟以便讀取和寫入,對於 “rw”,還要求對檔案的內容或後設資料的每個更新都同步寫入到底層儲存裝置。
//“rwd“——開啟以便讀取和寫入,對於 “rw”,還要求對檔案內容的每個更新都同步寫入到底層儲存裝置。
RandomAccessFile raf = new RandomAccessFile(filePath, "rwd");
raf.setLength(fileLength);
raf.close();
//計算各個執行緒下載的資料段
int blockLength = fileLength / threadCount;

二、分段完成後如何下載和下載完成後如何組裝到一起

分段完成後給每一個執行緒的請求頭設定Range引數,他允許客戶端只請求檔案的一部分資料,每一個執行緒只請求下載相應範圍內的資料,使用RandomAccessFile(可隨機讀寫的檔案)寫入到同一個檔案裡即可組裝成目標檔案Range,是在 HTTP/1.1裡新增的一個 header field,它允許客戶端實際上只請求文件的一部分(範圍可以相互重疊)

Range的使用形式:

屬性解釋
bytes=0-499表示頭500個位元組
bytes=500-999表示第二個500位元組
bytes=-500表示最後500個位元組
bytes=500-表示500位元組以後的範圍
bytes=0-0,-1第一個和最後一個位元組

HttpUrlConnection中設定請求頭

URL url = new URL(loadUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Range", "bytes="   startPosition   "-"   endPosition);
conn.setConnectTimeout(5000);
//若請求頭加上Range這個引數,則返回狀態碼為206,而不是200
if (conn.getResponseCode() == 206) {
    InputStream is = conn.getInputStream();
    RandomAccessFile raf = new RandomAccessFile(filePath, "rwd");
    raf.seek(startPosition);//跳到指定位置開始寫資料
}

三、暫停下載和繼續下載的實現(wait()、notifyAll()、synchronized的使用)

關於synchronized只需記住一下五點:

  1. 當兩個併發執行緒訪問同一個物件object中的這個synchronized(this)同步程式碼塊時,一個時間內只能有一個執行緒得到執行。另一個執行緒必須等待當前執行緒執行完這個程式碼塊以後才能執行該程式碼塊。
  2. 然而,當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,另一個執行緒仍然可以訪問該object中的非synchronized(this)同步程式碼塊。
  3. 尤其關鍵的是,當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,其他執行緒對object中所有其它synchronized(this)同步程式碼塊的訪問將被阻塞。
  4. 第三個例子同樣適用其它同步程式碼塊。也就是說,當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,它就獲得了這個object的物件鎖。結果,其它執行緒對該object物件所有同步程式碼部分的訪問都被暫時阻塞。
  5. 以上規則對其它物件鎖同樣適用.
    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()需要注意的是

  1. 呼叫任何物件的wait()方法時,都必須先獲得該物件的鎖,即呼叫的wait()方法必須得寫在synchronized(obj){…}之內
  2. 當呼叫物件的wait()方法後,該執行緒若想繼續執行,必須得再次獲得該物件的鎖才可以
  3. 如果A1,A2,A3執行緒都在obj.wait(),則B呼叫object.notify()只能喚醒A1,A2,A3中的一個(具體哪一個由JVM決定)
  4. 當B呼叫object.notify/notifyAll的時候,B正持有object鎖,因此,A1,A2,A3雖被喚醒,但是仍無法獲得object鎖直到B退出synchronized塊,釋放object鎖後,A1,A2,A3中的一個/全部才有機會獲得鎖繼續執行
    synchronized (DOWNLOAD_PAUSE) {
        if (stateDownload.equals(DOWNLOAD_PAUSE)) {
            DOWNLOAD_PAUSE.wait();
        }
    }

四、取消下載和斷點續傳的實現

取消下載即取消每個執行緒的執行,不建議直接使用Thread.stop()方法,安全的取消執行緒即run方法執行結束。只要控制住迴圈,就可以讓run方法結束,也就是執行緒結束

    while ((len = is.read(buffer)) != -1) {
        //是否繼續下載
        if (!isGoOn)
            break;
    }

斷點續傳即其實和重新下載是一樣的,不過檔案的大小和每一個執行緒下載時的起始位置和結束位置都不是重新計算的。而是上次取消下載時,每一個執行緒儲存的當前位置和結束位置,讓每一個執行緒接著上次的地方繼續下載即可

    SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
    //獲取上次取消下載的進度,若沒有則返回0
    currLength = sp.getInt(CURR_LENGTH, 0);
    for (int i = 0; i < threadCount; i  ) {
        //開始位置,獲取上次取消下載的進度,預設返回i*blockLength,即第i個執行緒開始下載的位置
        int startPosition = sp.getInt(SP_NAME   (i   1), i * blockLength);
        //結束位置,-1是為了防止上一個執行緒和下一個執行緒重複下載銜接處資料
        int endPosition = (i   1) * blockLength - 1;
        //將最後一個執行緒結束位置擴大,防止檔案下載不完全,大了不影響,小了檔案失效
        if ((i   1) == threadCount)
        endPosition = endPosition * 2;
        mThreads[i] = new DownThread(i   1, startPosition, endPosition);
        mThreads[i].start();
    }

網路獲取和讀寫SD卡都需要新增相應許可權

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

下面貼上全部的程式碼,裡面有詳細的註釋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;//預設下載執行緒數
//以下為執行緒狀態
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;//網路獲取的url
private String filePath;//下載到本地的path
private int threadCount = DEFAULT_THREAD_COUNT;//下載執行緒數
private int fileLength;//檔案總大小
//使用volatile防止多執行緒不安全
private volatile int currLength;//當前總共下載的大小
private volatile int runningThreadCount;//正在執行的執行緒數
private Thread[] mThreads;
private String stateDownload = DOWNLOAD_INIT;//當前執行緒狀態
private DownLoadListener mDownLoadListener;
public void setOnDownLoadListener(DownLoadListener mDownLoadListener) {
this.mDownLoadListener = mDownLoadListener;
}
interface DownLoadListener {
//返回當前下載進度的百分比
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 {
//初始化資料
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) {//請求成功,根據檔案大小開始分多執行緒下載
fileLength = conn.getContentLength();
//根據檔案大小,先建立一個空檔案
//“r“——以只讀方式開啟。呼叫結果物件的任何 write 方法都將導致丟擲 IOException。
//“rw“——開啟以便讀取和寫入。如果該檔案尚不存在,則嘗試建立該檔案。
//“rws“—— 開啟以便讀取和寫入,對於 “rw”,還要求對檔案的內容或後設資料的每個更新都同步寫入到底層儲存裝置。
//“rwd“——開啟以便讀取和寫入,對於 “rw”,還要求對檔案內容的每個更新都同步寫入到底層儲存裝置。
RandomAccessFile raf = new RandomAccessFile(filePath, "rwd");
raf.setLength(fileLength);
raf.close();
//計算各個執行緒下載的資料段
int blockLength = fileLength / threadCount;
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
//獲取上次取消下載的進度,若沒有則返回0
currLength = sp.getInt(CURR_LENGTH, 0);
for (int i = 0; i < threadCount; i  ) {
//開始位置,獲取上次取消下載的進度,預設返回i*blockLength,即第i個執行緒開始下載的位置
int startPosition = sp.getInt(SP_NAME   (i   1), i * blockLength);
//結束位置,-1是為了防止上一個執行緒和下一個執行緒重複下載銜接處資料
int endPosition = (i   1) * blockLength - 1;
//將最後一個執行緒結束位置擴大,防止檔案下載不完全,大了不影響,小了檔案失效
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) {
//若執行緒處於等待狀態,則while迴圈處於阻塞狀態,無法跳出迴圈,必須先喚醒執行緒,才能執行取消任務
if (stateDownload.equals(DOWNLOAD_PAUSE))
onStart();
for (Thread dt : mThreads) {
((DownThread) dt).cancel();
}
}
}
/**
* 暫停下載
*/
protected void onPause() {
if (mThreads != null)
stateDownload = DOWNLOAD_PAUSE;
}
/**
* 繼續下載
*/
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;//是否繼續下載
private int threadId;
private int startPosition;//開始下載點
private int endPosition;//結束下載點
private int currPosition;//當前執行緒的下載進度
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這個引數,則返回狀態碼為206,而不是200
if (conn.getResponseCode() == 206) {
InputStream is = conn.getInputStream();
RandomAccessFile raf = new RandomAccessFile(filePath, "rwd");
raf.seek(startPosition);//跳到指定位置開始寫資料
int len;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
//是否繼續下載
if (!isGoOn)
break;
//回撥當前進度
if (mDownLoadListener != null) {
currLength  = len;
int progress = (int) ((float) currLength / (float) fileLength * 100);
handler.sendEmptyMessage(progress);
}
raf.write(buffer, 0, len);
//寫完後將當前指標後移,為取消下載時儲存當前進度做準備
currPosition  = len;
synchronized (DOWNLOAD_PAUSE) {
if (stateDownload.equals(DOWNLOAD_PAUSE)) {
DOWNLOAD_PAUSE.wait();
}
}
}
is.close();
raf.close();
//執行緒計數器-1
runningThreadCount--;
//若取消下載,則直接返回
if (!isGoOn) {
//此處採用SharedPreferences儲存每個執行緒的當前進度,和三個執行緒的總下載進度
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() "/" "網易雲音樂.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("當前進度 :" 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();
}
}

關於多執行緒下載檔案就說道這,若有不正之處,多多指教!