做爲國內第一個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開銷也不小,使用多線程的意義並非很大。