先看下项目结构:

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的时候显示dialogif (done == 100) {// 提示用户下载完成// 线程下载完成以后就删除在数据库的缓存数据DBService.getInstance(getApplicationContext()).delete(path);// 做一个延时的效果,可以让用户多看一会100%Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic 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数组,他的长度影响到下载速度的快慢

@Overridepublic 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;来退出循环,从而达到中断下载的目的。

@Overrideprotected void onStop() {super.onStop();// 应用退到后台的时候就暂停下载pool.shutdownNow();dialog.dismiss();}
接口回调

更新进度到数据库,理论上来说进度不应该实时更新的,sqlite本质上也是文件,频繁的打开关闭文件太耗资源了,所以在实际项目中应该在用户暂停或者断网等特殊情况才更新进度

@Overridepublic 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,有兴趣的可以看看,如有疑问,欢迎留言或者联系我,一起探讨。

更多相关文章

  1. SpringBoot 2.0 中 HikariCP 数据库连接池原理解析
  2. Android(安卓)依赖注入:Dagger 实例讲解(Demo下载)
  3. Android中如何实现定时任务
  4. 下载Android单个项目源码的方法
  5. 如何在eclipse中添加android ADT
  6. 如何解决Android(安卓)SDK无法下载Package的问题(.net)
  7. Android消息处理机制①
  8. Android(安卓)线程处理
  9. 【多线程】七、阻塞队列

随机推荐

  1. Sql学习第一天——SQL 将变量定义为Table
  2. Sql学习第一天——SQL 练习题(建表/sql语
  3. Sql学习第一天——SQL UNION 和 UNION AL
  4. sqlserver附加.mdf权限问题解决
  5. SQL普通表转分区表的方法
  6. 游标删除多个表里脏数据的方法
  7. SQL重复记录查询的几种方法
  8. SQL直接操作excel表(查询/导入/插入)
  9. mssql 高效的分页存储过程分享
  10. oracle,mysql,SqlServer三种数据库的分页