okhttp下載文件斷點續傳(轉)實測有用

下,得力於谷歌官方強烈推薦,加上自身優秀特性,OkHttp成了當前最火的HTTP框架之一。如今公司的項目我也全都換成了基於OkHttp+Gson底層網絡訪問和解析平臺。java

        最近項目須要使用到斷點下載功能,筆者比較喜歡折騰,千方百計拋棄SharedPreferences,尤爲是sqlite做記錄輔助,改用臨時記錄文件的形式記錄下載進度,本文以斷點下載爲例。先看看demo運行效果圖:android

     

        斷點續傳:記錄上次上傳(下載)節點位置,下次接着該位置繼續上傳(下載)。多線程斷點續傳下載則是根據目標下載文件長度,儘量地等分給多個線程同時下載文件塊,當各個線程所有完成下載後,將文件塊合併成一個文件,即目標文件。多線程斷點續傳不只爲用戶避免了斷網等突發事故須要從新下載浪費流量的尷尬局面,也大大提升了下載速率,固然,不是線程越多越好,網絡帶寬纔是硬道理!如下爲原理圖:sql

   

          java,android中可使用RandomAccessFile類生成一個同目標文件大小的佔位文件,以便於各個線程能夠同時操做該文件,並寫入各線程實時下載的數據。緩存

          下面貼出OkHttp實現的單個多線程下載任務類的DownloadTask.java文件:服務器

[java]view plaincopy網絡

  • package cn.icheny.download;  
  • import android.os.Handler;  
  • import android.os.Message;  
  • import java.io.Closeable;  
  • import java.io.File;  
  • import java.io.IOException;  
  • import java.io.InputStream;  
  • import java.io.RandomAccessFile;  
  • import okhttp3.Call;  
  • import okhttp3.Response;  
  • /**
  • * 多線程下載任務
  • * Created by Cheny on 2017/05/03.
  • */  
  • public class DownloadTask extends Handler {  
  •     private final int THREAD_COUNT = 4;//下載線程數量  
  •     private FilePoint mPoint;  
  •     private long mFileLength;//文件大小  
  •     private boolean isDownloading = false;//是否正在下載  
  •     private int childCanleCount;//子線程取消數量  
  •     private int childPauseCount;//子線程暫停數量  
  •     private int childFinishCount;//子線程完成下載數量  
  •     private HttpUtil mHttpUtil;//http網絡通訊工具  
  •     private long[] mProgress;//各個子線程下載進度集合  
  •     private File[] mCacheFiles;//各個子線程下載緩存數據文件  
  •     private File mTmpFile;//臨時佔位文件  
  •     private boolean pause;//是否暫停  
  •     private boolean cancel;//是否取消下載  
  •     private final int MSG_PROGRESS = 1;//進度  
  •     private final int MSG_FINISH = 2;//完成下載  
  •     private final int MSG_PAUSE = 3;//暫停  
  •     private final int MSG_CANCEL = 4;//暫停  
  •     private DownloadListner mListner;//下載回調監聽  
  •     DownloadTask(FilePoint point, DownloadListner l) {  
  •         this.mPoint = point;  
  •         this.mListner = l;  
  •         this.mProgress = new long[THREAD_COUNT];  
  •         this.mCacheFiles = new File[THREAD_COUNT];  
  •         this.mHttpUtil = HttpUtil.getInstance();  
  •     }  
  •     /**
  •      * 開始下載
  •      */  
  •     public synchronized void start() {  
  •         try {  
  •             if (isDownloading) return;  
  •             isDownloading = true;  
  •             mHttpUtil.getContentLength(mPoint.getUrl(), new okhttp3.Callback() {  
  •                 @Override  
  •                 public void onResponse(Call call, Response response) throws IOException {  
  •                     if (response.code() != 200) {  
  •                         close(response.body());  
  •                         resetStutus();  
  •                         return;  
  •                     }  
  •                     // 獲取資源大小  
  •                     mFileLength = response.body().contentLength();  
  •                     close(response.body());  
  •                     // 在本地建立一個與資源一樣大小的文件來佔位  
  •                     mTmpFile = new File(mPoint.getFilePath(), mPoint.getFileName() + ".tmp");  
  •                     if (!mTmpFile.getParentFile().exists()) mTmpFile.getParentFile().mkdirs();  
  •                     RandomAccessFile tmpAccessFile = new RandomAccessFile(mTmpFile, "rw");  
  •                     tmpAccessFile.setLength(mFileLength);  
  •                     /*將下載任務分配給每一個線程*/  
  •                     long blockSize = mFileLength / THREAD_COUNT;// 計算每一個線程理論上下載的數量.  
  •                     /*爲每一個線程配置並分配任務*/  
  •                     for (int threadId = 0; threadId < THREAD_COUNT; threadId++) {  
  •                         long startIndex = threadId * blockSize; // 線程開始下載的位置  
  •                         long endIndex = (threadId + 1) * blockSize - 1; // 線程結束下載的位置  
  •                         if (threadId == (THREAD_COUNT - 1)) { // 若是是最後一個線程,將剩下的文件所有交給這個線程完成  
  •                             endIndex = mFileLength - 1;  
  •                         }  
  •                         download(startIndex, endIndex, threadId);// 開啓線程下載  
  •                     }  
  •                 }  
  •                 @Override  
  •                 public void onFailure(Call call, IOException e) {  
  •                     resetStutus();  
  •                 }  
  •             });  
  •         } catch (IOException e) {  
  •             e.printStackTrace();  
  •             resetStutus();  
  •         }  
  •     }  
  •     /**
  •      * 下載
  •      * @param startIndex 下載起始位置
  •      * @param endIndex  下載結束位置
  •      * @param threadId 線程id
  •      * @throws IOException
  •      */  
  •     public void download(final long startIndex, final long endIndex, final int threadId) throws IOException {  
  •         long newStartIndex = startIndex;  
  •         // 分段請求網絡鏈接,分段將文件保存到本地.  
  •         // 加載下載位置緩存數據文件  
  •         final File cacheFile = new File(mPoint.getFilePath(), "thread" + threadId + "_" + mPoint.getFileName() + ".cache");  
  •         mCacheFiles[threadId] = cacheFile;  
  •         final RandomAccessFile cacheAccessFile = new RandomAccessFile(cacheFile, "rwd");  
  •         if (cacheFile.exists()) {// 若是文件存在  
  •             String startIndexStr = cacheAccessFile.readLine();  
  •             try {  
  •                 newStartIndex = Integer.parseInt(startIndexStr);//從新設置下載起點  
  •             } catch (NumberFormatException e) {  
  •                 e.printStackTrace();  
  •             }  
  •         }  
  •         final long finalStartIndex = newStartIndex;  
  •         mHttpUtil.downloadFileByRange(mPoint.getUrl(), finalStartIndex, endIndex, new okhttp3.Callback() {  
  •             @Override  
  •             public void onResponse(Call call, Response response) throws IOException {  
  •                 if (response.code() != 206) {// 206:請求部分資源成功碼,表示服務器支持斷點續傳  
  •                     resetStutus();  
  •                     return;  
  •                 }  
  •                 InputStream is = response.body().byteStream();// 獲取流  
  •                 RandomAccessFile tmpAccessFile = new RandomAccessFile(mTmpFile, "rw");// 獲取前面已建立的文件.  
  •                 tmpAccessFile.seek(finalStartIndex);// 文件寫入的開始位置.  
  •                   /*  將網絡流中的文件寫入本地*/  
  •                 byte[] buffer = new byte[1024 << 2];  
  •                 int length = -1;  
  •                 int total = 0;// 記錄本次下載文件的大小  
  •                 long progress = 0;  
  •                 while ((length = is.read(buffer)) > 0) {//讀取流  
  •                     if (cancel) {  
  •                         close(cacheAccessFile, is, response.body());//關閉資源  
  •                         cleanFile(cacheFile);//刪除對應緩存文件  
  •                         sendMessage(MSG_CANCEL);  
  •                         return;  
  •                     }  
  •                     if (pause) {  
  •                         //關閉資源  
  •                         close(cacheAccessFile, is, response.body());  
  •                         //發送暫停消息  
  •                         sendMessage(MSG_PAUSE);  
  •                         return;  
  •                     }  
  •                     tmpAccessFile.write(buffer, 0, length);  
  •                     total += length;  
  •                     progress = finalStartIndex + total;  
  •                     //將該線程最新完成下載的位置記錄並保存到緩存數據文件中  
  •                     //建議轉成Base64碼,防止數據被修改,致使下載文件出錯(若真有這樣的狀況,這樣的朋友可真是無聊透頂啊)  
  •                     cacheAccessFile.seek(0);  
  •                     cacheAccessFile.write((progress + "").getBytes("UTF-8"));  
  •                     //發送進度消息  
  •                     mProgress[threadId] = progress - startIndex;  
  •                     sendMessage(MSG_PROGRESS);  
  •                 }  
  •                 //關閉資源  
  •                 close(cacheAccessFile, is, response.body());  
  •                 // 刪除臨時文件  
  •                 cleanFile(cacheFile);  
  •                 //發送完成消息  
  •                 sendMessage(MSG_FINISH);  
  •             }  
  •             @Override  
  •             public void onFailure(Call call, IOException e) {  
  •                 isDownloading = false;  
  •             }  
  •         });  
  •     }  
  •     /**
  •      * 輪迴消息回調
  •      *
  •      * @param msg
  •      */  
  •     @Override  
  •     public void handleMessage(Message msg) {  
  •         super.handleMessage(msg);  
  •         if (null == mListner) {  
  •             return;  
  •         }  
  •         switch (msg.what) {  
  •             case MSG_PROGRESS://進度  
  •                 long progress = 0;  
  •                 for (int i = 0, length = mProgress.length; i < length; i++) {  
  •                     progress += mProgress_;  _
  •                 }  
  •                 mListner.onProgress(progress * 1.0f / mFileLength);  
  •                 break;  
  •             case MSG_PAUSE://暫停  
  •                 childPauseCount++;  
  •                 if (childPauseCount % THREAD_COUNT != 0) return;//等待全部的線程完成暫停,真正意義的暫停,如下同理  
  •                 resetStutus();  
  •                 mListner.onPause();  
  •                 break;  
  •             case MSG_FINISH://完成  
  •                 childFinishCount++;  
  •                 if (childFinishCount % THREAD_COUNT != 0) return;  
  •                 mTmpFile.renameTo(new File(mPoint.getFilePath(), mPoint.getFileName()));//下載完畢後,重命名目標文件名  
  •                 resetStutus();  
  •                 mListner.onFinished();  
  •                 break;  
  •             case MSG_CANCEL://取消  
  •                 childCanleCount++;  
  •                 if (childCanleCount % THREAD_COUNT != 0) return;  
  •                 resetStutus();  
  •                 mProgress = new long[THREAD_COUNT];  
  •                 mListner.onCancel();  
  •                 break;  
  •         }  
  •     }  
  •     /**
  •      * 發送消息到輪迴器
  •      *
  •      * @param what
  •      */  
  •     private void sendMessage(int what) {  
  •         //發送暫停消息  
  •         Message message = new Message();  
  •         message.what = what;  
  •         sendMessage(message);  
  •     }  
  •     /**
  •      * 關閉資源
  •      *
  •      * @param closeables
  •      */  
  •     private void close(Closeable... closeables) {  
  •         int length = closeables.length;  
  •         try {  
  •             for (int i = 0; i < length; i++) {  
  •                 Closeable closeable = closeables_;  _
  •                 if (null != closeable)  
  •                     closeables_.close();  _
  •             }  
  •         } catch (IOException e) {  
  •             e.printStackTrace();  
  •         } finally {  
  •             for (int i = 0; i < length; i++) {  
  •                 closeables_= null;  _
  •             }  
  •         }  
  •     }  
  •     /**
  •      * 暫停
  •      */  
  •     public void pause() {  
  •         pause = true;  
  •     }  
  •     /**
  •      * 取消
  •      */  
  •     public void cancel() {  
  •         cancel = true;  
  •         cleanFile(mTmpFile);  
  •         if (!isDownloading) {//針對非下載狀態的取消,如暫停  
  •             if (null != mListner) {  
  •                 cleanFile(mCacheFiles);  
  •                 resetStutus();  
  •                 mListner.onCancel();  
  •             }  
  •         }  
  •     }  
  •     /**
  •      * 重置下載狀態
  •      */  
  •     private void resetStutus() {  
  •         pause = false;  
  •         cancel = false;  
  •         isDownloading = false;  
  •     }  
  •     /**
  •      * 刪除臨時文件
  •      */  
  •     private void cleanFile(File... files) {  
  •         for (int i = 0, length = files.length; i < length; i++) {  
  •             if (null != files_)  _
  •                 files_.delete();  _
  •         }  
  •     }  
  •     /**
  •      * 獲取下載狀態
  •      * @return boolean
  •      */  
  •     public boolean isDownloading() {  
  •         return isDownloading;  
  •     }  
  • }  

           先網絡請求獲取文件的長度mFileLength,根據長度藉助RandomAccessFile類在本地生成相同長度的佔位文件mTmpFile,再根據線程數量THREAD_COUNT拆分下載任務,最後for循環出THREAD_COUNT數量的異步請求下載拆份內容(字節)並從mTmpFile的對應位置寫入mTmpFile,每一個線程(任務)每寫入必定的數據後將任務的下載進度寫入經過RandomAccessFile生成的對應任務的記錄緩存文件中,以便於下次下載讀取該線程已下載的進度。註釋比較多,好像也沒啥好解釋的,有問題的朋友下方留言。多線程

          在貼上由OkHttp簡單封裝的網絡請求工具類HttpUtil的.java文件:框架

[java]view plaincopydom

  • package cn.icheny.download;  
  • import java.io.IOException;  
  • import java.util.concurrent.TimeUnit;  
  • import okhttp3.Call;  
  • import okhttp3.Callback;  
  • import okhttp3.OkHttpClient;  
  • import okhttp3.Request;  
  • import okhttp3.Response;  
  • /**
  • * Http網絡工具,基於OkHttp
  • * Created by Cheny on 2017/05/03.
  • */  
  • public class HttpUtil {  
  •     private OkHttpClient mOkHttpClient;  
  •     private static HttpUtil mInstance;  
  •     private final static long CONNECT_TIMEOUT = 60;//超時時間,秒  
  •     private final static long READ_TIMEOUT = 60;//讀取時間,秒  
  •     private final static long WRITE_TIMEOUT = 60;//寫入時間,秒  
  •     /**
  •      * @param url        下載連接
  •      * @param startIndex 下載起始位置
  •      * @param endIndex   結束爲止
  •      * @param callback   回調
  •      * @throws IOException
  •      */  
  •     public void downloadFileByRange(String url, long startIndex, long endIndex, Callback callback) throws IOException {  
  •         // 建立一個Request  
  •         // 設置分段下載的頭信息。 Range:作分段數據請求,斷點續傳指示下載的區間。格式: Range bytes=0-1024或者bytes:0-1024  
  •         Request request = new Request.Builder().header("RANGE", "bytes=" + startIndex + "-" + endIndex)  
  •                 .url(url)  
  •                 .build();  
  •         doAsync(request, callback);  
  •     }  
  •     public void getContentLength(String url, Callback callback) throws IOException {  
  •         // 建立一個Request  
  •         Request request = new Request.Builder()  
  •                 .url(url)  
  •                 .build();  
  •         doAsync(request, callback);  
  •     }  
  •     /**
  •      * 同步GET請求
  •      */  
  •     public void doGetSync(String url) throws IOException {  
  •         //建立一個Request  
  •         Request request = new Request.Builder()  
  •                 .url(url)  
  •                 .build();  
  •         doSync(request);  
  •     }  
  •     /**
  •      * 異步請求
  •      */  
  •     private void doAsync(Request request, Callback callback) throws IOException {  
  •         //建立請求會話  
  •         Call call = mOkHttpClient.newCall(request);  
  •         //同步執行會話請求  
  •         call.enqueue(callback);  
  •     }  
  •     /**
  •      * 同步請求
  •      */  
  •     private Response doSync(Request request) throws IOException {  
  •         //建立請求會話  
  •         Call call = mOkHttpClient.newCall(request);  
  •         //同步執行會話請求  
  •         return call.execute();  
  •     }  
  •     /**
  •      * @return HttpUtil實例對象
  •      */  
  •     public static HttpUtil getInstance() {  
  •         if (null == mInstance) {  
  •             synchronized (HttpUtil.class) {  
  •                 if (null == mInstance) {  
  •                     mInstance = new HttpUtil();  
  •                 }  
  •             }  
  •         }  
  •         return mInstance;  
  •     }  
  •     /**
  •      * 構造方法,配置OkHttpClient
  •      */  
  •     private HttpUtil() {  
  •         //建立okHttpClient對象  
  •         OkHttpClient.Builder builder = new OkHttpClient.Builder()  
  •                 .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)  
  •                 .writeTimeout(READ_TIMEOUT, TimeUnit.SECONDS)  
  •                 .readTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS);  
  •         mOkHttpClient = builder.build();  
  •     }  
  • }  

           header("RANGE", "bytes=" + startIndex + "-" + endIndex),在OkHttp請求頭中添加RANGE(範圍)參數,告訴服務器須要下載文件內容的始末位置。鑑於OkHttp的火熱程度,好像人人都會使用OkHttp,我就不贅言了。異步

         爲了更清晰的教程思路,這裏也貼出FilePoint.java:

[java]view plaincopy

  • package cn.icheny.download;  
  • /**
  • * 目標文件
  • * Created by Cheny on 2017/05/03.
  • */  
  • public class FilePoint {  
  •     private String fileName;//文件名  
  •     private String url;//文件url  
  •     private String filePath;//文件下載路徑  
  •     public FilePoint(String url) {  
  •         this.url = url;  
  •     }  
  •     public FilePoint(String filePath, String url) {  
  •         this.filePath = filePath;  
  •         this.url = url;  
  •     }  
  •     public FilePoint(String url, String filePath, String fileName) {  
  •         this.url = url;  
  •         this.filePath = filePath;  
  •         this.fileName = fileName;  
  •     }  
  •     public String getFileName() {  
  •         return fileName;  
  •     }  
  •     public void setFileName(String fileName) {  
  •         this.fileName = fileName;  
  •     }  
  •     public String getUrl() {  
  •         return url;  
  •     }  
  •     public void setUrl(String url) {  
  •         this.url = url;  
  •     }  
  •     public String getFilePath() {  
  •         return filePath;  
  •     }  
  •     public void setFilePath(String filePath) {  
  •         this.filePath = filePath;  
  •     }  
  • }  

      下面是下載管理器DownloadManager 代碼,統一管理全部文件的下載任務:

[java]view plaincopy

  • package cn.icheny.download;  
  • import android.os.Environment;  
  • import android.text.TextUtils;  
  • import java.io.File;  
  • import java.util.HashMap;  
  • import java.util.Map;  
  • /**
  • * 下載管理器,斷點續傳
  • *
  • * @author Cheny
  • */  
  • public class DownloadManager {  
  •     private String DEFAULT_FILE_DIR;//默認下載目錄  
  •     private Map<String, DownloadTask> mDownloadTasks;//文件下載任務索引,String爲url,用來惟一區別並操做下載的文件  
  •     private static DownloadManager mInstance;  
  •     /**
  •      * 下載文件
  •      */  
  •     public void download(String... urls) {  
  •         for (int i = 0, length = urls.length; i < length; i++) {  
  •             String url = urls_;  _
  •             if (mDownloadTasks.containsKey(url)) {  
  •                 mDownloadTasks.get(url).start();  
  •             }  
  •         }  
  •     }  
  •     /**
  •      * 經過url獲取下載文件的名稱
  •      */  
  •     public String getFileName(String url) {  
  •         return url.substring(url.lastIndexOf("/") + 1);  
  •     }  
  •     /**
  •      * 暫停
  •      */  
  •     public void pause(String... urls) {  
  •         for (int i = 0, length = urls.length; i < length; i++) {  
  •             String url = urls_;  _
  •             if (mDownloadTasks.containsKey(url)) {  
  •                 mDownloadTasks.get(url).pause();  
  •             }  
  •         }  
  •     }  
  •     /**
  •      * 取消下載
  •      */  
  •     public void cancel(String... urls) {  
  •         for (int i = 0, length = urls.length; i < length; i++) {  
  •             String url = urls_;  _
  •             if (mDownloadTasks.containsKey(url)) {  
  •                 mDownloadTasks.get(url).cancel();  
  •             }  
  •         }  
  •     }  
  •     /**
  •      * 添加下載任務
  •      */  
  •     public void add(String url, DownloadListner l) {  
  •         add(url, null, null, l);  
  •     }  
  •     /**
  •      * 添加下載任務
  •      */  
  •     public void add(String url, String filePath, DownloadListner l) {  
  •         add(url, filePath, null, l);  
  •     }  
  •     /**
  •      * 添加下載任務
  •      */  
  •     public void add(String url, String filePath, String fileName, DownloadListner l) {  
  •         if (TextUtils.isEmpty(filePath)) {//沒有指定下載目錄,使用默認目錄  
  •             filePath = getDefaultDirectory();  
  •         }  
  •         if (TextUtils.isEmpty(fileName)) {  
  •             fileName = getFileName(url);  
  •         }  
  •         mDownloadTasks.put(url, new DownloadTask(new FilePoint(url, filePath, fileName), l));  
  •     }  
  •     /**
  •      * 獲取默認下載目錄
  •      *
  •      * @return
  •      */  
  •     private String getDefaultDirectory() {  
  •         if (TextUtils.isEmpty(DEFAULT_FILE_DIR)) {  
  •             DEFAULT_FILE_DIR = Environment.getExternalStorageDirectory().getAbsolutePath()  
  •                     + File.separator + "icheny" + File.separator;  
  •         }  
  •         return DEFAULT_FILE_DIR;  
  •     }  
  •     /**
  •      * 是否正在下載
  •      * @param urls
  •      * @return boolean
  •      */  
  •     public boolean isDownloading(String... urls) {  
  •         boolean result = false;  
  •         for (int i = 0, length = urls.length; i < length; i++) {  
  •             String url = urls_;  _
  •             if (mDownloadTasks.containsKey(url)) {  
  •                 result = mDownloadTasks.get(url).isDownloading();  
  •             }  
  •         }  
  •         return result;  
  •     }  
  •     public static DownloadManager getInstance() {  
  •         if (mInstance == null) {  
  •             synchronized (DownloadManager.class) {  
  •                 if (mInstance == null) {  
  •                     mInstance = new DownloadManager();  
  •                 }  
  •             }  
  •         }  
  •         return mInstance;  
  •     }  
  •     /**
  •      * 初始化下載管理器
  •      */  
  •     private DownloadManager() {  
  •         mDownloadTasks = new HashMap<>();  
  •     }  
  • }  

           下載管理器經過一個Map將下載連接(url,教程圖方便使用url的方式。建議使用其餘惟一標識,畢竟通常url長度都很長,會影響必定性能。另外,考慮一個項目中可能須要下載同一個文件到不一樣的目錄,url作索引顯得生硬)與對應的下載任務( DownloadTask )綁定在一塊兒,以便於根據url判斷或獲取對應的下載任務,進行下載,取消和暫停等操做。

相關文章
相關標籤/搜索