Android實(shí)現(xiàn)下載工具的簡(jiǎn)單代碼
下載應(yīng)該是每個(gè)App都必須的一項(xiàng)功能,不采用第三方框架的話,就需要我們自己去實(shí)現(xiàn)下載工具了。如果我們自己實(shí)現(xiàn)可以怎么做呢?
首先如果服務(wù)器文件支持?jǐn)帱c(diǎn)續(xù)傳,則我們需要實(shí)現(xiàn)的主要功能點(diǎn)如下:
多線程、斷點(diǎn)續(xù)傳下載
下載管理:開(kāi)始、暫停、繼續(xù)、取消、重新開(kāi)始
如果服務(wù)器文件不支持?jǐn)帱c(diǎn)續(xù)傳,則只能進(jìn)行普通的單線程下載,而且不能暫停、繼續(xù)。當(dāng)然一般情況服務(wù)器文件都應(yīng)該支持?jǐn)帱c(diǎn)續(xù)傳吧!
下邊分別是單個(gè)任務(wù)下載、多任務(wù)列表下載、以及service下載的效果圖:
single_task
task_manage
service_task
基本實(shí)現(xiàn)原理:
接下來(lái)看看具體的實(shí)現(xiàn)原理,由于我們的下載是基于okhttp實(shí)現(xiàn)的,首先我們需要一個(gè)OkHttpManager類(lèi),進(jìn)行最基本的網(wǎng)絡(luò)請(qǐng)求封裝:
public class OkHttpManager { ............省略.............. /** * 異步(根據(jù)斷點(diǎn)請(qǐng)求) * * @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; } /** * 同步請(qǐng)求 * * @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(); } /** * 文件存在的情況下可判斷服務(wù)端文件是否已經(jīng)更改 * * @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請(qǐng)求時(shí)初始化證書(shū) * * @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(); } } }
這個(gè)類(lèi)里包含了基本的超時(shí)配置、根據(jù)斷點(diǎn)信息發(fā)起異步請(qǐng)求、校驗(yàn)服務(wù)器文件是否有更新、https證書(shū)配置等。這樣網(wǎng)絡(luò)請(qǐng)求部分就有了。
接下來(lái),我們還需要數(shù)據(jù)庫(kù)的支持,以便記錄下載文件的基本信息,這里我們使用SQLite,只有一張表:
/** * download_info表建表語(yǔ)句 */ 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)";
當(dāng)然還有對(duì)應(yīng)表的增刪改查工具類(lèi),具體的可參考源碼。
由于需要下載管理,所以線程池也是必不可少的,這樣可以避免過(guò)多的創(chuàng)建子線程,達(dá)到復(fù)用的目的,當(dāng)然線程池的大小可以根據(jù)需求進(jìn)行配置,主要代碼如下:
public class ThreadPool { //可同時(shí)下載的任務(wù)數(shù)(核心線程數(shù)) private int CORE_POOL_SIZE = 3; //緩存隊(duì)列的大小(最大線程數(shù)) private int MAX_POOL_SIZE = 20; //非核心線程閑置的超時(shí)時(shí)間(秒),如果超時(shí)則會(huì)被回收 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; } }
接下來(lái)就是我們核心的下載類(lèi)FileTask了,它實(shí)現(xiàn)了Runnable接口,這樣就能在線程池中執(zhí)行,首先看下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()); } }
如果下載的目標(biāo)文件、記錄斷點(diǎn)的臨時(shí)文件、數(shù)據(jù)庫(kù)記錄都存在,則我們先判斷服務(wù)器文件是否有更新,如果沒(méi)有更新則根據(jù)之前的記錄直接開(kāi)始下載,否則需要先進(jìn)行斷點(diǎn)下載前的準(zhǔn)備。如果記錄文件不全部存在則需要先判斷是否支持?jǐn)帱c(diǎn)續(xù)傳,如果支持則按照斷點(diǎn)續(xù)傳的流程進(jìn)行,否則采用普通下載。
首先看下prepareRangeFile()方法,在這里進(jìn)行斷點(diǎn)續(xù)傳的準(zhǔn)備工作:
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 { .............省略............ } }
首先是清除歷史記錄,創(chuàng)建新的目標(biāo)文件和臨時(shí)文件,childTaskCount代表文件需要通過(guò)幾個(gè)子任務(wù)去下載,這樣就可以得到每個(gè)子任務(wù)需要下載的任務(wù)大小,進(jìn)而得到具體的斷點(diǎn)信息并記錄到臨時(shí)文件中。文件下載我們采用MappedByteBuffer 類(lèi),相比RandomAccessFile 更加的高效。同時(shí)執(zhí)行onStart()方法將代表下載的準(zhǔn)備階段,具體細(xì)節(jié)后面會(huì)說(shuō)到。
接下來(lái)看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); } .................省略.............. }
就是根據(jù)臨時(shí)文件保存的斷點(diǎn)信息發(fā)起childTaskCount數(shù)量的異步請(qǐng)求,如果響應(yīng)成功則通過(guò)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循環(huán)中進(jìn)行目前文件的寫(xiě)入和將當(dāng)前下載到的位置保存到臨時(shí)文件:
saveBuffer.put(buffer, 0, len); tempBuffer.putLong(index * EACH_TEMP_SIZE, tempBuffer.getLong(index * EACH_TEMP_SIZE) + len);
同時(shí)調(diào)用onProgress()方法將進(jìn)度發(fā)送出去,其中取消、退出保存記錄、暫停需要中斷while循環(huán)。
因?yàn)橄螺d是在子線程進(jìn)行的,但我們一般需要在UI線程根據(jù)下載狀態(tài)來(lái)更新UI,所以我們通過(guò)Handler將下載過(guò)程的狀態(tài)數(shù)據(jù)發(fā)送到UI線程:即調(diào)用handler.sendEmptyMessage()方法。
最后FileTask類(lèi)還有一個(gè)saveCommonFile()方法,即進(jìn)行不支持?jǐn)帱c(diǎn)續(xù)傳的普通下載。
前邊我們提到了通過(guò)Handler將下載過(guò)程的狀態(tài)數(shù)據(jù)發(fā)送到UI線程,接下看下ProgressHandler類(lèi)基本的處理:
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()方法中,我們根據(jù)當(dāng)前的下載狀態(tài)進(jìn)行相應(yīng)的操作。
如果是START則需要將下載數(shù)據(jù)插入數(shù)據(jù)庫(kù),執(zhí)行初始化回調(diào)等;如果是PROGRESS則執(zhí)行下載進(jìn)度回調(diào);如果是CANCEL則刪除目標(biāo)文件、臨時(shí)文件、數(shù)據(jù)庫(kù)記錄并執(zhí)行對(duì)應(yīng)回調(diào)等;如果是PAUSE則更新數(shù)據(jù)庫(kù)文件記錄并執(zhí)行暫停的回調(diào)等;如果是FINISH則刪除臨時(shí)文件和數(shù)據(jù)庫(kù)記錄并執(zhí)行完成的回調(diào);如果是DESTROY則代表直接在Activity中下載,退出Activity則會(huì)更新數(shù)據(jù)庫(kù)記錄;最后的ERROR則對(duì)應(yīng)出錯(cuò)的情況。具體的細(xì)節(jié)可參考源碼。
最后在DownloadManger類(lèi)里使用線程池執(zhí)行下載操作:
ThreadPool.getInstance().getThreadPoolExecutor().execute(fileTask);
//如果正在下載的任務(wù)數(shù)量等于線程池的核心線程數(shù),則新添加的任務(wù)處于等待狀態(tài) if (ThreadPool.getInstance().getThreadPoolExecutor().getActiveCount() == ThreadPool.getInstance().getCorePoolSize()) { downloadCallback.onWait(); }
以及判斷新添加的任務(wù)是否處于等待的狀態(tài),方便在UI層處理。到這里核心的實(shí)現(xiàn)原理就完了,更多的細(xì)節(jié)可以參考源碼。
如何使用:
DownloadManger是個(gè)單例類(lèi),在這里封裝在了具體的使用操作,我們可以根據(jù)url進(jìn)行下載的開(kāi)始、暫停、繼續(xù)、取消、重新開(kāi)始、線程池配置、https證書(shū)配置、查詢(xún)數(shù)據(jù)的記錄數(shù)據(jù)、獲得當(dāng)前某個(gè)下載狀態(tài)的數(shù)據(jù):
開(kāi)始一個(gè)下載任務(wù)我們可以通過(guò)三種方式來(lái)進(jìn)行:
1、通過(guò)DownloadManager類(lèi)的start(DownloadData downloadData, DownloadCallback downloadCallback)方法,data可以設(shè)置url、保存路徑、文件名、子任務(wù)數(shù)量:
2、先執(zhí)行DownloadManager類(lèi)的setOnDownloadCallback(DownloadData downloadData, DownloadCallback downloadCallback)方法,綁定data和callback,再執(zhí)行start(String url)方法。
3、鏈?zhǔn)秸{(diào)用,需要通過(guò)DUtil類(lèi)來(lái)進(jìn)行:例如
DUtil.init(mContext) .url(url) .path(Environment.getExternalStorageDirectory() + "/DUtil/") .name(name.xxx) .childTaskCount(3) .build() .start(callback);
start()方法會(huì)返回DownloadManager類(lèi)的實(shí)例,如果你不關(guān)心返回值,使用DownloadManger.getInstance(context)同樣可以得到DownloadManager類(lèi)的實(shí)例,以便進(jìn)行后續(xù)的暫停、繼續(xù)、取消等操作。
關(guān)于callback可以使用DownloadCallback接口實(shí)現(xiàn)完整的回調(diào):
new DownloadCallback() { //開(kāi)始 @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() { } //下載出錯(cuò) @Override public void onError(String error) { } }
也可以使用SimpleDownloadCallback接口只實(shí)現(xiàn)需要的回調(diào)方法。
暫停下載中的任務(wù):pause(String url)
繼續(xù)暫停的任務(wù):resume(String url)
ps:不支持?jǐn)帱c(diǎn)續(xù)傳的文件無(wú)法進(jìn)行暫停和繼續(xù)操作。
取消任務(wù):cancel(String url),可以取消下載中、或暫停的任務(wù)。
重新開(kāi)始下載:restart(String url),暫停、下載中、已取消、已完成的任務(wù)均可重新開(kāi)始下載。
下載數(shù)據(jù)保存:destroy(String url)、destroy(String... urls),如在Activity中直接下載,直接退出時(shí)可在onDestroy()方法中調(diào)用,以保存數(shù)據(jù)。
配置線程池:setTaskPoolSize(int corePoolSize, int maxPoolSize),設(shè)置核心線程數(shù)以及總線程數(shù)。
配置okhttp證書(shū):setCertificates(InputStream... certificates)
在數(shù)據(jù)庫(kù)查詢(xún)單個(gè)數(shù)據(jù)DownloadData getDbData(String url),查詢(xún)?nèi)繑?shù)據(jù):List<DownloadData> getAllDbData()
ps:數(shù)據(jù)庫(kù)不保存已下載完成的數(shù)據(jù)
獲得下載隊(duì)列中的某個(gè)文件數(shù)據(jù):DownloadData getCurrentData(String url)
到這里基本的就介紹完了,更多的細(xì)節(jié)和具體的使用都在demo中,不合理的地方還請(qǐng)多多指教哦。
github地址:https://github.com/Othershe/DUtil
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 解決Android SDK下載和更新失敗的方法詳解
- Android zip文件下載和解壓實(shí)例
- Android實(shí)現(xiàn)下載文件功能的方法
- Android中實(shí)現(xiàn)下載和解壓zip文件功能代碼分享
- Android編程實(shí)現(xiàn)應(yīng)用自動(dòng)更新、下載、安裝的方法
- Android使用okHttp(get方式)下載圖片
- Android實(shí)現(xiàn)多線程下載文件的方法
- 使用Android的OkHttp包實(shí)現(xiàn)基于HTTP協(xié)議的文件上傳下載
- Android通過(guò)startService實(shí)現(xiàn)文件批量下載
- 詳解Android使用OKHttp3實(shí)現(xiàn)下載(斷點(diǎn)續(xù)傳、顯示進(jìn)度)
相關(guān)文章
Android?RecyclerLineChart實(shí)現(xiàn)圖表繪制教程
這篇文章主要為大家介紹了Android?RecyclerLineChart實(shí)現(xiàn)圖表繪制教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12Android實(shí)現(xiàn)獲取驗(yàn)證碼倒計(jì)時(shí)功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)獲取驗(yàn)證碼倒計(jì)時(shí)功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12Android自定義半圓形圓盤(pán)滾動(dòng)選擇器
這篇文章主要為大家詳細(xì)介紹了Android自定義半圓形圓盤(pán)滾動(dòng)選擇器 ,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07Android Wifi的forget()操作實(shí)例詳解
這篇文章主要介紹了Android Wifi的forget()操作實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-02-02Android 使用jarsigner給apk簽名的方法詳細(xì)介紹
這篇文章主要介紹了Android 使用jarsigner給apk簽名的方法詳細(xì)介紹的相關(guān)資料,APP 完成需要在一些APP 商店進(jìn)行上傳審核,供用戶(hù)下載使用,APP 需要簽名認(rèn)證,需要的朋友可以參考下2016-12-12Android開(kāi)發(fā)之ImageLoader本地緩存
ImageLoader是一個(gè)圖片緩存的開(kāi)源庫(kù),提供了強(qiáng)大的圖片緩存機(jī)制,很多開(kāi)發(fā)者都在使用,今天給大家介紹Android開(kāi)發(fā)之ImageLoader本地緩存。對(duì)imageloader本地緩存相關(guān)知識(shí)感興趣的朋友一起學(xué)習(xí)吧2016-01-01Android編程之內(nèi)存溢出解決方案(OOM)實(shí)例總結(jié)
這篇文章主要介紹了Android編程之內(nèi)存溢出解決方案(OOM),結(jié)合實(shí)例實(shí)例總結(jié)分析了Android編程過(guò)程中常見(jiàn)的內(nèi)存溢出情況與對(duì)應(yīng)的解決方法,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11基于Android實(shí)現(xiàn)可滾動(dòng)的環(huán)形菜單效果
這篇文章主要為大家詳細(xì)介紹了Android如何使用kotlin實(shí)現(xiàn)可滾動(dòng)的環(huán)形菜單,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03Android畫(huà)中畫(huà)窗口開(kāi)啟方法
Android8.0 Oreo(API Level26)允許活動(dòng)啟動(dòng)畫(huà)中畫(huà)Picture-in-picture(PIP)模式。PIP是一種特殊類(lèi)型的多窗口模式,主要用于視頻播放。PIP模式已經(jīng)可用于Android TV,而Android8.0則讓該功能可進(jìn)一步用于其他Android設(shè)備2023-01-01