Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android/java http多線程斷點下載(附源碼)

Android/java http多線程斷點下載(附源碼)

編輯:關於Android編程

先看下項目結構:

\

http多線程斷點下載涉及到 數據庫,多線程和http請求等幾個模塊,東西不是很多,想弄清楚也不是很困難,接下來我和大家分享下我的做法。

一、先看MainActivity.java

成員變量,主要是一些下載過程的變量和handler

<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHByZSBjbGFzcz0="brush:java;"> 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> 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、谷歌推薦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);
				//當線程被暗示需要中斷以後,退出循環,終止下載操作
				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;來退出循環,從而達到中斷下載的目的。

@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 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,有興趣的可以看看,如有疑問,歡迎留言或者聯系我,一起探討。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved