1. 程式人生 > >Android實現多執行緒下載檔案,支援斷點

Android實現多執行緒下載檔案,支援斷點

本篇部落格主要介紹多執行緒去下載檔案,以下載多個app為例。不管去下在app,音視訊等檔案,實現起來都一樣。篇幅有點長,先貼張美女圖看看

這裡寫圖片描述

正在下載的效果圖

2018-04-25_13_36_47.gif

下載完成效果圖

這裡寫圖片描述
小編的下載路徑是放在sd卡的絕對路徑中,方便驗證!

工程目錄圖

這裡寫圖片描述

介紹下每個類是幹什麼的


DownloadCallback:下載完成回撥介面,包含三個方法 void onSuccess(File file)、void onFailure(Exception e)、void onProgress(long progress,long currentLength);
DownloadDispatcher:負責建立執行緒池,連線下載的檔案;
DownloadRunnable:每個執行緒的執行對應的任務;
DownloadTask:每個apk的下載,這個類需要複用的;
ircleProgressbar:自定義的圓形進度條;

具體思路:

1、首先自定義一個圓形進度條CircleProgressbar,實時更新進度
2、建立執行緒池,計算每個執行緒對應的不同的Range
3、每個執行緒下載完畢之後的回撥,若出現了異常怎麼處理

OkHttpManager類

public class OkHttpManager {
private static final OkHttpManager sOkHttpManager = new OkHttpManager();
private OkHttpClient okHttpClient;

private OkHttpManager() {
    okHttpClient = new
OkHttpClient(); } public static OkHttpManager getInstance() { return sOkHttpManager; } public Call asyncCall(String url) { Request request = new Request.Builder() .url(url) .build(); return okHttpClient.newCall(request); } public Response syncResponse(String url, long
start, long end) throws IOException { Request request = new Request.Builder() .url(url) //Range 請求頭格式Range: bytes=start-end .addHeader("Range", "bytes=" + start + "-" + end) .build(); return okHttpClient.newCall(request).execute(); } }

大家可能會看到這個Range很懵,Range是啥?

什麼是Range?
當用戶在聽一首歌的時候,如果聽到一半(網路下載了一半),網路斷掉了,使用者需要繼續聽的時候,檔案伺服器不支援斷點的話,則使用者需要重新下載這個檔案。而Range支援的話,客戶端應該記錄了之前已經讀取的檔案範圍,網路恢復之後,則向伺服器傳送讀取剩餘Range的請求,服務端只需要傳送客戶端請求的那部分內容,而不用整個 檔案傳送回客戶端,以此節省網路頻寬。

例如:
Range: bytes=10- :第10個位元組及最後個位元組的資料 。
Range: bytes=40-100 :第40個位元組到第100個位元組之間的資料。
注意,這個表示[start,end],即是包含請求頭的start及end位元組的,所以,下一個請求,應該是上一個請求的[end+1, nextEnd]

DownloadCallback類

public interface DownloadCallback {
    /**
     * 下載成功
     *
     * @param file
     */
    void onSuccess(File file);

    /**
     * 下載失敗
     *
     * @param e
     */
    void onFailure(Exception e);

    /**
     * 下載進度
     *
     * @param progress
     */
    void onProgress(long progress,long currentLength);
}

DownloadCallback:下載完成回撥介面,包含三個方法 void onSuccess(File file)下載檔案成功回撥、void onFailure(Exception e)下載檔案失敗回撥、void onProgress(long progress,long currentLength) 下載檔案實時更新下圓形進度條。

DownloadDispatcher 類

public class DownloadDispatcher {
    private static final String TAG = "DownloadDispatcher";
    private static volatile DownloadDispatcher sDownloadDispatcher;
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int THREAD_SIZE = Math.max(3, Math.min(CPU_COUNT - 1, 5));
    //核心執行緒數
    private static final int CORE_POOL_SIZE = THREAD_SIZE;
    //執行緒池
    private ExecutorService mExecutorService;
    //private final Deque<DownloadTask> readyTasks = new ArrayDeque<>();
    private final Deque<DownloadTask> runningTasks = new ArrayDeque<>();
    //private final Deque<DownloadTask> stopTasks = new ArrayDeque<>();


private DownloadDispatcher() {
}

public static DownloadDispatcher getInstance() {
    if (sDownloadDispatcher == null) {
        synchronized (DownloadDispatcher.class) {
            if (sDownloadDispatcher == null) {
                sDownloadDispatcher = new DownloadDispatcher();
            }
        }
    }
    return sDownloadDispatcher;
}

/**
 * 建立執行緒池
 *
 * @return mExecutorService
 */
public synchronized ExecutorService executorService() {
    if (mExecutorService == null) {
        mExecutorService = new ThreadPoolExecutor(CORE_POOL_SIZE, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>(), new ThreadFactory() {
            @Override
            public Thread newThread(@NonNull Runnable r) {
                Thread thread = new Thread(r);
                thread.setDaemon(false);
                return thread;
            }
        });
    }
    return mExecutorService;
}


/**
 * @param name     檔名
 * @param url      下載的地址
 * @param callBack 回撥介面
 */
public void startDownload(final String name, final String url, final DownloadCallback callBack) {
    Call call = OkHttpManager.getInstance().asyncCall(url);
    call.enqueue(new Callback() {
        @Override
        public void onFailure(@NonNull Call call, @NonNull IOException e) {
            callBack.onFailure(e);
        }

        @Override
        public void onResponse(@NonNull Call call, @NonNull Response response) {
            //獲取檔案的大小
            long contentLength = response.body().contentLength();
            Log.i(TAG, "contentLength=" + contentLength);
            if (contentLength <= -1) {
                return;
            }
            DownloadTask downloadTask = new DownloadTask(name, url, THREAD_SIZE, contentLength, callBack);
            downloadTask.init();
            runningTasks.add(downloadTask);
        }
    });
}


/**
 * @param downLoadTask 下載任務
 */
public void recyclerTask(DownloadTask downLoadTask) {
    runningTasks.remove(downLoadTask);
    //參考OkHttp的Dispatcher()的原始碼
    //readyTasks.
}

public void stopDownLoad(String url) {
    //這個停止是不是這個正在下載的
}
}

DownloadDispatcher這個類主要負責建立執行緒池,連線下載的檔案,如果你要控制下載檔案的個數,比如3-5個,可以在這個類控制,比如你最大允許同時下載三個檔案,每個檔案有五個執行緒去下載,那麼maxRequest只有15個執行緒,其餘的可以放到readyTasks 中,有一個執行緒下載完畢了可以remove()掉,總結起來說一句話,去仿照okhttp的Dispatcher原始碼去寫,runningTasks、readyTasks、stopTasks。

DownloadTask類

public class DownloadTask {
    private static final String TAG = "DownloadTask";
    //檔案下載的url
    private String url;
    //檔案的名稱
    private String name;
    //檔案的大小
    private long mContentLength;
    //下載檔案的執行緒的個數
    private int mThreadSize;
    //執行緒下載成功的個數,變數加個volatile,多執行緒保證變數可見性
    private volatile int mSuccessNumber;
    //總進度=每個執行緒的進度的和
    private long mTotalProgress;
    private List<DownloadRunnable> mDownloadRunnables;
    private DownloadCallback mDownloadCallback;


public DownloadTask(String name, String url, int threadSize, long contentLength, DownloadCallback callBack) {
    this.name = name;
    this.url = url;
    this.mThreadSize = threadSize;
    this.mContentLength = contentLength;
    this.mDownloadRunnables = new ArrayList<>();
    this.mDownloadCallback = callBack;
}

public void init() {
    for (int i = 0; i < mThreadSize; i++) {
        //初始化的時候,需要讀取資料庫
        //每個執行緒的下載的大小threadSize
        long threadSize = mContentLength / mThreadSize;
        //開始下載的位置
        long start = i * threadSize;
        //結束下載的位置
        long end = start + threadSize - 1;
        if (i == mThreadSize - 1) {
            end = mContentLength - 1;
        }
        DownloadRunnable downloadRunnable = new DownloadRunnable(name, url, mContentLength, i, start, end, new DownloadCallback() {
            @Override
            public void onFailure(Exception e) {
                //有一個執行緒發生異常,下載失敗,需要把其它執行緒停止掉
                mDownloadCallback.onFailure(e);
                stopDownload();
            }

            @Override
            public void onSuccess(File file) {
                mSuccessNumber = mSuccessNumber + 1;
                if (mSuccessNumber == mThreadSize) {
                    mDownloadCallback.onSuccess(file);
                    DownloadDispatcher.getInstance().recyclerTask(DownloadTask.this);
                    //如果下載完畢,清除資料庫  todo
                }
            }

            @Override
            public void onProgress(long progress, long currentLength) {
                //疊加下progress,實時去更新進度條
                //這裡需要synchronized下
                synchronized (DownloadTask.this) {
                    mTotalProgress = mTotalProgress + progress;
                    //Log.i(TAG, "mTotalProgress==" + mTotalProgress);
                    mDownloadCallback.onProgress(mTotalProgress, currentLength);
                }
            }
        });
        //通過執行緒池去執行
        DownloadDispatcher.getInstance().executorService().execute(downloadRunnable);
        mDownloadRunnables.add(downloadRunnable);
    }
}

/**
 * 停止下載
 */
public void stopDownload() {
    for (DownloadRunnable runnable : mDownloadRunnables) {
        runnable.stop();
    }
}

DownloadTask負責每個apk的下載,這個類需要複用的。計算每個執行緒下載範圍的大小,具體的每個變數是啥?註釋寫的很清楚。注意的是這個變數mSuccessNumber,執行緒下載成功的個數,變數加個volatile,多執行緒保證變數可見性。還有的就是疊加下progress的時候mTotalProgress = mTotalProgress + progress,需要synchronized(DownloadTask.this)下,保證這個變數mTotalProgress記憶體可見,並同步下。

DownloadRunnable類

public class DownloadRunnable implements Runnable {
    private static final String TAG = "DownloadRunnable";
    private static final int STATUS_DOWNLOADING = 1;
    private static final int STATUS_STOP = 2;
    //執行緒的狀態
    private int mStatus = STATUS_DOWNLOADING;
    //檔案下載的url
    private String url;
    //檔案的名稱
    private String name;
    //執行緒id
    private int threadId;
    //每個執行緒下載開始的位置
    private long start;
    //每個執行緒下載結束的位置
    private long end;
    //每個執行緒的下載進度
    private long mProgress;
    //檔案的總大小 content-length
    private long mCurrentLength;
    private DownloadCallback downloadCallback;

public DownloadRunnable(String name, String url, long currentLength, int threadId, long start, long end, DownloadCallback downloadCallback) {
    this.name = name;
    this.url = url;
    this.mCurrentLength = currentLength;
    this.threadId = threadId;
    this.start = start;
    this.end = end;
    this.downloadCallback = downloadCallback;
}

@Override
public void run() {
    InputStream inputStream = null;
    RandomAccessFile randomAccessFile = null;
    try {
        Response response = OkHttpManager.getInstance().syncResponse(url, start, end);
        Log.i(TAG, "fileName=" + name + " 每個執行緒負責下載檔案大小contentLength=" + response.body().contentLength()
                + " 開始位置start=" + start + "結束位置end=" + end + " threadId=" + threadId);
        inputStream = response.body().byteStream();
        //儲存檔案的路徑
        File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), name);
        randomAccessFile = new RandomAccessFile(file, "rwd");
        //seek從哪裡開始
        randomAccessFile.seek(start);
        int length;
        byte[] bytes = new byte[10 * 1024];
        while ((length = inputStream.read(bytes)) != -1) {
            if (mStatus == STATUS_STOP)
                break;
            //寫入
            randomAccessFile.write(bytes, 0, length);
            //儲存下進度,做斷點 todo
            mProgress = mProgress + length;
            //實時去更新下進度條,將每次寫入的length傳出去
            downloadCallback.onProgress(length, mCurrentLength);
        }
        downloadCallback.onSuccess(file);
    } catch (IOException e) {
        e.printStackTrace();
        downloadCallback.onFailure(e);
    } finally {
        Utils.close(inputStream);
        Utils.close(randomAccessFile);
        //儲存到資料庫 怎麼存?? todo
    }
}

public void stop() {
    mStatus = STATUS_STOP;
}
}

DownloadRunnable負責每個執行緒的執行對應的任務,使用RandomAccessFile寫入檔案。最後看一張截圖

這裡寫圖片描述

哈,看到最後斷點下載並沒有實現,這個不急,小編還沒寫,但是實現多執行緒下載檔案整體的思路和程式碼都已經出來了,至於斷點怎麼弄,其實具體的思路,在程式碼註解也已經寫出來了,主要是DownloadRunnable 這個類,向資料庫儲存下檔案的下載路徑url,檔案的大小currentLength,每個執行緒id,對應的每個執行緒的start位置和結束位置,以及每個執行緒的下載進度progress。使用者下次進來可以讀取資料庫的內容,有網的情況下重新發請請求,對應Range: bytes=start-end;

整個專案貼上了部分主要的程式碼。

專案完整程式碼https://github.com/StevenYan88/MultiThreadDownload

如果小編的文章對你有幫助,或者喜歡小編的技術文章,可以關注下公眾號,大家互相學習,共同進步!

這裡寫圖片描述