運行效果圖:
多任務多線程下載並不麻煩,只要思路清晰,邏輯清晰正確,是很好實現的。我最後遇到的糾結問題是數據庫的操作上,我是拿數據庫來存儲下載信息的,所以在數據庫的關閉上遇到了麻煩。上面那個版本是建立在前面N個demo的基礎之上的,在這裏我寫下來的唯一目的就是能夠以一個清晰的思路寫清楚,同時讓大家看明白。
一、首先是數據庫,
數據庫五個字段:
任務的ID:_id
線程ID:thread_id
線程下載的起始位置:start_pos
這個線程下載的結束位置:end_pos
這個任務已經下載的大小:compelete_size
這個任務的下載地址:urlString
create table download_info(_id integer PRIMARY KEY AUTOINCREMENT, thread_id integer,start_pos integer, end_pos integer, compelete_size integer,urlString char)
二、操作數據庫的類。在實現斷點續傳下載的時候,我是把線程每次下載結束後的當前任務信息都保存到數據庫裏面一次,相當於每次一個線程下載一次,就給當前任務拍個照片,把當前信息存到數據庫裏面。這樣一旦暫停,或者退出程序,下次再下載的時候,直接從數據庫裏面讀數據,然後在這個數據的基礎上繼續下載就行。
package com.song.dao;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.song.db.DBHelper;
import com.song.entity.ThreadDownloadInfo;
/**
* 操作數據庫
* @author song
*
*/
public class DownloadDao
{
private DBHelper dbHelper;
public DownloadDao(Context context)
{
dbHelper = new DBHelper(context);
}
/**
*
* 判斷數據庫中是不是有對應這個urlString的信息
*
* @return
*/
public boolean unhasInfo(String urlString)
{
SQLiteDatabase db = dbHelper.getReadableDatabase();
String sql = "select count(*) from download_info where urlString=?";
Cursor cursor = db.rawQuery(sql, new String[]{urlString});
cursor.moveToFirst();
int count = cursor.getInt(0);
cursor.close();
return count == 0;
}
/**
* 把線程信息保存在數據庫裏面
* @param infos
*/
public void saveInfos(List<ThreadDownloadInfo> infos)
{
SQLiteDatabase db = dbHelper.getWritableDatabase();
for (ThreadDownloadInfo info : infos)
{
String sql = "insert into download_info(thread_id,start_pos, end_pos,compelete_size,urlString) values (?,?,?,?,?)";
Object[] bindArgs =
{ info.getThreadId(), info.getStartPos(), info.getEndPos(),
info.getCompleteSize() ,info.getUrlString()};
db.execSQL(sql, bindArgs);
}
}
/**
* 暫停之後,把當前數據保存在數據庫中,該方法是從數據庫中查詢數據
*
* @return
*/
public List<ThreadDownloadInfo> getInfos(String urlString)
{
List<ThreadDownloadInfo> list = new ArrayList<ThreadDownloadInfo>();
SQLiteDatabase db = dbHelper.getReadableDatabase();
String sql = "select thread_id, start_pos, end_pos,compelete_size, urlString from download_info where urlString=?";
Cursor cursor = db.rawQuery(sql, new String[]{urlString});
while (cursor.moveToNext())
{
ThreadDownloadInfo info = new ThreadDownloadInfo(cursor.getInt(0),
cursor.getInt(1), cursor.getInt(2), cursor.getInt(3),cursor.getString(4));
list.add(info);
}
cursor.close();
return list;
}
/**
* 把當前的數據照片 存進數據庫中
*
* @param threadId
* @param completeSize
*/
public void updateInfo(int threadId, int completeSize,String urlString)
{
SQLiteDatabase db = dbHelper.getWritableDatabase();
String sql = "update download_info set compelete_size=? where thread_id=? and urlString=?";
Object[] bindArgs =
{ completeSize, threadId,urlString };
db.execSQL(sql, bindArgs);
}
/**
* 關閉數據庫
*/
public void closeDB()
{
dbHelper.close();
}
/**
* 下載完成之後,從數據庫裏面把這個任務的信息刪除
* 不同的任務對應不同的urlString
* @param urlString
*/
public void deleteInfos(String urlString)
{
SQLiteDatabase db=dbHelper.getWritableDatabase();
db.delete("download_info", "urlString=?", new String[]{urlString});
}
}
數據庫助手類DBhelper:
package com.song.db;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
/**
* 數據庫助手類
* @author song
*
*/
public class DBHelper extends SQLiteOpenHelper
{
public DBHelper(Context context)
{
super(context, "download.db", null, 1);
}
@Override
public void onCreate(SQLiteDatabase db)
{
Log.v("TAG", "DBHelper-->conCreate()");
String sql = "create table download_info(_id integer PRIMARY KEY AUTOINCREMENT, thread_id integer,start_pos integer, end_pos integer, compelete_size integer,urlString char)";
db.execSQL(sql);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
{
}
}
二、數據庫中的方法,很多是建立在別的類的基礎上,當別的類需要操作數據庫的時候,在數據中添加相對應的方法即可。
下面是兩個實體類: DownloadInfo是每一個下載任務的信息; ThreadDownloadInfo是一個下載任務對應的下載線程的信息類。
package com.song.entity;
/**
* 每一個下載文件的信息
*
* @author song
*
*/
public class DownloadInfo
{
private int fileSize;
private int completeSize;
private String urlString;
public DownloadInfo(int fileSize, int completeSize, String urlString)
{
super();
this.fileSize = fileSize;
this.completeSize = completeSize;
this.urlString = urlString;
}
public DownloadInfo()
{
super();
}
public int getFileSize()
{
return fileSize;
}
public void setFileSize(int fileSize)
{
this.fileSize = fileSize;
}
public int getCompleteSize()
{
return completeSize;
}
public void setCompleteSize(int completeSize)
{
this.completeSize = completeSize;
}
public String getUrlString()
{
return urlString;
}
public void setUrlString(String urlString)
{
this.urlString = urlString;
}
@Override
public String toString()
{
return "DownloadInfo [fileSize=" + fileSize + ", completeSize="
+ completeSize + ", urlString=" + urlString + "]";
}
}
線程信息類:
package com.song.entity;
/**
* 線程信息類
* @author song
*
*/
public class ThreadDownloadInfo
{
private int threadId;// 開啓的線程數
private int startPos;// 該進程的起始位置
private int endPos;// 該進程的終止位置
private int completeSize;// 完成的進度
private String urlString;// 當前任務的url
public String getUrlString()
{
return urlString;
}
public void setUrlString(String urlString)
{
this.urlString = urlString;
}
@Override
public String toString()
{
return "ThreadDownloadInfo [threadId=" + threadId + ", startPos="
+ startPos + ", endPos=" + endPos + ", completeSize="
+ completeSize + ", urlString=" + urlString + "]";
}
public ThreadDownloadInfo(int threadId, int startPos, int endPos,
int completeSize, String urlString)
{
this.threadId = threadId;
this.startPos = startPos;
this.endPos = endPos;
this.completeSize = completeSize;
this.urlString = urlString;
}
public ThreadDownloadInfo()
{
}
public int getCompleteSize()
{
return completeSize;
}
public void setCompleteSize(int completeSize)
{
this.completeSize = completeSize;
}
public int getThreadId()
{
return threadId;
}
public void setThreadId(int threadId)
{
this.threadId = threadId;
}
public int getStartPos()
{
return startPos;
}
public void setStartPos(int startPos)
{
this.startPos = startPos;
}
public int getEndPos()
{
return endPos;
}
public void setEndPos(int endPos)
{
this.endPos = endPos;
}
}
三、MainActivity
package com.song.activity;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import android.app.ListActivity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import android.widget.Toast;
import com.song.R;
import com.song.entity.DownloadInfo;
import com.song.service.DownLoader;
public class MainActivity extends ListActivity
{
//下載地址
private static final String URL = "http://192.168.1.101:8080/struts2_net/";
//SD卡目錄
private static final String SD_DIR = "/mnt/sdcard/";
//下載器的Map KEY是URL
private Map<String, DownLoader> downLoaders=new HashMap<String, DownLoader>();
//進度條 用URL標記
private Map<String,ProgressBar> bars =new HashMap<String, ProgressBar>();
//handler用來處理進度條
private Handler mHandler = new Handler()
{
public void handleMessage(Message msg)
{
if (msg.what==1)
{
int length=msg.arg2;
String urlString=(String) msg.obj;
ProgressBar bar=bars.get(urlString);
if (bar!=null)
{
bar.incrementProgressBy(length);
if(bar.getProgress()==bar.getMax())
{
Toast.makeText(MainActivity.this, "下載完成", 0).show();
LinearLayout layout=(LinearLayout) bar.getParent();
//下載完成後,從視圖中移除進度條
layout.removeView(bar);
//從進度條的Map中移除這個進度條
bars.remove(urlString);
//在數據庫中把對應這個任務的下載信息刪除
downLoaders.get(urlString).deleteInfo(urlString);
//把這個任務的下載器移除
DownLoader loader = downLoaders.remove(urlString);
loader.closeDB();
}
}
}
}
};
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//顯示ListView
showList();
}
/**
* 在MainACtivity上顯示listView
*/
private void showList()
{
List<Map<String, String>> data= new ArrayList<Map<String,String>>();
Map<String, String> map= new HashMap<String, String>();
map.put("name", "tt.mp3");
data.add(map);
map= new HashMap<String, String>();
map.put("name", "mm.mp3");
data.add(map);
map= new HashMap<String, String>();
map.put("name", "pp.mp3");
data.add(map);
SimpleAdapter adapter =new SimpleAdapter(this, data, R.layout.list_item, new String[]{"name"}, new int[]{R.id.tv_resouce_name});
setListAdapter(adapter);
}
/**
* 點擊開始下載
*
* @param view
*/
public void startDownload(View view)
{
//得到Button所在的LinearLayout,利用他得到textview上的文件名
LinearLayout layout = (LinearLayout) view.getParent();
String name = ((TextView)layout.findViewById(R.id.tv_resouce_name)).getText().toString();
String urlString=URL+name;//下載地址
String localFile=SD_DIR+name;//保存的目錄
int threadCount=3;//啓動三個線程開始下載
DownLoader downLoader= downLoaders.get(urlString);
//如果下載器是空的,表示第一次下載或者下載已完成
if (downLoader==null)
{
Log.v("TAG", "startDownload------->downLoader==null");
//開始一次下載,初始化一個下載器
downLoader= new DownLoader(urlString, localFile, threadCount, this, mHandler);
//把下載器和標識這個下載器的URL放進MAP
downLoaders.put(urlString, downLoader);
}
//如果是正在下載,點擊下載按鈕
if(downLoader.isDownloading())
{
return;
}
DownloadInfo info = downLoader.getDownloadInfo();
showProcessBar(view,info,urlString);
downLoader.download();
}
/**
* 顯示進度條
* @param infos
*/
private void showProcessBar(View view, DownloadInfo info, String urlString)
{
ProgressBar bar=bars.get(urlString);
if (bar==null)
{
bar=new ProgressBar(this, null,android.R.attr.progressBarStyleHorizontal);
bars.put(urlString, bar);
bar.setMax(info.getFileSize());
bar.setProgress(info.getCompleteSize());
Log.v("TAG", "urlString="+urlString + "completeSize="+info.getCompleteSize());
LinearLayout.LayoutParams params= new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, 4);
params.setMargins(5, 5, 5, 5);
((LinearLayout)((LinearLayout)view.getParent()).getParent()).addView(bar);
}
}
/**
* 暫停下載
*
* @param view
*/
public void pauseDownload(View view)
{
LinearLayout layout= (LinearLayout) view.getParent();
String name=((TextView)layout.findViewById(R.id.tv_resouce_name)).getText().toString();
String urlString=URL+name;
downLoaders.get(urlString).pause();
}
/**
* 退出Activity的時候,把數據庫關掉,並且把下載器的的list賦爲空
* 每次打開activity的時候,會創建activity的對象
*/
@Override
protected void onDestroy()
{
super.onDestroy();
if(!downLoaders.isEmpty()) {
Set<Map.Entry<String,DownLoader>> set = downLoaders.entrySet();
for(Entry<String,DownLoader> entry : set) {
entry.getValue().closeDB();
}
downLoaders = null;
}
}
}
四、下載器類,爲MainActivity提供服務(方法)
package com.song.service;
import java.io.File;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import com.song.dao.DownloadDao;
import com.song.entity.DownloadInfo;
import com.song.entity.ThreadDownloadInfo;
/**
* 下載服務類
*
* @author song
*
*/
public class DownLoader
{
private String urlString;// 下載地址
private String localFile;// 保存的文件
private int threadCount;// 開啓的線程數
private int fileSize;// 文件大小
private Handler mHandler;// 同步進度條
private DownloadDao dao;// 數據庫操作類
private List<ThreadDownloadInfo> infos;// 保存下載信息
// 標記下載狀態
public static final int INIT = 1; // 初始狀態
public static final int DOWNLOADING = 2;// 正在下載
public static final int PAUSE = 3;// 暫停
private int state = INIT;
public DownLoader(String urlString, String localFile, int threadCount,
Context context, Handler handler)
{
this.urlString = urlString;
this.localFile = localFile;
this.threadCount = threadCount;
mHandler = handler;
dao = new DownloadDao(context);
}
/**
* 下載
*/
public void download()
{
if (infos != null)
{
Log.v("TAG", "download()------->infos != null");
if (state == DOWNLOADING)
{
return;
}
state = DOWNLOADING;
for (ThreadDownloadInfo info : infos)
{
new DownloadThread(info.getThreadId(), info.getStartPos(),
info.getEndPos(), info.getCompleteSize(),info.getUrlString()).start();
}
}
}
/**
* 下載器是否正在下載 true: 正在下載
*/
public boolean isDownloading()
{
return state == DOWNLOADING;
}
/**
* 得到當前下載信息
*
* @return
*/
public DownloadInfo getDownloadInfo()
{
if (isFirst(urlString))
{
init();
infos = new ArrayList<ThreadDownloadInfo>();
int range = fileSize / threadCount;
for (int i = 0; i < threadCount - 1; i++)
{
ThreadDownloadInfo info = new ThreadDownloadInfo(i, i * range,
(i + 1) * range - 1, 0, urlString);
infos.add(info);
}
ThreadDownloadInfo info = new ThreadDownloadInfo(threadCount - 1,
(threadCount - 1) * range, fileSize - 1, 0, urlString);
infos.add(info);
dao.saveInfos(infos);
return new DownloadInfo(fileSize, 0, urlString);
} else
{
infos = dao.getInfos(urlString);
int size = 0;
int completeSize = 0;
for (ThreadDownloadInfo info : infos)
{
completeSize += info.getCompleteSize();
size += info.getEndPos() - info.getStartPos() + 1;
}
return new DownloadInfo(size, completeSize, urlString);
}
}
/**
* 初始化 連接網絡,準備文件的保存路徑等
*/
private void init()
{
try
{
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
fileSize = conn.getContentLength();
File file = new File(localFile);
if (file.exists())
{
file.delete();
}
file.createNewFile();
RandomAccessFile rFile = new RandomAccessFile(localFile, "rwd");
rFile.setLength(fileSize);
rFile.close();
conn.disconnect();
} catch (Exception e)
{
e.printStackTrace();
}
}
/**
* 判斷是不是斷點續傳(即是不是第一次下載) true:第一次下載
*
* @param urlString
* @return
*/
private boolean isFirst(String urlString)
{
return dao.unhasInfo(urlString);
}
/**
* 暫停
*/
public void pause()
{
state = PAUSE;
}
/**
* 下載的線程類
*
* @author song
*/
private class DownloadThread extends Thread
{
private int threadId;
private int startPos;
private int endPos;
private int completeSize;
private String urlString;
public DownloadThread(int threadId, int startPos, int endPos,
int completeSize, String urlString)
{
this.threadId = threadId;
this.startPos = startPos;
this.endPos = endPos;
this.completeSize = completeSize;
this.urlString = urlString;
}
@Override
public void run()
{
HttpURLConnection conn = null;
RandomAccessFile rFile = null;
InputStream is = null;
try
{
URL url = new URL(urlString);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(4000);
conn.setRequestProperty("Range", "bytes="
+ (startPos + completeSize) + "-" + endPos);
rFile = new RandomAccessFile(localFile, "rwd");
rFile.seek(startPos + completeSize);
is = conn.getInputStream();
byte[] buffer = new byte[2048];
int len = -1;
while ((len = is.read(buffer)) != -1)
{
rFile.write(buffer, 0, len);
completeSize += len;
dao.updateInfo(threadId, completeSize, urlString);
Message msg = Message.obtain();
msg.what = 1;
msg.arg2 = len;
msg.obj = urlString;
mHandler.sendMessage(msg);
Log.v("TAG", "completeSize="+completeSize);
if (state == PAUSE)
{
return;
}
}
} catch (Exception e)
{
e.printStackTrace();
} finally
{
try
{
is.close();
rFile.close();
conn.disconnect();
dao.closeDB();
} catch (Exception e)
{
e.printStackTrace();
}
}
}
}
public void deleteInfo(String urlString)
{
dao.deleteInfos(urlString);
}
public void closeDB() {
dao.closeDB();
}
}
PS:這個寫的很簡單,實現了最基本的功能。後續可以加上服務,實現後臺下載。
這段時間情緒低落,代碼寫的少了,僅僅完成了幾個慘不忍睹的半成品。感覺完全沒有得到提高。
Luffy ,you should do sth to change it.