您好,登錄后才能下訂單哦!
這篇文章給大家介紹怎么在Android應用添加一個下載工具,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
首先如果服務器文件支持斷點續傳,則我們需要實現的主要功能點如下:
多線程、斷點續傳下載
下載管理:開始、暫停、繼續、取消、重新開始
如果服務器文件不支持斷點續傳,則只能進行普通的單線程下載,而且不能暫停、繼續。當然一般情況服務器文件都應該支持斷點續傳吧!
基本實現原理:
接下來看看具體的實現原理,由于我們的下載是基于okhttp實現的,首先我們需要一個OkHttpManager類,進行最基本的網絡請求封裝:
public class OkHttpManager { ............省略.............. /** * 異步(根據斷點請求) * * @param url * @param start * @param end * @param callback * @return */ public Call initRequest(String url, long start, long end, final Callback callback) { Request request = new Request.Builder() .url(url) .header("Range", "bytes=" + start + "-" + end) .build(); Call call = builder.build().newCall(request); call.enqueue(callback); return call; } /** * 同步請求 * * @param url * @return * @throws IOException */ public Response initRequest(String url) throws IOException { Request request = new Request.Builder() .url(url) .header("Range", "bytes=0-") .build(); return builder.build().newCall(request).execute(); } /** * 文件存在的情況下可判斷服務端文件是否已經更改 * * @param url * @param lastModify * @return * @throws IOException */ public Response initRequest(String url, String lastModify) throws IOException { Request request = new Request.Builder() .url(url) .header("Range", "bytes=0-") .header("If-Range", lastModify) .build(); return builder.build().newCall(request).execute(); } /** * https請求時初始化證書 * * @param certificates * @return */ public void setCertificates(InputStream... certificates) { try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null); int index = 0; for (InputStream certificate : certificates) { String certificateAlias = Integer.toString(index++); keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate)); try { if (certificate != null) certificate.close(); } catch (IOException e) { } } SSLContext sslContext = SSLContext.getInstance("TLS"); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keyStore); sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom()); builder.sslSocketFactory(sslContext.getSocketFactory()); } catch (Exception e) { e.printStackTrace(); } } }
這個類里包含了基本的超時配置、根據斷點信息發起異步請求、校驗服務器文件是否有更新、https證書配置等。這樣網絡請求部分就有了。
接下來,我們還需要數據庫的支持,以便記錄下載文件的基本信息,這里我們使用SQLite,只有一張表:
/** * download_info表建表語句 */ public static final String CREATE_DOWNLOAD_INFO = "create table download_info (" + "id integer primary key autoincrement, " + "url text, " + "path text, " + "name text, " + "child_task_count integer, " + "current_length integer, " + "total_length integer, " + "percentage real, " + "last_modify text, " + "date text)";
當然還有對應表的增刪改查工具類,具體的可參考源碼。
由于需要下載管理,所以線程池也是必不可少的,這樣可以避免過多的創建子線程,達到復用的目的,當然線程池的大小可以根據需求進行配置,主要代碼如下:
public class ThreadPool { //可同時下載的任務數(核心線程數) private int CORE_POOL_SIZE = 3; //緩存隊列的大小(最大線程數) private int MAX_POOL_SIZE = 20; //非核心線程閑置的超時時間(秒),如果超時則會被回收 private long KEEP_ALIVE = 10L; private ThreadPoolExecutor THREAD_POOL_EXECUTOR; private ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(); @Override public Thread newThread(@NonNull Runnable runnable) { return new Thread(runnable, "download_task#" + mCount.getAndIncrement()); } }; ...................省略................ public void setCorePoolSize(int corePoolSize) { if (corePoolSize == 0) { return; } CORE_POOL_SIZE = corePoolSize; } public void setMaxPoolSize(int maxPoolSize) { if (maxPoolSize == 0) { return; } MAX_POOL_SIZE = maxPoolSize; } public int getCorePoolSize() { return CORE_POOL_SIZE; } public int getMaxPoolSize() { return MAX_POOL_SIZE; } public ThreadPoolExecutor getThreadPoolExecutor() { if (THREAD_POOL_EXECUTOR == null) { THREAD_POOL_EXECUTOR = new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(), sThreadFactory); } return THREAD_POOL_EXECUTOR; } }
接下來就是我們核心的下載類FileTask了,它實現了Runnable接口,這樣就能在線程池中執行,首先看下run()方法的邏輯:
@Override public void run() { try { File saveFile = new File(path, name); File tempFile = new File(path, name + ".temp"); DownloadData data = Db.getInstance(context).getData(url); if (Utils.isFileExists(saveFile) && Utils.isFileExists(tempFile) && data != null) { Response response = OkHttpManager.getInstance().initRequest(url, data.getLastModify()); if (response != null && response.isSuccessful() && Utils.isNotServerFileChanged(response)) { TEMP_FILE_TOTAL_SIZE = EACH_TEMP_SIZE * data.getChildTaskCount(); onStart(data.getTotalLength(), data.getCurrentLength(), "", true); } else { prepareRangeFile(response); } saveRangeFile(); } else { Response response = OkHttpManager.getInstance().initRequest(url); if (response != null && response.isSuccessful()) { if (Utils.isSupportRange(response)) { prepareRangeFile(response); saveRangeFile(); } else { saveCommonFile(response); } } } } catch (IOException e) { onError(e.toString()); } }
如果下載的目標文件、記錄斷點的臨時文件、數據庫記錄都存在,則我們先判斷服務器文件是否有更新,如果沒有更新則根據之前的記錄直接開始下載,否則需要先進行斷點下載前的準備。如果記錄文件不全部存在則需要先判斷是否支持斷點續傳,如果支持則按照斷點續傳的流程進行,否則采用普通下載。
首先看下prepareRangeFile()方法,在這里進行斷點續傳的準備工作:
private void prepareRangeFile(Response response) { .................省略................. try { File saveFile = Utils.createFile(path, name); File tempFile = Utils.createFile(path, name + ".temp"); long fileLength = response.body().contentLength(); onStart(fileLength, 0, Utils.getLastModify(response), true); Db.getInstance(context).deleteData(url); Utils.deleteFile(saveFile, tempFile); saveRandomAccessFile = new RandomAccessFile(saveFile, "rws"); saveRandomAccessFile.setLength(fileLength); tempRandomAccessFile = new RandomAccessFile(tempFile, "rws"); tempRandomAccessFile.setLength(TEMP_FILE_TOTAL_SIZE); tempChannel = tempRandomAccessFile.getChannel(); MappedByteBuffer buffer = tempChannel.map(READ_WRITE, 0, TEMP_FILE_TOTAL_SIZE); long start; long end; int eachSize = (int) (fileLength / childTaskCount); for (int i = 0; i < childTaskCount; i++) { if (i == childTaskCount - 1) { start = i * eachSize; end = fileLength - 1; } else { start = i * eachSize; end = (i + 1) * eachSize - 1; } buffer.putLong(start); buffer.putLong(end); } } catch (Exception e) { onError(e.toString()); } finally { .............省略............ } }
首先是清除歷史記錄,創建新的目標文件和臨時文件,childTaskCount代表文件需要通過幾個子任務去下載,這樣就可以得到每個子任務需要下載的任務大小,進而得到具體的斷點信息并記錄到臨時文件中。文件下載我們采用MappedByteBuffer 類,相比RandomAccessFile 更加的高效。同時執行onStart()方法將代表下載的準備階段,具體細節后面會說到。
接下來看saveRangeFile()方法:
private void saveRangeFile() { .................省略.............. for (int i = 0; i < childTaskCount; i++) { final int tempI = i; Call call = OkHttpManager.getInstance().initRequest(url, range.start[i], range.end[i], new Callback() { @Override public void onFailure(Call call, IOException e) { onError(e.toString()); } @Override public void onResponse(Call call, Response response) throws IOException { startSaveRangeFile(response, tempI, range, saveFile, tempFile); } }); callList.add(call); } .................省略.............. }
就是根據臨時文件保存的斷點信息發起childTaskCount數量的異步請求,如果響應成功則通過startSaveRangeFile()方法分段保存文件:
private void startSaveRangeFile(Response response, int index, Ranges range, File saveFile, File tempFile) { .................省略.............. try { saveRandomAccessFile = new RandomAccessFile(saveFile, "rws"); saveChannel = saveRandomAccessFile.getChannel(); MappedByteBuffer saveBuffer = saveChannel.map(READ_WRITE, range.start[index], range.end[index] - range.start[index] + 1); tempRandomAccessFile = new RandomAccessFile(tempFile, "rws"); tempChannel = tempRandomAccessFile.getChannel(); MappedByteBuffer tempBuffer = tempChannel.map(READ_WRITE, 0, TEMP_FILE_TOTAL_SIZE); inputStream = response.body().byteStream(); int len; byte[] buffer = new byte[BUFFER_SIZE]; while ((len = inputStream.read(buffer)) != -1) { //取消 if (IS_CANCEL) { handler.sendEmptyMessage(CANCEL); callList.get(index).cancel(); break; } saveBuffer.put(buffer, 0, len); tempBuffer.putLong(index * EACH_TEMP_SIZE, tempBuffer.getLong(index * EACH_TEMP_SIZE) + len); onProgress(len); //退出保存記錄 if (IS_DESTROY) { handler.sendEmptyMessage(DESTROY); callList.get(index).cancel(); break; } //暫停 if (IS_PAUSE) { handler.sendEmptyMessage(PAUSE); callList.get(index).cancel(); break; } } addCount(); } catch (Exception e) { onError(e.toString()); } finally { .................省略.............. }
在while循環中進行目前文件的寫入和將當前下載到的位置保存到臨時文件:
saveBuffer.put(buffer, 0, len); tempBuffer.putLong(index * EACH_TEMP_SIZE, tempBuffer.getLong(index * EACH_TEMP_SIZE) + len);
同時調用onProgress()方法將進度發送出去,其中取消、退出保存記錄、暫停需要中斷while循環。
因為下載是在子線程進行的,但我們一般需要在UI線程根據下載狀態來更新UI,所以我們通過Handler將下載過程的狀態數據發送到UI線程:即調用handler.sendEmptyMessage()方法。
最后FileTask類還有一個saveCommonFile()方法,即進行不支持斷點續傳的普通下載。
前邊我們提到了通過Handler將下載過程的狀態數據發送到UI線程,接下看下ProgressHandler類基本的處理:
private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (mCurrentState) { case START: break; case PROGRESS: break; case CANCEL: break; case PAUSE: break; case FINISH: break; case DESTROY: break; case ERROR: break; } } };
在handleMessage()方法中,我們根據當前的下載狀態進行相應的操作。
如果是START則需要將下載數據插入數據庫,執行初始化回調等;如果是PROGRESS則執行下載進度回調;如果是CANCEL則刪除目標文件、臨時文件、數據庫記錄并執行對應回調等;如果是PAUSE則更新數據庫文件記錄并執行暫停的回調等;如果是FINISH則刪除臨時文件和數據庫記錄并執行完成的回調;如果是DESTROY則代表直接在Activity中下載,退出Activity則會更新數據庫記錄;最后的ERROR則對應出錯的情況。具體的細節可參考源碼。
最后在DownloadManger類里使用線程池執行下載操作:
ThreadPool.getInstance().getThreadPoolExecutor().execute(fileTask);
//如果正在下載的任務數量等于線程池的核心線程數,則新添加的任務處于等待狀態 if (ThreadPool.getInstance().getThreadPoolExecutor().getActiveCount() == ThreadPool.getInstance().getCorePoolSize()) { downloadCallback.onWait(); }
以及判斷新添加的任務是否處于等待的狀態,方便在UI層處理。到這里核心的實現原理就完了,更多的細節可以參考源碼。
如何使用:
DownloadManger是個單例類,在這里封裝在了具體的使用操作,我們可以根據url進行下載的開始、暫停、繼續、取消、重新開始、線程池配置、https證書配置、查詢數據的記錄數據、獲得當前某個下載狀態的數據:
開始一個下載任務我們可以通過三種方式來進行:
1、通過DownloadManager類的start(DownloadData downloadData, DownloadCallback downloadCallback)方法,data可以設置url、保存路徑、文件名、子任務數量:
2、先執行DownloadManager類的setOnDownloadCallback(DownloadData downloadData, DownloadCallback downloadCallback)方法,綁定data和callback,再執行start(String url)方法。
3、鏈式調用,需要通過DUtil類來進行:例如
DUtil.init(mContext) .url(url) .path(Environment.getExternalStorageDirectory() + "/DUtil/") .name(name.xxx) .childTaskCount(3) .build() .start(callback);
start()方法會返回DownloadManager類的實例,如果你不關心返回值,使用DownloadManger.getInstance(context)同樣可以得到DownloadManager類的實例,以便進行后續的暫停、繼續、取消等操作。
關于callback可以使用DownloadCallback接口實現完整的回調:
new DownloadCallback() { //開始 @Override public void onStart(long currentSize, long totalSize, float progress) { } //下載中 @Override public void onProgress(long currentSize, long totalSize, float progress) { } //暫停 @Override public void onPause() { } //取消 @Override public void onCancel() { } //下載完成 @Override public void onFinish(File file) { } //等待 @Override public void onWait() { } //下載出錯 @Override public void onError(String error) { } }
也可以使用SimpleDownloadCallback接口只實現需要的回調方法。
暫停下載中的任務:pause(String url)
繼續暫停的任務:resume(String url)
ps:不支持斷點續傳的文件無法進行暫停和繼續操作。
取消任務:cancel(String url),可以取消下載中、或暫停的任務。
重新開始下載:restart(String url),暫停、下載中、已取消、已完成的任務均可重新開始下載。
下載數據保存:destroy(String url)、destroy(String... urls),如在Activity中直接下載,直接退出時可在onDestroy()方法中調用,以保存數據。
配置線程池:setTaskPoolSize(int corePoolSize, int maxPoolSize),設置核心線程數以及總線程數。
配置okhttp證書:setCertificates(InputStream... certificates)
在數據庫查詢單個數據DownloadData getDbData(String url),查詢全部數據:List<DownloadData> getAllDbData()
ps:數據庫不保存已下載完成的數據
獲得下載隊列中的某個文件數據:DownloadData getCurrentData(String url)
關于怎么在Android應用添加一個下載工具就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。