先看下项目结构:
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,有兴趣的可以看看,如有疑问,欢迎留言或者联系我,一起探讨。
时间: 2024-10-12 20:57:27