/**
* 作者:Pich
* 原文鏈接:http://me.woblog.cn/
* QQ群:129961195
* Github:https://github.com/lifengsofts
*/
概述
為什么是更好的Android多線程下載框架呢,原因你懂的嵌赠,廣告法嘛词渤!
本篇我們我們就來聊聊多線程下載框架,先聊聊我們框架的特點:
- 多線程
- 多任務(wù)
- 斷點續(xù)傳
- 支持大文件
- 可以自定義下載數(shù)據(jù)庫
- 高度可配置剪芍,像超時時間這類
- 業(yè)務(wù)數(shù)據(jù)和下載數(shù)據(jù)分離
下面我們在說下該框架能實現(xiàn)那些的應(yīng)用場景:
- 該框架可以很方便的下載單個文件,并且顯示各種狀態(tài),包括開始下載,下載中伦籍,下載失敗蓝晒,刪除等狀態(tài)。
- 也可以實現(xiàn)常見的需要下載功能應(yīng)用帖鸦,比如:某某手機助手芝薇,在該應(yīng)用內(nèi)可以說是下載是核心功能,所以對框架的穩(wěn)定性作儿,代碼可靠性洛二,框架擴展性依賴很大,所以該框架真是從這種出發(fā)點而生的。通常這類應(yīng)用的表示形式分三個頁面需要用到下載功能晾嘶,一個列表用來顯示來自業(yè)務(wù)數(shù)據(jù)的列表妓雾,在該列表右邊可以點擊單個條目,或者多選實現(xiàn)下載变擒,點擊每個條目進入詳情君珠,同時還有個一個下載管理寝志,包括大概兩個界面娇斑,正在下載,下載完成的材部,在這幾個界面都需要一個核心的功能就是都可以暫停毫缆,恢復(fù),刪除并且能顯示下載進度乐导。在列表一個最重要的問題就是界面刷新苦丁,如果每次更新都刷新整個列表,那么這將是異常災(zāi)難物臂,而我們這個框架正好解決了該問題旺拉,采用了回調(diào)單個條目并更新該條目的進度和狀態(tài)。
該項目狀態(tài)
該項目的雛形始于14年的公司項目需要用到多線程下載棵磷,但當(dāng)時實現(xiàn)的單線程多任務(wù)斷點續(xù)傳蛾狗,后面不斷完善,在這之間遇到過很多坑仪媒,也對一個下載框架有了更深的認識沉桌,所以在16年又重寫了該框架。
項目的Github地址:https://github.com/lifengsofts/AndroidDownloader
項目的官網(wǎng)地址:http://i.woblog.cn/AndroidDownloader
項目還處于發(fā)展?fàn)顟B(tài)算吩,但已經(jīng)趨于穩(wěn)定留凭,并且有一定的編碼規(guī)范,同時采用了多個開源項目的質(zhì)量控制方案以保證每次代碼提交的可靠性偎巢。
下面上幾張框架Demo的截圖蔼夜,這樣用戶在心中有一個自己的概念,但是推薦各位還是講Demo下載到本地親自压昼,運行一下求冷。
截圖
第一個界面是單獨下載一個文件。
第二個界面是應(yīng)用中最常用的一個界面巢音,該界面來自業(yè)務(wù)數(shù)據(jù)遵倦。
第三個頁面是離線管理中的下載中的界面。
第四個頁面是離線管理中的下載完成的界面官撼。
可以看到他們在每個界面都能暫停下載梧躺,繼續(xù)下載,以及刪除,并且都能拿到進度掠哥,狀態(tài)等信息巩踏。
下面就來看看這么強大的下載框架那該如何來使用呢?
添加權(quán)限
我相信這一步任何一個項目都已經(jīng)添加了续搀,但是還是不得不提一下塞琼。
該框架需要網(wǎng)絡(luò)訪問權(quán)限,如果你是講文件下載到存儲卡禁舷,那相應(yīng)的需要添加存儲卡訪問權(quán)限彪杉。
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
配置Service
因為該框架采用才service中下載一個文件的。這樣做的目的是下載任務(wù)一般都需要在后頭下載牵咙,如果在Activity中來做這類任務(wù)派近,我想任何一個新手都知道這樣不行。
<service android:name="cn.woblog.android.downloader.DownloadService">
<intent-filter>
<action android:name="cn.woblog.android.downloader.DOWNLOAD_SERVICE" />
</intent-filter>
</service>
添加依賴
我們提供了多種集成方式洁桌,比如:gradle,maven,jar渴丸。選擇適合你自己的就行了。
Gradle
在module目錄下面的build.gradle文件中添加如下內(nèi)容:
compile 'cn.woblog.android:downloader:1.0.1'
Maven
或者你使用的Maven依賴管理工具另凌。那道理其實是一樣的谱轨,在pom文件中添加:
<dependency>
<groupId>cn.woblog.android</groupId>
<artifactId>downloader</artifactId>
<version>1.0.0</version>
</dependency>
或者你也可以參考該鏈接使用Snapshots版本。
混淆配置
如果你的項目使用了混淆規(guī)則吠谢,那么一定要加上土童。
-keep public class * implements cn.woblog.android.downloader.db.DownloadDBController
-keep class cn.woblog.android.downloader.domain.** { *; }
創(chuàng)建下載管理器
現(xiàn)在萬事俱備只欠東風(fēng)了,接下來只需要創(chuàng)建一個下載管理器囊卜,該框架所有的操作都是通過該來實現(xiàn)的:
downloadManager = DownloadService.getDownloadManager(context.getApplicationContext());
或者你可以使用更詳細的來配置該框架:
Config config = new Config();
//set database path.
// config.setDatabaseName("/sdcard/a/d.db");
// config.setDownloadDBController(dbController);
//set download quantity at the same time.
config.setDownloadThread(3);
//set each download info thread number
config.setEachDownloadThread(2);
// set connect timeout,unit millisecond
config.setConnectTimeout(10000);
// set read data timeout,unit millisecond
config.setReadTimeout(10000);
downloadManager = DownloadService.getDownloadManager(this.getApplicationContext(), config);
下載一個文件
//create download info set download uri and save path.
final DownloadInfo downloadInfo = new DownloadInfo.Builder().setUrl("http://example.com/a.apk")
.setPath("/sdcard/a.apk")
.build();
//set download callback.
downloadInfo.setDownloadListener(new DownloadListener() {
@Override
public void onStart() {
tv_download_info.setText("Prepare downloading");
}
@Override
public void onWaited() {
tv_download_info.setText("Waiting");
bt_download_button.setText("Pause");
}
@Override
public void onPaused() {
bt_download_button.setText("Continue");
tv_download_info.setText("Paused");
}
@Override
public void onDownloading(long progress, long size) {
tv_download_info
.setText(FileUtil.formatFileSize(progress) + "/" + FileUtil
.formatFileSize(size));
bt_download_button.setText("Pause");
}
@Override
public void onRemoved() {
bt_download_button.setText("Download");
tv_download_info.setText("");
downloadInfo = null;
}
@Override
public void onDownloadSuccess() {
bt_download_button.setText("Delete");
tv_download_info.setText("Download success");
}
@Override
public void onDownloadFailed(DownloadException e) {
e.printStackTrace();
tv_download_info.setText("Download fail:" + e.getMessage());
}
});
//submit download info to download manager.
downloadManager.download(downloadInfo);
下載一個文件時直接創(chuàng)建一個DownloadInfo娜扇,然后設(shè)置下載鏈接和下載路徑。再添加一個監(jiān)聽栅组。就可以提交到下載框架了雀瓢。
通過下載監(jiān)聽器我們可以獲取到很多狀態(tài)。開始下載玉掸,等待中刃麸,暫停完成,下載中司浪,刪除成功泊业,下載成功,下載失敗等狀態(tài)啊易。
在列表控件使用
我們這里演示如何在RecyclerView這類列表控件使用吁伺。當(dāng)然如果你用的是ListView那道理是一樣的。
class ViewHolder extends RecyclerView.ViewHolder {
private final ImageView iv_icon;
private final TextView tv_size;
private final TextView tv_status;
private final ProgressBar pb;
private final TextView tv_name;
private final Button bt_action;
private DownloadInfo downloadInfo;
public ViewHolder(View view) {
super(view);
iv_icon = (ImageView) view.findViewById(R.id.iv_icon);
tv_size = (TextView) view.findViewById(R.id.tv_size);
tv_status = (TextView) view.findViewById(R.id.tv_status);
pb = (ProgressBar) view.findViewById(R.id.pb);
tv_name = (TextView) view.findViewById(R.id.tv_name);
bt_action = (Button) view.findViewById(R.id.bt_action);
}
@SuppressWarnings("unchecked")
public void bindData(final MyDownloadInfo data, int position, final Context context) {
Glide.with(context).load(data.getIcon()).into(iv_icon);
tv_name.setText(data.getName());
// Get download task status
downloadInfo = downloadManager.getDownloadById(data.getUrl().hashCode());
// Set a download listener
if (downloadInfo != null) {
downloadInfo
.setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {
// Call interval about one second
@Override
public void onRefresh() {
if (getUserTag() != null && getUserTag().get() != null) {
ViewHolder viewHolder = (ViewHolder) getUserTag().get();
viewHolder.refresh();
}
}
});
}
refresh();
// Download button
bt_action.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (downloadInfo != null) {
switch (downloadInfo.getStatus()) {
case DownloadInfo.STATUS_NONE:
case DownloadInfo.STATUS_PAUSED:
case DownloadInfo.STATUS_ERROR:
//resume downloadInfo
downloadManager.resume(downloadInfo);
break;
case DownloadInfo.STATUS_DOWNLOADING:
case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
case STATUS_WAIT:
//pause downloadInfo
downloadManager.pause(downloadInfo);
break;
case DownloadInfo.STATUS_COMPLETED:
downloadManager.remove(downloadInfo);
break;
}
} else {
// Create new download task
File d = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "d");
if (!d.exists()) {
d.mkdirs();
}
String path = d.getAbsolutePath().concat("/").concat(data.getName());
downloadInfo = new Builder().setUrl(data.getUrl())
.setPath(path)
.build();
downloadInfo
.setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {
@Override
public void onRefresh() {
if (getUserTag() != null && getUserTag().get() != null) {
ViewHolder viewHolder = (ViewHolder) getUserTag().get();
viewHolder.refresh();
}
}
});
downloadManager.download(downloadInfo);
}
}
});
}
private void refresh() {
if (downloadInfo == null) {
tv_size.setText("");
pb.setProgress(0);
bt_action.setText("Download");
tv_status.setText("not downloadInfo");
} else {
switch (downloadInfo.getStatus()) {
case DownloadInfo.STATUS_NONE:
bt_action.setText("Download");
tv_status.setText("not downloadInfo");
break;
case DownloadInfo.STATUS_PAUSED:
case DownloadInfo.STATUS_ERROR:
bt_action.setText("Continue");
tv_status.setText("paused");
try {
pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
} catch (Exception e) {
e.printStackTrace();
}
tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
.formatFileSize(downloadInfo.getSize()));
break;
case DownloadInfo.STATUS_DOWNLOADING:
case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
bt_action.setText("Pause");
try {
pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
} catch (Exception e) {
e.printStackTrace();
}
tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
.formatFileSize(downloadInfo.getSize()));
tv_status.setText("downloading");
break;
case STATUS_COMPLETED:
bt_action.setText("Delete");
try {
pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
} catch (Exception e) {
e.printStackTrace();
}
tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
.formatFileSize(downloadInfo.getSize()));
tv_status.setText("success");
break;
case STATUS_REMOVED:
tv_size.setText("");
pb.setProgress(0);
bt_action.setText("Download");
tv_status.setText("not downloadInfo");
case STATUS_WAIT:
tv_size.setText("");
pb.setProgress(0);
bt_action.setText("Pause");
tv_status.setText("Waiting");
break;
}
}
}
}
關(guān)鍵代碼就是bindData方法中先通過業(yè)務(wù)的id租谈,我們這里使用的url來獲取該業(yè)務(wù)數(shù)據(jù)是否有對應(yīng)的下載任務(wù)篮奄。如果有,則從新綁定監(jiān)聽器,也就是這段代碼
downloadInfo
.setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {
// Call interval about one second
@Override
public void onRefresh() {
if (getUserTag() != null && getUserTag().get() != null) {
ViewHolder viewHolder = (ViewHolder) getUserTag().get();
viewHolder.refresh();
}
}
});
其中要注意到的是緩存每個條目我們使用了SoftReference窟却,這樣做的目的內(nèi)容在吃緊的情況下而已及時的是否這些條目昼丑。
接下來又一個重要的點是,設(shè)置按鈕的點擊事件夸赫,通常在這樣的列表中有一個或多個按鈕控制下載狀態(tài)菩帝。
if (downloadInfo != null) {
switch (downloadInfo.getStatus()) {
case DownloadInfo.STATUS_NONE:
case DownloadInfo.STATUS_PAUSED:
case DownloadInfo.STATUS_ERROR:
//resume downloadInfo
downloadManager.resume(downloadInfo);
break;
case DownloadInfo.STATUS_DOWNLOADING:
case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
case STATUS_WAIT:
//pause downloadInfo
downloadManager.pause(downloadInfo);
break;
case DownloadInfo.STATUS_COMPLETED:
downloadManager.remove(downloadInfo);
break;
}
} else {
// Create new download task
File d = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "d");
if (!d.exists()) {
d.mkdirs();
}
String path = d.getAbsolutePath().concat("/").concat(data.getName());
downloadInfo = new Builder().setUrl(data.getUrl())
.setPath(path)
.build();
downloadInfo
.setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {
@Override
public void onRefresh() {
if (getUserTag() != null && getUserTag().get() != null) {
ViewHolder viewHolder = (ViewHolder) getUserTag().get();
viewHolder.refresh();
}
}
});
downloadManager.download(downloadInfo);
}
關(guān)鍵點就是如果沒有下載任務(wù)就創(chuàng)建一個下載任務(wù),如果已有下載任務(wù)就根據(jù)任務(wù)現(xiàn)在的狀態(tài)執(zhí)行相應(yīng)的操作茬腿,比如當(dāng)前是沒有下載呼奢,點擊就是創(chuàng)建一個下載任務(wù)。
接下還有一個重點就是滓彰,我們在回調(diào)監(jiān)聽中調(diào)用了refresh方法控妻,在該方法中根據(jù)狀態(tài)顯示進度和相應(yīng)的操作按鈕州袒。這樣做的好處上面已經(jīng)提到了揭绑。
private void refresh() {
if (downloadInfo == null) {
tv_size.setText("");
pb.setProgress(0);
bt_action.setText("Download");
tv_status.setText("not downloadInfo");
} else {
switch (downloadInfo.getStatus()) {
case DownloadInfo.STATUS_NONE:
bt_action.setText("Download");
tv_status.setText("not downloadInfo");
break;
case DownloadInfo.STATUS_PAUSED:
case DownloadInfo.STATUS_ERROR:
bt_action.setText("Continue");
tv_status.setText("paused");
try {
pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
} catch (Exception e) {
e.printStackTrace();
}
tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
.formatFileSize(downloadInfo.getSize()));
break;
case DownloadInfo.STATUS_DOWNLOADING:
case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
bt_action.setText("Pause");
try {
pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
} catch (Exception e) {
e.printStackTrace();
}
tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
.formatFileSize(downloadInfo.getSize()));
tv_status.setText("downloading");
break;
case STATUS_COMPLETED:
bt_action.setText("Delete");
try {
pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
} catch (Exception e) {
e.printStackTrace();
}
tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
.formatFileSize(downloadInfo.getSize()));
tv_status.setText("success");
break;
case STATUS_REMOVED:
tv_size.setText("");
pb.setProgress(0);
bt_action.setText("Download");
tv_status.setText("not downloadInfo");
case STATUS_WAIT:
tv_size.setText("");
pb.setProgress(0);
bt_action.setText("Pause");
tv_status.setText("Waiting");
break;
}
}
}
到這里改下框架的核心使用方法就介紹完了。
支持
如有任何問題可以在加我們的QQ群或者在Github上提Issue郎哭,另外請?zhí)酙ssue或者的PR的一定要看下項目的貢獻代碼的方法以及一要求他匪,因為如果要保證一個開源項目的質(zhì)量就必須在各方面都規(guī)范化。