Android/java http多線程斷點下載(附源碼)
先看下項目結構:
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> headerMap = conn.getHeaderFields();Iterator iterator = headerMap.keySet().iterator();while (iterator.hasNext()) {String key = iterator.next();List 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();// 計算每條線程下載的資料長度int block = filelength % threadsize == 0 ? filelength / threadsize: filelength / threadsize + 1;// 判斷是不是第一次下載,不是就計算已經下載了多少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、Google推薦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);//當線程被暗示需要中斷以後,退出迴圈,終止下載操作if(Thread.interrupted()){isInterrupted=true;break;}}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 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 getInfos(String urlstr) {List list = new ArrayList();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,有興趣的可以看看,如有疑問,歡迎留言或者聯絡我,一起探討。