先看下项目结构:
http多线程断点下载涉及到 数据库,多线程和http请求等几个模块,东西不是很多,想弄清楚也不是很困难,接下来我和大家分享下我的做法。
一、先看MainActivity.java
成员变量,主要是一些下载过程的变量和handler
private String path = "http://192.168.1.3:8080/wanmei/yama.apk";
private String sdcardPath;
private int threadNum = 5;
ProgressDialog dialog;
// 下载的进度
private int process;
// 下载完成的百分比
private int done;
private int filelength;
// 本次下载开始之前,已经完成的下载量
private int completed;
// 用线程池是为了能够优雅的中断线程下载
ExecutorService pool;
@SuppressLint("HandlerLeak")
private Handler handler = new Handler() {
public void handleMessage(android.os.Message msg) {
process += msg.arg1;
done = (int) ((1.0 * process / filelength) * 100);
Log.i("process", "process" + done);
dialog.setProgress(done);
// 第一次没有显示dialog的时候显示dialog
if (done == 100) {// 提示用户下载完成
// 线程下载完成以后就删除在数据库的缓存数据
DBService.getInstance(getApplicationContext()).delete(path);
// 做一个延时的效果,可以让用户多看一会100%
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
dialog.dismiss();
}
}, 1000);
}
};
};
download方法触发下载事件,先检查有没有sd卡,然后才开始开线程下载
public void download(View v) {
completed = 0;
process = 0;
done = 0;
pool = Executors.newFixedThreadPool(threadNum);
initProgressDialog();
new Thread() {
public void run() {
try {
if (Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
sdcardPath = Environment.getExternalStorageDirectory()
.getAbsolutePath();
} else {
toast("没有内存卡");
return;
}
download(path, threadNum);
} catch (Exception e) {
e.printStackTrace();
}
};
}.start();
}
在真正开始下载之前,我们得先做一次http请求,为的是获取下载文件的大小和文件名,好预先准备好本地文件的大小以及各个线程应该下载的区域。这个时候我们请求的信息在响应头里面都有,只需要请求head就行了,既缩短了响应时间,也能节省流量
public void download(String path, int threadsize) throws Exception {
long startTime = System.currentTimeMillis();
URL url = new URL(path);
// HttpHead head = new HttpHead(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 这里只需要获取httphead,至请求头文件,不需要body,
// 不仅能缩短响应时间,也能节省流量
// conn.setRequestMethod("GET");
conn.setRequestMethod("HEAD");
conn.setConnectTimeout(5 * 1000);
Map<String, List<String>> headerMap = conn.getHeaderFields();
Iterator<String> iterator = headerMap.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
List<String> values = headerMap.get(key);
System.out.println(key + ":" + values.toString());
}
filelength = conn.getContentLength();// 获取要下载的文件的长度
long endTime = System.currentTimeMillis();
Log.i("spend", "spend time = " + (endTime - startTime));
String filename = getFilename(path);// 从路径中获取文件名称
File File = new File(sdcardPath + "/download/");
if (!File.exists()) {
File.mkdirs();
}
File saveFile = new File(sdcardPath + "/download/" + filename);
RandomAccessFile accessFile = new RandomAccessFile(saveFile, "rwd");
accessFile.setLength(filelength);// 设置本地文件的长度和下载文件相同
accessFile.close();
// 计算每条线程下载的数据长度
<strong>int block = filelength % threadsize == 0 ? filelength / threadsize
: filelength / threadsize + 1;</strong>
// 判断是不是第一次下载,不是就计算已经下载了多少
if (!DBService.getInstance(getApplicationContext()).isHasInfors(path)) {
for (int threadid = 0; threadid < threadNum; threadid++) {
completed += DBService.getInstance(getApplicationContext())
.getInfoByIdAndUrl(threadid, path);
}
}
Message msg = handler.obtainMessage();
msg.arg1 = completed;
handler.sendMessage(msg);
for (int threadid = 0; threadid < threadsize; threadid++) {
pool.execute(new DownloadThread(getApplicationContext(), path,
saveFile, block, threadid, threadNum)
.setOnDownloadListener(this));
}
}
DownloadThread.java
有两点:1、谷歌推荐httpurlconnection,我试了下下载速度确实比httpclient快
2、下载的时候用来缓存的byte数组,他的长度影响到下载速度的快慢
@Override
public void run() {
Log.i("download", "线程id:" + threadid + "开始下载");
// 计算开始位置公式:线程id*每条线程下载的数据长度+已下载完成的(断点续传)= ?
// 计算结束位置公式:(线程id +1)*每条线程下载的数据长度-1 =?
completed = DBService.getInstance(context).getInfoByIdAndUrl(threadid, url);
int startposition = threadid * block+completed;
int endposition = (threadid + 1) * block - 1;
try {
RandomAccessFile accessFile = new RandomAccessFile(saveFile, "rwd");
accessFile.seek(startposition);// 设置从什么位置开始写入数据
// 我测试的时候,用httpurlconnection下载速度比httpclient快了10倍不止
HttpURLConnection conn = (HttpURLConnection) new URL(url)
.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5 * 1000);
conn.setRequestProperty("Accept-Language", "zh-CN");
conn.setRequestProperty(
"Accept",
"image/gif, image/jpeg, image/pjpeg," +
" image/pjpeg, application/x-shockwave-flash," +
" application/xaml+xml, application/vnd.ms-xpsdocument," +
" application/x-ms-xbap, application/x-ms-application, " +
"application/vnd.ms-excel, application/vnd.ms-powerpoint, " +
"application/msword, */*");
conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0;" +
" Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727;" +
" .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
conn.setRequestProperty("Referer", url);
conn.setRequestProperty("Connection", "Keep-Alive");
conn.setRequestProperty("RANGE", "bytes=" + startposition + "-"
+ endposition);// 设置获取实体数据的范围
// HttpClient httpClient = new DefaultHttpClient();
// HttpGet httpGet = new HttpGet(url);
// httpGet.addHeader("Range",
// "bytes="+startposition+"-"+endposition);
// HttpResponse response = httpClient.execute(httpGet);
InputStream inStream = conn.getInputStream();
// 这里需要注意,数组的长度其实代表了每次下载的流的大小
// 如果太小的话,例如1024,每次就都只会下载1024byte的内容,速度太慢了,
// 对于下载十几兆的文件来说太难熬了,太小了相当于限速了
// 但也不能太大,如果太大了,那么缓冲区中的数据会过大,从而造成oom
// 为了不oom又能开最大的速度,这里可以获取应用可用内容,动态分配
int freeMemory = ((int) Runtime.getRuntime().freeMemory());// 获取应用剩余可用内存
byte[] buffer = new byte[freeMemory / threadNum];// 可用内存得平分给几个线程
// byte[] buffer = new byte[1024];
int len = 0;
int total = 0;
boolean isInterrupted=false;
while ((len = inStream.read(buffer)) != -1) {
accessFile.write(buffer, 0, len);
total += len;
Log.i("download", "线程id:" + threadid + "已下载" + total + "总共有" + block);
// 实时更新进度
listener.onDownload(threadid,len,total,url);
//当线程被暗示需要中断以后,退出循环,终止下载操作
<strong>if(Thread.interrupted()){
isInterrupted=true;
break;
}</strong>
}
inStream.close();
accessFile.close();
if(isInterrupted){
Log.i("download", "线程id:" + threadid + "下载停止");
}else{
Log.i("download", "线程id:" + threadid + "下载完成");
}
} catch (Exception e) {
e.printStackTrace();
}
}
我是在应用退到后台,就让停止下载的,不为什么,就是不想多写那个button,需要的可以自己写。
这里,我通过线程池的shutdownNow()来尝试中断所有线程的,其实也不是中断,只是在调用了这个方法之后,线程里的Thread.interrupted()方法就返回true了,然后我就通过break;来退出循环,从而达到中断下载的目的。
@Override
protected void onStop() {
super.onStop();
// 应用退到后台的时候就暂停下载
pool.shutdownNow();
dialog.dismiss();
}
接口回调
更新进度到数据库,理论上来说进度不应该实时更新的,sqlite本质上也是文件,频繁的打开关闭文件太耗资源了,所以在实际项目中应该在用户暂停或者断网等特殊情况才更新进度
@Override
public void onDownload(int threadId, int process, int completed, String url) {
// 更新进度到数据库,理论上来说进度不应该实时更新的,
//sqlite本质上也是文件,频繁的打开关闭文件太耗资源了,
//所以在实际项目中应该在用户暂停或者断网等特殊情况才更新进度
DBService.getInstance(getApplicationContext()).updataInfos(threadId,
completed, url);
Message msg = handler.obtainMessage();
msg.arg1 = process;
handler.sendMessage(msg);
}
DBService.java
package com.huxq.multhreaddownload;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
public class DBService {
private DBHelper dbHelper;
private static DBService instance;
private DBService(Context context) {
dbHelper = new DBHelper(context);
}
/**
* 单例模式,不必每次使用都重新new
*
* @param context
* @return
*/
public static DBService getInstance(Context context) {
if (instance == null) {
synchronized (DBService.class) {
if (instance == null) {
instance = new DBService(context);
return instance;
}
}
}
return instance;
}
/**
* 查看数据库中是否有数据
*/
public boolean isHasInfors(String urlstr) {
SQLiteDatabase database = dbHelper.getReadableDatabase();
String sql = "select count(*) from download_info where url=?";
Cursor cursor = database.rawQuery(sql, new String[] { urlstr });
cursor.moveToFirst();
int count = cursor.getInt(0);
Log.i("count", "count=" + count);
cursor.close();
return count == 0;
}
/**
* 保存下载的具体信息
*/
public void saveInfos(List<DownloadInfo> infos) {
SQLiteDatabase database = dbHelper.getWritableDatabase();
for (DownloadInfo info : infos) {
String sql = "insert into download_info(thread_id,start_pos,"
+ " end_pos,compelete_size,url) values (?,?,?,?,?)";
Object[] bindArgs = { info.getThreadId(), info.getStartPos(),
info.getEndPos(), info.getCompeleteSize(), info.getUrl() };
database.execSQL(sql, bindArgs);
}
}
/**
* 得到下载具体信息
*/
public List<DownloadInfo> getInfos(String urlstr) {
List<DownloadInfo> list = new ArrayList<DownloadInfo>();
SQLiteDatabase database = dbHelper.getReadableDatabase();
String sql = "select thread_id, start_pos, end_pos,compelete_size,url"
+ " from download_info where url=?";
Cursor cursor = database.rawQuery(sql, new String[] { urlstr });
while (cursor.moveToNext()) {
DownloadInfo info = new DownloadInfo(cursor.getInt(0),
cursor.getInt(1), cursor.getInt(2), cursor.getInt(3),
cursor.getString(4));
list.add(info);
}
cursor.close();
return list;
}
/**
* 获取特定ID的线程已下载的进度
*
* @param id
* @param url
* @return
*/
public synchronized int getInfoByIdAndUrl(int id, String url) {
SQLiteDatabase database = dbHelper.getReadableDatabase();
String sql = "select compelete_size"
+ " from download_info where thread_id=? and url=?";
Cursor cursor = database.rawQuery(sql, new String[] { id + "", url });
if (cursor!=null&&cursor.moveToFirst()) {
Log.i("count",
"thread id="
+ id
+ "completed="
+ cursor.getInt(0));
return cursor.getInt(0);
}
return 0;
}
/**
* 更新数据库中的下载信息
*/
public synchronized void updataInfos(int threadId, int compeleteSize, String urlstr) {
SQLiteDatabase database = dbHelper.getReadableDatabase();
// 如果存在就更新,不存在就插入
String sql = "replace into download_info"
+ "(compelete_size,thread_id,url) values(?,?,?)";
Object[] bindArgs = { compeleteSize, threadId, urlstr };
database.execSQL(sql, bindArgs);
}
/**
* 关闭数据库
*/
public void closeDb() {
dbHelper.close();
}
/**
* 下载完成后删除数据库中的数据
*/
public void delete(String url) {
SQLiteDatabase database = dbHelper.getReadableDatabase();
int count = database.delete("download_info", "url=?", new String[] { url });
Log.i("delete", "delete count="+count);
database.close();
}
public void saveOrUpdateInfos() {
}
public synchronized void deleteByIdAndUrl(int id, String url) {
SQLiteDatabase database = dbHelper.getReadableDatabase();
int count = database.delete("download_info", "thread_id=? and url=?", new String[] {
id + "", url });
Log.i("delete", "delete id="+id+","+"count="+count);
database.close();
}
}
写这些东西也花了我点时间,因为牵扯到的东西也不少,最后我会贴出 DEMO,有兴趣的可以看看,如有疑问,欢迎留言或者联系我,一起探讨。