1. 程式人生 > >android學習-多檔案下載以及斷點續傳

android學習-多檔案下載以及斷點續傳

首先先感謝丰神,本文源於他的這篇微博http://blog.csdn.net/cfy137000/article/details/54838608,思路很棒,然後自己跟著程式碼擼了一遍,然後為了加深理解就上傳到部落格上來。
首先說下主要都到了什麼開發技術吧,網路請求是使用okhttp,然後涉及多執行緒的部分是使用rxjava,之前自己只是簡單的看了一下rxjava,然後實際運用了下,感覺熟悉了很多;然後還涉及到了lambda表示式以及檔案的流的讀寫,開發要求的jdk的版本的要求是在1.8以上,不然沒辦法支援lambda表示式。
首先是配置gradle,匯入okhttp以及rxjava,//OKHttp
compile 'com.squareup.okhttp3:okhttp:3.6.0'
//RxJava和RxAndroid 用來做執行緒切換的
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile 'io.reactivex.rxjava2:rxjava:2.0.1'


然後,//開啟Java1.8 能夠使用lambda表示式
compileOptions{
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
//為了開啟Java8
jackOptions{
enabled true;
}

然後大概思路捋了下:
1,使用單例模式初始化okhttpclient,同時使用hashmap來存放請求;
2.使用rxjava進行下載的一些操作;
3.點選暫停的時候,通過hashmap移除請求;
4.斷點續傳主要是包括:移除請求,判斷本地是否有下載的檔案,比較本地檔案和下載檔案的大小,重新開始請求;
5.需要提到的一點是我們通過新增請求頭來實現接著之前的進度繼續下載。

為了開發方便新建了一個包括下載資訊的實體類(downloadInfo),一個數據接受(downloadObserver),
還有一個關閉流的工具類(IOUtil),新建了一個application(myApp).

1.跟著思路開始理解程式碼,首先來說的是單例模式的實現,這裡涉及到了一個之前沒有接觸的新東西(AtomicReference),百度了一下,介紹說這是java的原子性引用,在多執行緒的情況下更新物件可以保持一致性;然後這裡用來實現單例

 //獲得一個單例類
    public static DownloadManager getInstance() {
        for
(; ; ) { DownloadManager current = INSTANCE.get(); if (current != null) { return current; } current = new DownloadManager(); if (INSTANCE.compareAndSet(null, current)) { return current; } } } private DownloadManager() { downCalls = new HashMap<>(); mClient = new OkHttpClient.Builder().build(); }

其中的comareAndSet方法的意思是如果此方法的呼叫者和引數一相等,那麼就將引數二的值賦值給呼叫者;然後構造方法私有化。

2.
然後就是下載檔案的方法,先看程式碼:

 /**
     * 開始下載
     *
     * @param url              下載請求的網址
     * @param downLoadObserver 用來回調的介面
     */
    public void download(String url, DownLoadObserver downLoadObserver) {
        Observable.just(url)
                .filter(s -> !downCalls.containsKey(s))//call的map已經有了,就證明正在下載,則這次不下載
                .flatMap(s -> Observable.just(createDownInfo(s)))
                .map(this::getRealFileName)//檢測本地資料夾,生成新的檔名
                .flatMap(downloadInfo -> Observable.create(new DownloadSubscribe(downloadInfo)))//下載
                .observeOn(AndroidSchedulers.mainThread())//在主執行緒回撥
                .subscribeOn(Schedulers.io())//在子執行緒執行
                .subscribe(downLoadObserver);//新增觀察者

    }

引數一是我們下載檔案的url,引數二就是我們的資料接收源,裡面的程式碼很簡單,就是用來更新ui:

public  abstract class DownLoadObserver implements Observer<DownloadInfo> {
    protected Disposable d;//可以用於取消註冊的監聽者
    protected DownloadInfo downloadInfo;
    @Override
    public void onSubscribe(Disposable d) {
        this.d = d;
    }
    @Override
    public void onNext(DownloadInfo downloadInfo) {
        this.downloadInfo = downloadInfo;
    }
    @Override
    public void onError(Throwable e) {
        e.printStackTrace();
    }
}

然後說下我們的downloadfile方法,裡面的(s->)即lambda表示式的形式,這裡的s代表的是url;第一個是過濾器(判斷當前的網路請求下載狀態),關於flatmap操作符的意思可以參考這篇部落格http://blog.csdn.net/johnny901114/article/details/51532776;然後map操作檢測本地資料夾,如果已經存在的話,就更新檔名(在原來檔名的名字後加上(1)方法介紹詳見後面方法介紹);然後再次執行flatmap操作,接著就是rxjava的一系列操作,關於rxjava的操作,不是太熟練,所以這塊現在只是跟著
寫了。

3.取消網路請求的操作,取消網路請求的操作即通過每條url來操作hashmap中的call,執行call的cancel操作。

 public void cancel(String url) {
        Call call = downCalls.get(url);
        if (call != null) {
            call.cancel();//取消
        }
        downCalls.remove(url);
    }

4.根據網址建立downloadInfo;

 /**
     * 建立DownInfo
     *
     * @param url 請求網址
     * @return DownInfo
     */
    private DownloadInfo createDownInfo(String url) {
        DownloadInfo downloadInfo = new DownloadInfo(url);
        long contentLength = getContentLength(url);//獲得檔案大小
        downloadInfo.setTotal(contentLength);
        String fileName = url.substring(url.lastIndexOf("/"));
        downloadInfo.setFileName(fileName);
        return downloadInfo;
    }

5.
獲取下載檔案的大小,進行網路請求,檔案的大小在請求頭中可以得到;

  /**
     * 獲取下載長度
     *
     * @param downloadUrl
     * @return
     */
    private long getContentLength(String downloadUrl) {
        Request request = new Request.Builder()
                .url(downloadUrl)
                .build();
        try {
            Response response = mClient.newCall(request).execute();
            if (response != null && response.isSuccessful()) {
                long contentLength = response.body().contentLength();
                response.close();
                return contentLength == 0 ? DownloadInfo.TOTAL_ERROR : contentLength;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return DownloadInfo.TOTAL_ERROR;
    }

6.獲取本地下載檔案的大小。通過檔名來判斷是否存在同名下載檔案,如果有,將大小賦值給實體類,然後比較已存在檔案的大小和下載檔案的大小;如果等於或者大於的情況,則新增小標,區別於之前下載過的檔案。程式碼如下:

 private DownloadInfo getRealFileName(DownloadInfo downloadInfo) {
        String fileName = downloadInfo.getFileName();
        long downloadLength = 0, contentLength = downloadInfo.getTotal();
        File file = new File(MyApp.sContext.getFilesDir(), fileName);
        if (file.exists()) {
            //找到了檔案,代表已經下載過,則獲取其長度
            downloadLength = file.length();
        }
        //之前下載過,需要重新來一個檔案
        int i = 1;
        while (downloadLength >= contentLength) {
            int dotIndex = fileName.lastIndexOf(".");
            String fileNameOther;
            if (dotIndex == -1) {
                fileNameOther = fileName + "(" + i + ")";
            } else {
                fileNameOther = fileName.substring(0, dotIndex)
                        + "(" + i + ")" + fileName.substring(dotIndex);
            }
            File newFile = new File(MyApp.sContext.getFilesDir(), fileNameOther);
            file = newFile;
            downloadLength = newFile.length();
            i++;
        }
        //設定改變過的檔名/大小
        downloadInfo.setProgress(downloadLength);
        downloadInfo.setFileName(file.getName());
        return downloadInfo;
    }

7.建立DownloadSubscribe,在subscribe中進行斷點續傳,主要就是通過設定
請求頭,將檔案的讀寫範圍跳躍至已下載檔案大小處,然後通過流的形式,將檔案銜接上去:

 private class DownloadSubscribe implements ObservableOnSubscribe<DownloadInfo> {
        private DownloadInfo downloadInfo;

        public DownloadSubscribe(DownloadInfo downloadInfo) {
            this.downloadInfo = downloadInfo;
        }

        @Override
        public void subscribe(ObservableEmitter<DownloadInfo> e) throws Exception {
            String url = downloadInfo.getUrl();
            long downloadLength = downloadInfo.getProgress();//已經下載好的長度
            long contentLength = downloadInfo.getTotal();//檔案的總長度
            //初始進度資訊
            e.onNext(downloadInfo);

            Request request = new Request.Builder()
                    //確定下載的範圍,新增此頭,則伺服器就可以跳過已經下載好的部分
                    .addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength)
                    .url(url)
                    .build();
            Call call = mClient.newCall(request);
            downCalls.put(url, call);//把這個新增到call裡,方便取消
            Response response = call.execute();

新建請求,然後新增請求頭,同時,將本次請求也加入到hashmap中,方便取消;通過 call.execute()拿到我們的返回結果。

然後,檔案的io流將檔案寫入到已下載檔案中;

  File file = new File(MyApp.sContext.getFilesDir(), downloadInfo.getFileName());
            InputStream is = null;
            FileOutputStream fileOutputStream = null;
            try {
                is = response.body().byteStream();
                fileOutputStream = new FileOutputStream(file, true);
                byte[] buffer = new byte[2048];//緩衝陣列2kB
                int len;
                while ((len = is.read(buffer)) != -1) {
                    fileOutputStream.write(buffer, 0, len);
                    downloadLength += len;
                    downloadInfo.setProgress(downloadLength);
                    e.onNext(downloadInfo);
                }
                fileOutputStream.flush();
                downCalls.remove(url);
            } finally {
                //關閉IO流
                IOUtil.closeAll(is, fileOutputStream);

            }
            e.onComplete();//完成
        }

至此,就完成了多個檔案的同時下載以及暫停和斷點續傳,如果要實現取消的話,就是將我們的進度條清零,然後刪除本次的已下載檔案。