Android實現(xiàn)下載工具的簡單代碼
下載應該是每個App都必須的一項功能,不采用第三方框架的話,就需要我們自己去實現(xiàn)下載工具了。如果我們自己實現(xiàn)可以怎么做呢?
首先如果服務器文件支持斷點續(xù)傳,則我們需要實現(xiàn)的主要功能點如下:
多線程、斷點續(xù)傳下載
下載管理:開始、暫停、繼續(xù)、取消、重新開始
如果服務器文件不支持斷點續(xù)傳,則只能進行普通的單線程下載,而且不能暫停、繼續(xù)。當然一般情況服務器文件都應該支持斷點續(xù)傳吧!
下邊分別是單個任務下載、多任務列表下載、以及service下載的效果圖:

single_task

task_manage

service_task
基本實現(xiàn)原理:
接下來看看具體的實現(xiàn)原理,由于我們的下載是基于okhttp實現(xiàn)的,首先我們需要一個OkHttpManager類,進行最基本的網(wǎng)絡請求封裝:
public class OkHttpManager {
............省略..............
/**
* 異步(根據(jù)斷點請求)
*
* @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();
}
}
}
這個類里包含了基本的超時配置、根據(jù)斷點信息發(fā)起異步請求、校驗服務器文件是否有更新、https證書配置等。這樣網(wǎng)絡請求部分就有了。
接下來,我們還需要數(shù)據(jù)庫的支持,以便記錄下載文件的基本信息,這里我們使用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)";
當然還有對應表的增刪改查工具類,具體的可參考源碼。
由于需要下載管理,所以線程池也是必不可少的,這樣可以避免過多的創(chuàng)建子線程,達到復用的目的,當然線程池的大小可以根據(jù)需求進行配置,主要代碼如下:
public class ThreadPool {
//可同時下載的任務數(shù)(核心線程數(shù))
private int CORE_POOL_SIZE = 3;
//緩存隊列的大小(最大線程數(shù))
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了,它實現(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());
}
}
如果下載的目標文件、記錄斷點的臨時文件、數(shù)據(jù)庫記錄都存在,則我們先判斷服務器文件是否有更新,如果沒有更新則根據(jù)之前的記錄直接開始下載,否則需要先進行斷點下載前的準備。如果記錄文件不全部存在則需要先判斷是否支持斷點續(xù)傳,如果支持則按照斷點續(xù)傳的流程進行,否則采用普通下載。
首先看下prepareRangeFile()方法,在這里進行斷點續(xù)傳的準備工作:
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)建新的目標文件和臨時文件,childTaskCount代表文件需要通過幾個子任務去下載,這樣就可以得到每個子任務需要下載的任務大小,進而得到具體的斷點信息并記錄到臨時文件中。文件下載我們采用MappedByteBuffer 類,相比RandomAccessFile 更加的高效。同時執(zhí)行onStart()方法將代表下載的準備階段,具體細節(jié)后面會說到。
接下來看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ù)臨時文件保存的斷點信息發(fā)起childTaskCount數(shù)量的異步請求,如果響應成功則通過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)中進行目前文件的寫入和將當前下載到的位置保存到臨時文件:
saveBuffer.put(buffer, 0, len); tempBuffer.putLong(index * EACH_TEMP_SIZE, tempBuffer.getLong(index * EACH_TEMP_SIZE) + len);
同時調用onProgress()方法將進度發(fā)送出去,其中取消、退出保存記錄、暫停需要中斷while循環(huán)。
因為下載是在子線程進行的,但我們一般需要在UI線程根據(jù)下載狀態(tài)來更新UI,所以我們通過Handler將下載過程的狀態(tài)數(shù)據(jù)發(fā)送到UI線程:即調用handler.sendEmptyMessage()方法。
最后FileTask類還有一個saveCommonFile()方法,即進行不支持斷點續(xù)傳的普通下載。
前邊我們提到了通過Handler將下載過程的狀態(tài)數(shù)據(jù)發(fā)送到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()方法中,我們根據(jù)當前的下載狀態(tài)進行相應的操作。
如果是START則需要將下載數(shù)據(jù)插入數(shù)據(jù)庫,執(zhí)行初始化回調等;如果是PROGRESS則執(zhí)行下載進度回調;如果是CANCEL則刪除目標文件、臨時文件、數(shù)據(jù)庫記錄并執(zhí)行對應回調等;如果是PAUSE則更新數(shù)據(jù)庫文件記錄并執(zhí)行暫停的回調等;如果是FINISH則刪除臨時文件和數(shù)據(jù)庫記錄并執(zhí)行完成的回調;如果是DESTROY則代表直接在Activity中下載,退出Activity則會更新數(shù)據(jù)庫記錄;最后的ERROR則對應出錯的情況。具體的細節(jié)可參考源碼。
最后在DownloadManger類里使用線程池執(zhí)行下載操作:
ThreadPool.getInstance().getThreadPoolExecutor().execute(fileTask);
//如果正在下載的任務數(shù)量等于線程池的核心線程數(shù),則新添加的任務處于等待狀態(tài)
if (ThreadPool.getInstance().getThreadPoolExecutor().getActiveCount() == ThreadPool.getInstance().getCorePoolSize()) {
downloadCallback.onWait();
}
以及判斷新添加的任務是否處于等待的狀態(tài),方便在UI層處理。到這里核心的實現(xiàn)原理就完了,更多的細節(jié)可以參考源碼。
如何使用:
DownloadManger是個單例類,在這里封裝在了具體的使用操作,我們可以根據(jù)url進行下載的開始、暫停、繼續(xù)、取消、重新開始、線程池配置、https證書配置、查詢數(shù)據(jù)的記錄數(shù)據(jù)、獲得當前某個下載狀態(tài)的數(shù)據(jù):
開始一個下載任務我們可以通過三種方式來進行:
1、通過DownloadManager類的start(DownloadData downloadData, DownloadCallback downloadCallback)方法,data可以設置url、保存路徑、文件名、子任務數(shù)量:
2、先執(zhí)行DownloadManager類的setOnDownloadCallback(DownloadData downloadData, DownloadCallback downloadCallback)方法,綁定data和callback,再執(zhí)行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類的實例,以便進行后續(xù)的暫停、繼續(xù)、取消等操作。
關于callback可以使用DownloadCallback接口實現(xiàn)完整的回調:
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接口只實現(xiàn)需要的回調方法。
暫停下載中的任務:pause(String url)
繼續(xù)暫停的任務:resume(String url)
ps:不支持斷點續(xù)傳的文件無法進行暫停和繼續(xù)操作。
取消任務:cancel(String url),可以取消下載中、或暫停的任務。
重新開始下載:restart(String url),暫停、下載中、已取消、已完成的任務均可重新開始下載。
下載數(shù)據(jù)保存:destroy(String url)、destroy(String... urls),如在Activity中直接下載,直接退出時可在onDestroy()方法中調用,以保存數(shù)據(jù)。
配置線程池:setTaskPoolSize(int corePoolSize, int maxPoolSize),設置核心線程數(shù)以及總線程數(shù)。
配置okhttp證書:setCertificates(InputStream... certificates)
在數(shù)據(jù)庫查詢單個數(shù)據(jù)DownloadData getDbData(String url),查詢全部數(shù)據(jù):List<DownloadData> getAllDbData()
ps:數(shù)據(jù)庫不保存已下載完成的數(shù)據(jù)
獲得下載隊列中的某個文件數(shù)據(jù):DownloadData getCurrentData(String url)
到這里基本的就介紹完了,更多的細節(jié)和具體的使用都在demo中,不合理的地方還請多多指教哦。
github地址:https://github.com/Othershe/DUtil
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
- 解決Android SDK下載和更新失敗的方法詳解
- Android zip文件下載和解壓實例
- Android實現(xiàn)下載文件功能的方法
- Android中實現(xiàn)下載和解壓zip文件功能代碼分享
- Android編程實現(xiàn)應用自動更新、下載、安裝的方法
- Android使用okHttp(get方式)下載圖片
- Android實現(xiàn)多線程下載文件的方法
- 使用Android的OkHttp包實現(xiàn)基于HTTP協(xié)議的文件上傳下載
- Android通過startService實現(xiàn)文件批量下載
- 詳解Android使用OKHttp3實現(xiàn)下載(斷點續(xù)傳、顯示進度)
相關文章
Android?RecyclerLineChart實現(xiàn)圖表繪制教程
這篇文章主要為大家介紹了Android?RecyclerLineChart實現(xiàn)圖表繪制教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12
Android 使用jarsigner給apk簽名的方法詳細介紹
這篇文章主要介紹了Android 使用jarsigner給apk簽名的方法詳細介紹的相關資料,APP 完成需要在一些APP 商店進行上傳審核,供用戶下載使用,APP 需要簽名認證,需要的朋友可以參考下2016-12-12
基于Android實現(xiàn)可滾動的環(huán)形菜單效果
這篇文章主要為大家詳細介紹了Android如何使用kotlin實現(xiàn)可滾動的環(huán)形菜單,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03

