最近因?yàn)橐恍┰虮萌枰玫蕉辔募嗑€程斷點(diǎn)下載文件烟很,所以四處查找資料然后做了一個(gè)Demo. 本項(xiàng)目主要參考的是簡(jiǎn)書寶塔上的貓-《Android實(shí)戰(zhàn):多線程多文件斷點(diǎn)續(xù)傳下載+通知欄控制》
本項(xiàng)目GitHub地址:https://github.com/JonyZeng/JonyDownload
對(duì)于多線程下載文件,我們應(yīng)該首先需要了解單線程下載文件的原理师枣,多線程下載就是把文件分為幾份冒掌,每一份由一個(gè)線程去下載。然后將每一個(gè)線程單獨(dú)下載的文件保存在一起就實(shí)現(xiàn)了多線程下載铣猩。
斷點(diǎn)下載相對(duì)于理解比較輕松躁垛,每一次暫停線程的時(shí)候寄猩,將當(dāng)前下載的進(jìn)度保存,下次繼續(xù)從保存的進(jìn)度進(jìn)行下載芥牌。
而多文件多線程下載的原理是基于多線程單文件下載的基礎(chǔ)上贿肩,首先確定多文件同時(shí)下載需要多少個(gè)線程峦椰,然后再確定每一個(gè)文件多線程同時(shí)下載的線程數(shù)。
本項(xiàng)目是采用MVP模式進(jìn)行架構(gòu)的汰规,因?yàn)楸救藢?duì)MVP也是剛了解汤功,所以選擇它來(lái)進(jìn)行架構(gòu)。
- 首先在布局文件中進(jìn)行主界面activity_main的布局控轿。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.jonyz.jonydownload.MainActivity">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/LV_down"
>
</ListView>
</android.support.constraint.ConstraintLayout>
然后設(shè)置listView條目的布局item.xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<TextView
android:id="@+id/Tv_fileName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="下載的文件名"
/>
<ProgressBar
android:id="@+id/Pb_down"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/file_textview" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<Button
android:id="@+id/Btn_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/progressBar2"
android:layout_toLeftOf="@+id/stop_button"
android:text="開(kāi)始" />
<Button
android:id="@+id/Btn_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_below="@+id/progressBar2"
android:text="暫停" />
</LinearLayout>
2.布局完成之后冤竹,按照國(guó)際慣例拂封,我們需要給ListView設(shè)置適配器,并優(yōu)化鹦蠕。由于我們listView是需要顯示下載文件的信息冒签。所以我們需要?jiǎng)?chuàng)建一個(gè)參數(shù)類型是FileBean的list。 這里簡(jiǎn)單的貼一些FileBean(需要序列化)的參數(shù)代碼
public class FileBean implements Serializable {
public String fileName;
public Integer fileSize;
public Integer filePause;//下載暫停位置
public Integer DownSize; //finished
public Integer id;
public String Url;
/**
*
* @param fileName 文件名
* @param fileSize 文件大小
* @param downSize 文件下載
* @param id 文件ID
* @param url 文件下載地址
*/
public FileBean(Integer id,String fileName, Integer fileSize, Integer downSize, String url) {
this.fileName = fileName;
this.fileSize = fileSize;
DownSize = downSize;
this.id = id;
Url = url;
}
之后就到了最精彩的地方了钟病。對(duì)getView的優(yōu)化和對(duì)控件的賦值萧恕。
/**
* 進(jìn)行數(shù)據(jù)和View的適配
*
* @param i
* @param view
* @param viewGroup
* @return
*/
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
final FileBean fileBean=list.get(i);
if (view==null){
view=inflater.inflate(R.layout.item,null);
viewHolder = new MyViewHolder();
viewHolder.mTvfileName=(TextView)view.findViewById(R.id.Tv_fileName);
viewHolder.mBarDown=(ProgressBar)view.findViewById(R.id.Pb_down);
viewHolder.mBtnstart=(Button)view.findViewById(R.id.Btn_start);
viewHolder.mBtnstop=(Button) view.findViewById(R.id.Btn_stop);
viewHolder.mTvfileName.setText(list.get(i).getFileName());
viewHolder.mBarDown.setProgress(fileBean.getDownSize());
viewHolder.mBtnstart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//// TODO: 2017/8/23 開(kāi)始下載
presenter = new DownloadPresenter();
presenter.startDownload(fileBean,context);
Log.d(TAG, "onClick:開(kāi)始");
Toast.makeText(context, "點(diǎn)擊了開(kāi)始", Toast.LENGTH_SHORT).show();
}
});
viewHolder.mBtnstop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//// TODO: 2017/8/23 暫停下載
presenter.stopDownload(fileBean,context);
Log.d(TAG, "onClick:暫停");
Toast.makeText(context, "點(diǎn)擊了暫停", Toast.LENGTH_SHORT).show();
}
});
view.setTag(viewHolder);
}else {
viewHolder= (MyViewHolder) view.getTag();
}
viewHolder.mBarDown.setProgress(fileBean.getDownSize());
return view;
}
根據(jù)ListView的優(yōu)化需求,需要?jiǎng)?chuàng)建一個(gè)靜態(tài)的ViewHolder
/**
* 靜態(tài)的View ,避免重復(fù)加載
*/
static class MyViewHolder {
TextView mTvfileName;
ProgressBar mBarDown;
Button mBtnstart;
Button mBtnstop;
}
兩個(gè)按鈕的點(diǎn)擊事件的邏輯代碼是通過(guò)MVP的實(shí)現(xiàn)的肠阱。所以需要?jiǎng)?chuàng)建一個(gè)Contrast類設(shè)置接口票唆,在model層實(shí)現(xiàn)代碼的邏輯實(shí)現(xiàn)。
屹徘。
最終在model層里面開(kāi)啟服務(wù)走趋。通過(guò)intent傳遞值過(guò)去
private static final String TAG = DownloadModel.class.getSimpleName();
private Intent intent;
@Override
public void startDownload(FileBean fileBean, Context context) {
//開(kāi)始下載
intent = new Intent(context,DownloadService.class);
intent.setAction(Config.ACTION_START);
intent.putExtra("fileBean",fileBean);
Log.d(TAG, "startDownload: 開(kāi)啟下載的服務(wù)");
context.startService(intent);
}
@Override
public void stopDownload(FileBean fileBean, Context context) {
//暫停下載
Intent intent = new Intent(context, DownloadService.class);
intent.setAction(Config.ACTION_STOP);
intent.putExtra("fileBean", fileBean);
Log.d(TAG, "stopDownload: 停止下載服務(wù)");
context.startService(intent);
}
- 多線程斷點(diǎn)下載服務(wù)。創(chuàng)建一個(gè)類繼承Service噪伊,在AndroidMainfest.xml中聲明簿煌。重寫onStartCommand方法,在里面根據(jù)不同的intent值進(jìn)行不同的操作鉴吹。在開(kāi)始下載的時(shí)候姨伟,需要通過(guò)線程池開(kāi)啟一個(gè)線程,所以我們需要自定義一個(gè)類繼承Thread豆励,在里面實(shí)現(xiàn)下載的代碼夺荒。
自定義線程類所需要的參數(shù)
private URL url;
private int responseCode;
private int length = 0; //判斷長(zhǎng)度
private RandomAccessFile randomAccessFile;
private FileBean fileBean = null;
在重寫的run方法中,進(jìn)行對(duì)當(dāng)前點(diǎn)擊下載文件的進(jìn)行獲取和設(shè)置下載文件的路徑良蒸。
url = new URL(fileBean.getUrl());
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(2000);
connection.setRequestMethod("GET");
Log.i(TAG, "run:獲取網(wǎng)絡(luò)請(qǐng)求");
responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
length = connection.getContentLength();
}else{
Toast.makeText(DownloadService.this, "請(qǐng)檢查網(wǎng)絡(luò)情況", Toast.LENGTH_SHORT).show();
}
if (length <= 0) {//說(shuō)明下載文件不存在
return;
}
//文件存在技扼,開(kāi)始下載
//判斷文件路徑是否存在
File dir = new File(Config.DownloadPath);
if (!dir.exists()) {
dir.mkdir();
}
通過(guò)RandomAccessFile在內(nèi)存中根據(jù)當(dāng)前文件大小進(jìn)行占位。
File file = new File(dir, fileBean.getFileName());
randomAccessFile = new RandomAccessFile(file, "rwd");//隨機(jī)訪問(wèn)诚啃,隨時(shí)讀寫
randomAccessFile.setLength(length);
fileBean.setDownSize(length); //設(shè)置文件的大小
將通過(guò)handler將fileBean傳遞淮摔。
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case Config.MSG_INIT://開(kāi)始下載
//調(diào)用DownloadTask的download下載
fileBean= (FileBean) msg.obj;
DownloadTask task = new DownloadTask(DownloadService.this, fileBean, 3);
task.download();
taskMap.put(fileBean.getId(),task);
Intent intent = new Intent(Config.ACTION_START);
intent.putExtra("fileBean",fileBean);
sendBroadcast(intent);
}
}
};
創(chuàng)建一個(gè)下載任務(wù)類DownloadTask,在里面執(zhí)行下載任務(wù)始赎。
DownloadTask所需要的一些參數(shù)
private static final String TAG = DownloadTask.class.getSimpleName();
public static ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); //創(chuàng)建一個(gè)線程池,每次開(kāi)啟線程通過(guò)線程池調(diào)用仔燕。
public List<UrlBean> urlBeanList; //一個(gè)UrlBean的list集合造垛。
private FileBean fileBean; //下載文件的信息類
public DBUtil dbUtil; //數(shù)據(jù)庫(kù)操作工具類
private ThreadUtil threadUtil; //下載線程的信息類
public int threadCount=1;
public Context context;
private List<DownloadList> downloadLists=null;
private UrlBean urlBean;
public boolean isonPause=false;//判斷是否暫停
創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)線程下載幫助類,里面實(shí)現(xiàn)線程對(duì)數(shù)據(jù)庫(kù)的增刪查改晰搀。查詢數(shù)據(jù)庫(kù)線程的方法
/**
* 查詢數(shù)據(jù)庫(kù)線程
* @param url
* @return
*/
public synchronized List<UrlBean>queryThread(String url){
Log.i(TAG, "queryThread: url: " + url);
readableDatabase=dbUtil.getReadableDatabase();
List<UrlBean> list=new ArrayList<>();
Cursor cursor=readableDatabase.query("download_info", null, "url = ?", new String[] { url }, null, null, null);
while (cursor.moveToNext()){
UrlBean urlBean= new UrlBean();
urlBean.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
urlBean.setUrl(cursor.getString(cursor.getColumnIndex("url")));
urlBean.setStart(cursor.getInt(cursor.getColumnIndex("start")));
urlBean.setEnd(cursor.getInt(cursor.getColumnIndex("end")));
urlBean.setFinished(cursor.getInt(cursor.getColumnIndex("finished")));
list.add(urlBean);
}
cursor.close();
readableDatabase.close();
//readableDatabase.query("urlBean",)
return list;
}
創(chuàng)建一個(gè)download方法五辽,方便其他地方調(diào)用。在這個(gè)方法里面實(shí)現(xiàn)查詢文件的大小和開(kāi)啟多個(gè)線程外恕,分塊下載杆逗。
public void download(){
Log.i(TAG, "download:"+fileBean.getUrl());
urlBeanList =threadUtil.queryThread(fileBean.getUrl());
//獲取到數(shù)據(jù)庫(kù)里面查詢的list
if (urlBeanList.size()==0){ //數(shù)據(jù)庫(kù)中不存在乡翅,第一次下載
//獲取文件大小
int length=fileBean.getDownSize();
//獲取需要分的模塊
int block=length/threadCount;
for (int i = 0; i < threadCount; i++) {
//確定開(kāi)始下載的位置
int star=block*i;
//確定結(jié)束下載的位置
int end=(i+1)*block-1;
if (i==threadCount-1){ // //最后一個(gè)線程下載結(jié)束的位置。
end=length-1;
}
//開(kāi)啟線程
urlBean = new UrlBean(fileBean.getUrl(),i,star,end,0);
urlBeanList.add(urlBean);
}
}
Log.d(TAG, "download:");
//下載文件線程的內(nèi)部類
downloadLists = new ArrayList<>();
for (UrlBean urlBean:urlBeanList) {
DownloadList dowmload=new DownloadList(urlBean);
DownloadTask.cachedThreadPool.execute(dowmload);
downloadLists.add(dowmload);
threadUtil.insertThread(urlBean);
}
}
創(chuàng)建一個(gè)內(nèi)部類罪郊,用于將下載下來(lái)的文件蠕蚜,寫入到內(nèi)存中去。
部分參數(shù)
private HttpURLConnection urlConnection=null;
private RandomAccessFile accessFile=null;
private InputStream inputStream=null;
private File file;
private Integer finished=0;
private Intent intent;
private UrlBean urlBean;
private boolean isFinished=false;
重寫run 方法悔橄,在里面請(qǐng)求網(wǎng)絡(luò)靶累,并且將請(qǐng)求下來(lái)的數(shù)據(jù)通過(guò)RandomAccessFile寫入到內(nèi)存中去。
@Override
public void run() {//執(zhí)行下載耗時(shí)操作
//獲取http對(duì)象
try {
URL url= new URL(fileBean.getUrl());
try {
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setConnectTimeout(2000);
urlConnection.setRequestMethod("GET");
//設(shè)置下載的頭部
int start=urlBean.getStart()+urlBean.getFinished();
//設(shè)置下載結(jié)束的位置
urlConnection.setRequestProperty("Range", "bytes=" + start + "-" + urlBean.getEnd());
//新建文件對(duì)象
file = new File(Config.DownloadPath, fileBean.getFileName());
//隨機(jī)訪問(wèn)讀寫對(duì)象
accessFile = new RandomAccessFile(file, "rwd");
accessFile.seek(start);
//刷新當(dāng)前以及下載的大小
finished +=urlBean.getFinished();
intent = new Intent();
intent.setAction(Config.ACTION_UPDATE);
int respCode=urlConnection.getResponseCode();
if (respCode==HttpURLConnection.HTTP_PARTIAL){ //請(qǐng)求成功
//獲取輸入流對(duì)象
inputStream = urlConnection.getInputStream();
//設(shè)置一個(gè)byte數(shù)組癣疟,中轉(zhuǎn)數(shù)據(jù)
byte[] bytes = new byte[1024];
int length=-1;
//定義UI刷新時(shí)間
long time=System.currentTimeMillis();
while ((length=inputStream.read(bytes))!=-1){
accessFile.write(bytes,0,length);
//實(shí)時(shí)獲取下載進(jìn)度挣柬,刷新UI
finished+=length;
urlBean.setFinished(urlBean.getFinished()+length);
if (System.currentTimeMillis()-time>500){
time=System.currentTimeMillis();
intent.putExtra("finished",finished);
Log.d(TAG, "finished:"+finished);
intent.putExtra("id",fileBean.getId());
Log.d(TAG, "finished"+fileBean.getId());
context.sendBroadcast(intent);
}
if (isonPause){
threadUtil.updateThread(urlBean);
return;
}
}
}
//當(dāng)前線程是否下載完成
isFinished = true;
//判斷所有線程是否下載完成
checkAllFinished();
判斷所有的線是否已經(jīng)下載完成。
private synchronized void checkAllFinished() {
boolean allFinished=true;
for (DownloadList down:downloadLists) {
if (!down.isFinished)
allFinished=false;
break;
}
if (allFinished==true){
Log.d(TAG, "checkAllFinished: 下載完成");
threadUtil.delThread(fileBean.getUrl());
intent=new Intent(Config.ACTION_FINISHED);
intent.putExtra("urlBean",urlBean);
context.sendBroadcast(intent);
}
}
4.好了睛挚,終于到最后MainActivity里面的一些邏輯實(shí)現(xiàn)了邪蛔。
private static final String TAG = MainActivity.class.getSimpleName();
ListView listView;
private FileAdapter mAdapter;
private UrlBean urlBean;
private List<FileBean> list;
private UIRecive mRecive;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
/**
* 初始化
*/
private void init() {
initData();
initView();
mAdapter = new FileAdapter(this, list);
listView.setAdapter(mAdapter);
mRecive=new UIRecive();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Config.ACTION_UPDATE);
intentFilter.addAction(Config.ACTION_FINISHED);
intentFilter.addAction(Config.ACTION_START);
registerReceiver(mRecive, intentFilter);
}
/**
* 初始化控件
*/
private void initView() {
listView = (ListView) findViewById(R.id.LV_down);
}
/**
* 初始化數(shù)據(jù)
*/
private void initData() {
//文件類集合
list = new ArrayList<>();
list.add(new FileBean(0,getfileName(Config.URLONE),0,0,Config.URLONE)); //(文件ID,文件名扎狱,文件大小店溢,已經(jīng)下載大小,URL)
list.add(new FileBean(1,getfileName(Config.URLTWO),0,0,Config.URLTWO)); //(文件ID,文件名委乌,文件大小床牧,已經(jīng)下載大小,URL)
list.add(new FileBean(2,getfileName(Config.URLTHREE),0,0,Config.URLTHREE)); //(文件ID,文件名遭贸,文件大小戈咳,已經(jīng)下載大小,URL)
list.add(new FileBean(3,getfileName(Config.URLFOUR),0,0,Config.URLFOUR)); //(文件ID,文件名壕吹,文件大小著蛙,已經(jīng)下載大小,URL)
}
@Override
public String getfileName(String url) {
return url.substring(url.lastIndexOf("/") + 1);
}
@Override
protected void onDestroy() {
unregisterReceiver(mRecive);
super.onDestroy();
}
/**
* 刷新Ui
*/
private class UIRecive extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) { //接收到傳遞過(guò)來(lái)的數(shù)據(jù)
if (Config.ACTION_UPDATE.equals(intent.getAction())) {
// 更新進(jìn)度條的時(shí)候
int finished = intent.getIntExtra("finished", 0);
Log.d(TAG, "onReceive:finsihed"+finished);
int id = intent.getIntExtra("id", 0);
mAdapter.updataProgress(id, finished);
} else if (Config.ACTION_FINISHED.equals(intent.getAction())){
// 下載結(jié)束的時(shí)候
FileBean fileBean = (FileBean) intent.getSerializableExtra("fileBean");
mAdapter.updataProgress(fileBean.getId(), 0);
Toast.makeText(MainActivity.this, list.get(fileBean.getId()).getFileName() + "下載完畢", Toast.LENGTH_SHORT).show();
}
}
}