运行效果图:
多任务多线程下载并不麻烦,只要思路清晰,逻辑清晰正确,是很好实现的。我最后遇到的纠结问题是数据库的操作上,我是拿数据库来存储下载信息的,所以在数据库的关闭上遇到了麻烦。上面那个版本是建立在前面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.