一個來自Afinal斷點下載BUG的解決方案

    做爲國內第一個Android開發框架Afinal,相信有不少開發者都知道的。雖然隨着Android版本的迭代,其中有一些方法有了更好的解決辦法但歷來沒有人懷疑Afinal的價值。java

    最近在作一個斷點下載的功能,參考了比較多的例子,無心間發現了FinalHttp.download()方法中的一個BUG。android

    首先跟你們介紹一下afinal中download下載的實現原理。與其餘衆多下載方法不一樣,afinal使用的是一個單線程斷點下載,且其中沒有數據庫或額外的文件操做。那麼是如何實現斷點續傳的呢,主要是使用了FileOutputStream的一個構造方法查看api文檔看到git

參數append能夠在一個文件的結尾處續寫數據,這樣就實現了斷點續傳功能。數據庫

知道了實現原理,咱們來看代碼(參數名略有改動)你能夠在這裏看到完整的代碼api

public Object handleEntity(HttpEntity entity, EntityCallBack callback,
            String target, boolean isResume) throws IOException {
        if (TextUtils.isEmpty(target) || target.trim().length() == 0)
            return null;
        File targetFile = new File(target);
        if (!targetFile.exists())
            targetFile.createNewFile();
        if (mStop) {
            return targetFile;
        }

        long current = 0;
        FileOutputStream os = null;
        if (isResume) {
            current = targetFile.length();
            os = new FileOutputStream(target, true);
        } else {
            os = new FileOutputStream(target);
        }
        if (mStop) {
            return targetFile;
        }

        InputStream input = entity.getContent();
        long count = entity.getContentLength() + current;
        if (current >= count || mStop) {
            return targetFile;
        }
        int readLen = 0;
        byte[] buffer = new byte[1024];
        while (!mStop && !(current >= count)
                && ((readLen = input.read(buffer, 0, 1024)) > 0)) {// 未所有讀取
            os.write(buffer, 0, readLen);
            current += readLen;
            callback.callBack(count, current, false);
        }
        callback.callBack(count, current, true);
        return targetFile;
    }

    根據代碼,咱們能夠看到一個明顯的問題——流沒有關閉。這個問題好改,本身發現了關閉就行我就很少解釋了。還有一個問題,就是這一句多線程

long count = entity.getContentLength() + current;app

        遠端文件的大小是被不斷增大的,下載任務每被暫停一次,遠端文件的大小就被增大一次,增長的大小等於本地已下載的碎片文件的大小。這麼作的後果有兩個:一、這個斷點下載功能徹底沒有使用,每次下載都是從0開始。二、本地文件因爲是續傳,愈來愈大。框架

        舉個例子,遠端文件大小是1024 b,本地已經讀取了512b的大小,而此時用戶暫停下載。下次從新下載時,程序從新讀取遠端文件大小,變成了1024+512大小,而本地文件是512,input.read再次從0開始讀取,而本地文件從512開始寫,以後下載完成。兩次下載共下載了512+1024個字節,本地文件因爲是續傳,第一次下載512碎片文件,而第二次又下載完整的1024文件,最後下載變成了1536個字節大小的文件。(順便一說,這個文件是能夠正常打開的,可是打開的速度會比源文件慢)dom

        找到了問題,解決辦法有了嗎,單純的將 count = entity.getContentLength() + current;改爲count = entity.getContentLength() ;而後while循環裏面的current<count去掉就好了?你能夠嘗試一下,這樣是不行的。首先這樣依舊不能達到斷點續傳的目的,只要在續傳的時候spa

InputStream input = entity.getContent();

這一句獲取的流沒有跳過已經下載的部分,就達不到節省流量續傳下載的目的,我想這一點應該能夠理解吧。

OK,那麼InputStream有一個skip方法,跳過已下載的部分總能夠吧,NO,也不行,由於負責續傳寫入文件的FileOutPutStream中有一部分數據是損壞的,不能被使用,而這時你跳過的字節數也就不是真正須要跳過的字節數了。

        繼續,看代碼,這裏是個人解決辦法:

public File handleEntity(HttpEntity entity, DownloadProgress callback,
            File save, boolean isResume) throws IOException {
        long current = 0;
        RandomAccessFile file = new RandomAccessFile(save, "rw");
        if (isResume) {
            current = file.length();
        }
        InputStream input = entity.getContent();
        long count = entity.getContentLength();
        if (mStop) {
            FileUtils.closeIO(file);
            return save;
        }
        
        current = input.skip(current);  
        file.seek(current); 

        int readLen = 0;
        byte[] buffer = new byte[1024];
        while ((readLen = input.read(buffer, 0, 1024)) != -1) {
            if (mStop) {
                break;
            } else {
                file.write(buffer, 0, readLen);
                current += readLen;
                callback.onProgress(count, current);
            }
        }
        callback.onProgress(count, current);

        if (mStop && current < count) { // 用戶主動中止
            FileUtils.closeIO(file);
            throw new IOException("user stop download thread");
        }
        FileUtils.closeIO(file);
        return save;
    }

        能夠看到使用一個支持對隨機訪問文件的讀取和寫入的RandomAccessFile類來替換能夠續傳的FileOutPutStream,同時經過對current從新賦值

 current = input.skip(current);

完美解決了碎片文件中不可用部分形成的文件損壞問題。

        以上方法其實仍是有一個小問題,在Android中咱們都知道,下載的時候通常都會伴隨着一個進度條展現。這個小問題就是當暫停後繼續下載的時候,因爲暫停前那一段不可用的損壞的文件佔用了大小,可能會在恢復下載的時候發生進度條大幅反彈的現象,這對用戶體驗是很糟糕的,畢竟誰想我辛辛苦苦等了半天的進度讀條,又一會兒退回去那麼多。

        那麼解決辦法實際上是給個心理安慰,看以下代碼

public File handleEntity(HttpEntity entity, DownloadProgress callback,
            File save, boolean isResume) throws IOException {
        long current = 0;
        RandomAccessFile file = new RandomAccessFile(save, "rw");
        if (isResume) {
            current = file.length();
        }
        InputStream input = entity.getContent();
        long count = entity.getContentLength() + current;
        if (mStop) {
            FileUtils.closeIO(file);
            return save;
        }
        // 在這裏其實這樣寫是不對的,之因此如此是爲了用戶體驗,誰都不想本身下載時進度條都走了一大半了,就由於一個暫停一會兒少了一大串
        /**
         * 這裏實際的寫法應該是: <br>
         * current = input.skip(current); <br>
         * file.seek(current); <br>
         * 根據JDK文檔中的解釋:Inputstream.skip(long i)方法跳過i個字節,並返回實際跳過的字節數。<br>
         * 致使這種狀況的緣由不少,跳過 n 個字節以前已到達文件末尾只是其中一種可能。這裏我猜想多是碎片文件的損害形成的。
         */
        file.seek(input.skip(current));

        int readLen = 0;
        byte[] buffer = new byte[1024];

        while ((readLen = input.read(buffer, 0, 1024)) != -1) {
            if (mStop) {
                break;
            } else {
                file.write(buffer, 0, readLen);
                current += readLen;
                callback.onProgress(count, current);
            }
        }
        callback.onProgress(count, current);

        if (mStop && current < count) { // 用戶主動中止
            FileUtils.closeIO(file);
            throw new IOException("user stop download thread");
        }
        FileUtils.closeIO(file);
        return save;
    }

    這裏順帶提一下,android的下載我我的並不提倡使用多線程。主要是由於手機通常不會下載多麼大的文件,而多線程自己的線程開銷加上使用數據庫或額外的記錄文件產生的IO開銷也不小,使用多線程的意義並非很大。

相關文章
相關標籤/搜索