1. 程式人生 > >【Android開發經驗】關於“多執行緒斷點續傳下載”功能的一個簡單實現和講解

【Android開發經驗】關於“多執行緒斷點續傳下載”功能的一個簡單實現和講解

    上班第一天,在技術群裡面和大家閒扯,無意中談到了關於框架的使用,一個同學說為了用xUtils的斷線續傳下載功能,把整個庫引入到了專案中,在google的官方建議中,是非常不建議這種做法的,集合框架雖然把很多功能整合起來,但是程式碼越多,出現問題的可能越大,而且無形之中增加了APK的大小,因此,得不償失。所以,這篇文章主要就“斷線續傳”下載功能,簡單的說下思路和程式碼實現,因為這類程式碼比較多,所以找了一個寫的不錯的demo,簡單優化了一下。

    在貼程式碼之前,我們先分析一下需求和解決思路。首先是下載功能,我們簡單的使用HttpURLConnection就可以了,沒有引入框架的必要,然後就是斷點續傳了,其實斷點續傳指的就是我們可以隨時停止我們的下載任務,當下次再次開始的時候,可以從上次下載到的位置繼續下載,節省下載時間,很方便也很實用,做法無非就是在下載的過程中,紀錄下下載到的位置,當再次開始下載的時候,我們從上一次的位置繼續請求伺服器即可。說到這裡,有個類不得不提,那就是RandomAccessFile,這個類是實現斷點續傳功能的核心類,RandomAccessFile允許我們從我們想要的位置進行讀寫操作,因此,我們可以把我們要下載的檔案切分成幾部分,然後開啟多個執行緒,分別從檔案不同的位置進行下載,這樣等所有的部分都下載完成之後,我們就能夠得到一個完整的檔案了,這就是多執行緒下載的原理,完成上面幾個步驟,我們的多執行緒斷線續傳下載功能就基本完成了,下面是在網上找的一個Demo,我對程式碼進行了部分修改,從程式碼裡面,我們看一下如何進行程式碼的實現。

    首先,如果要實現斷點續傳,我們就要紀錄每個執行緒下載的檔案的位置,可以使用檔案,也可以使用sp,也可以使用DB,這個Demo裡面使用的DB,我們首先看一下資料庫的Helper實現類,裡面儲存主鍵、執行緒號、開始位置、結束位置、完成位置和下載地址即可。

    DownLoadHelper.java

public class DownLoadHelper extends SQLiteOpenHelper {

	private static final String SQL_NAME = "download.db";
	private static final int DOWNLOAD_VERSION = 1;

	public DownLoadHelper(Context context) {
		super(context, SQL_NAME, null, DOWNLOAD_VERSION);
	}

	/**
	 * 在download.db資料庫下建立一個download_info表儲存下載資訊
	 */
	@Override
	public void onCreate(SQLiteDatabase db) {
		db.execSQL("create table download_info(_id integer PRIMARY KEY AUTOINCREMENT, thread_id integer, "
				+ "start_pos integer, end_pos integer, compelete_size integer,url char)");
	}

	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

	}

}

    有了Helper之後,我們在建立一個Sql工具類,完成對錶的資料操作

    DownlaodSqlTool.java

public class DownlaodSqlTool {

	private DownLoadHelper dbHelper;

	public DownlaodSqlTool(Context context) {
		dbHelper = new DownLoadHelper(context);
	}

	/**
	 * 建立下載的具體資訊
	 */
	public void insertInfos(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.getWritableDatabase();
		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);
		}
		return list;
	}

	/**
	 * 更新資料庫中的下載資訊
	 */
	public void updataInfos(int threadId, int compeleteSize, String urlstr) {
		SQLiteDatabase database = dbHelper.getWritableDatabase();
		String sql = "update download_info set compelete_size=? where thread_id=? and url=?";
		Object[] bindArgs = { compeleteSize, threadId, urlstr };
		database.execSQL(sql, bindArgs);
	}

	/**
	 * 關閉資料庫
	 */
	public void closeDb() {
		dbHelper.close();
	}

	/**
	 * 下載完成後刪除資料庫中的資料
	 */
	public void delete(String url) {
		SQLiteDatabase database = dbHelper.getWritableDatabase();
		database.delete("download_info", "url=?", new String[] { url });
	}
}

    資料庫相關的類就這些,斷點續傳的功能已經完成,下面看下載如何實現。

    首先,為了操作方便,我們對下載的檔案抽取實體類

    DownloadInfo.java

public class DownloadInfo {

	private int threadId;// 下載器id
	private int startPos;// 開始點
	private int endPos;// 結束點
	private int compeleteSize;// 完成度
	private String url;// 下載檔案的URL地址

	public DownloadInfo(int threadId, int startPos, int endPos,
			int compeleteSize, String url) {
		this.threadId = threadId;
		this.startPos = startPos;
		this.endPos = endPos;
		this.compeleteSize = compeleteSize;
		this.url = url;
	}

	public DownloadInfo() {
	}

	public String getUrl() {
		return url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public int getThreadId() {
		return threadId;
	}

	public void setThreadId(int threadId) {
		this.threadId = threadId;
	}

	public int getStartPos() {
		return startPos;
	}

	public void setStartPos(int startPos) {
		this.startPos = startPos;
	}

	public int getEndPos() {
		return endPos;
	}

	public void setEndPos(int endPos) {
		this.endPos = endPos;
	}

	public int getCompeleteSize() {
		return compeleteSize;
	}

	public void setCompeleteSize(int compeleteSize) {
		this.compeleteSize = compeleteSize;
	}

	@Override
	public String toString() {
		return "DownloadInfo [threadId=" + threadId + ", startPos=" + startPos
				+ ", endPos=" + endPos + ", compeleteSize=" + compeleteSize
				+ "]";
	}
}

    實體類抽取之後,我們就可以實現下載功能了,DownloadHttpTool是實現下載功能的主要類

    DownloadHttpTool.java

public class DownloadHttpTool {

	private static final String TAG = DownloadHttpTool.class.getSimpleName();
	// 執行緒數量
	private int threadCount;
	// URL地址
	private String urlstr;
	private Context mContext;
	private Handler mHandler;
	// 儲存下載資訊的類
	private List<DownloadInfo> downloadInfos;
	// 目錄
	private String localPath;
	// 檔名
	private String fileName;
	private int fileSize;
	// 檔案資訊儲存的資料庫操作類
	private DownlaodSqlTool sqlTool;

	// 利用列舉表示下載的三種狀態
	private enum Download_State {
		Downloading, Pause, Ready, Delete;
	}

	// 當前下載狀態
	private Download_State state = Download_State.Ready;
	// 所有執行緒下載的總數
	private int globalCompelete = 0;

	public DownloadHttpTool(int threadCount, String urlString,
			String localPath, String fileName, Context context, Handler handler) {
		super();
		this.threadCount = threadCount;
		this.urlstr = urlString;
		this.localPath = localPath;
		this.mContext = context;
		this.mHandler = handler;
		this.fileName = fileName;
		sqlTool = new DownlaodSqlTool(mContext);
	}

	// 在開始下載之前需要呼叫ready方法進行配置
	public void ready() {
		Log.w(TAG, "ready");
		globalCompelete = 0;
		downloadInfos = sqlTool.getInfos(urlstr);
		if (downloadInfos.size() == 0) {
			initFirst();
		} else {
			File file = new File(localPath + "/" + fileName);
			if (!file.exists()) {
				sqlTool.delete(urlstr);
				initFirst();
			} else {
				fileSize = downloadInfos.get(downloadInfos.size() - 1)
						.getEndPos();
				for (DownloadInfo info : downloadInfos) {
					globalCompelete += info.getCompeleteSize();
				}
				Log.w(TAG, "globalCompelete:::" + globalCompelete);
			}
		}
	}

	public void start() {
		Log.w(TAG, "start");
		if (downloadInfos != null) {
			if (state == Download_State.Downloading) {
				return;
			}
			state = Download_State.Downloading;
			for (DownloadInfo info : downloadInfos) {
				Log.v(TAG, "startThread");
				new DownloadThread(info.getThreadId(), info.getStartPos(),
						info.getEndPos(), info.getCompeleteSize(),
						info.getUrl()).start();
			}
		}
	}

	public void pause() {
		state = Download_State.Pause;
		sqlTool.closeDb();
	}

	public void delete() {
		state = Download_State.Delete;
		compelete();
		new File(localPath + File.separator + fileName).delete();
	}

	public void compelete() {
		sqlTool.delete(urlstr);
		sqlTool.closeDb();
	}

	public int getFileSize() {
		return fileSize;
	}

	public int getCompeleteSize() {
		return globalCompelete;
	}

	/**
	 * 第一次下載初始化
	 */
	private void initFirst() {
		Log.w(TAG, "initFirst");
		try {
			URL url = new URL(urlstr);
			HttpURLConnection connection = (HttpURLConnection) url
					.openConnection();
			connection.setConnectTimeout(5000);
			connection.setRequestMethod("GET");
			fileSize = connection.getContentLength();
			Log.w(TAG, "fileSize::" + fileSize);
			File fileParent = new File(localPath);
			if (!fileParent.exists()) {
				fileParent.mkdir();
			}
			File file = new File(fileParent, fileName);
			if (!file.exists()) {
				file.createNewFile();
			}
			// 本地訪問檔案
			RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");
			accessFile.setLength(fileSize);
			accessFile.close();
			connection.disconnect();
		} catch (Exception e) {
			e.printStackTrace();
		}
		int range = fileSize / threadCount;
		downloadInfos = new ArrayList<DownloadInfo>();
		for (int i = 0; i < threadCount - 1; i++) {
			DownloadInfo info = new DownloadInfo(i, i * range, (i + 1) * range
					- 1, 0, urlstr);
			downloadInfos.add(info);
		}
		DownloadInfo info = new DownloadInfo(threadCount - 1, (threadCount - 1)
				* range, fileSize - 1, 0, urlstr);
		downloadInfos.add(info);
		sqlTool.insertInfos(downloadInfos);
	}

	/**
	 * 自定義下載執行緒
	 * 
	 * @author zhaokaiqiang
	 * @time 2015-2-25下午5:52:28
	 */
	private class DownloadThread extends Thread {

		private int threadId;
		private int startPos;
		private int endPos;
		private int compeleteSize;
		private String urlstr;
		private int totalThreadSize;

		public DownloadThread(int threadId, int startPos, int endPos,
				int compeleteSize, String urlstr) {
			this.threadId = threadId;
			this.startPos = startPos;
			this.endPos = endPos;
			totalThreadSize = endPos - startPos + 1;
			this.urlstr = urlstr;
			this.compeleteSize = compeleteSize;
		}

		@Override
		public void run() {
			HttpURLConnection connection = null;
			RandomAccessFile randomAccessFile = null;
			InputStream is = null;
			try {
				randomAccessFile = new RandomAccessFile(localPath
						+ File.separator + fileName, "rwd");
				randomAccessFile.seek(startPos + compeleteSize);
				URL url = new URL(urlstr);
				connection = (HttpURLConnection) url.openConnection();
				connection.setConnectTimeout(5000);
				connection.setRequestMethod("GET");
				connection.setRequestProperty("Range", "bytes="
						+ (startPos + compeleteSize) + "-" + endPos);
				is = connection.getInputStream();
				byte[] buffer = new byte[1024];
				int length = -1;
				while ((length = is.read(buffer)) != -1) {
					randomAccessFile.write(buffer, 0, length);
					compeleteSize += length;
					Message message = Message.obtain();
					message.what = threadId;
					message.obj = urlstr;
					message.arg1 = length;
					mHandler.sendMessage(message);
					Log.w(TAG, "Threadid::" + threadId + "    compelete::"
							+ compeleteSize + "    total::" + totalThreadSize);
					// 當程式不再是下載狀態的時候,紀錄當前的下載進度
					if ((state != Download_State.Downloading)
							|| (compeleteSize >= totalThreadSize)) {
						sqlTool.updataInfos(threadId, compeleteSize, urlstr);
						break;
					}
				}

			} catch (Exception e) {
				e.printStackTrace();
				sqlTool.updataInfos(threadId, compeleteSize, urlstr);
			} finally {
				try {
					if (is != null) {
						is.close();
					}
					randomAccessFile.close();
					connection.disconnect();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}
}

    在上面的程式碼中,我們定義了一個執行緒,進行檔案的多執行緒下載,並且在退出下載狀態和完成下載的時候,紀錄下載的位置,存到資料庫中。在原來的程式碼中,是沒獲取一次資料,存取一次資料庫,大大的增加了資料庫的操作頻率,降低了效率,原先下載420k左右的檔案,需要操作420次資料庫,現在只要一次即可。

    在initFirst()裡面,首先進行了初始化,根據下載檔案的大小和開啟執行緒的數量,對下載實體類進行了初始化和賦值。在RandomAccessFile建立完畢,DownloadInfo初始化完畢之後,就可以通過start()進行檔案的下載了。

    其實到這裡,基本的功能已經實現了。為了使得我們的操作更加的方便,同時可以監控到下載的進度,我們對下載類進行一次封裝,程式碼如下:

    DownloadUtil.java

public class DownloadUtil {

	private DownloadHttpTool mDownloadHttpTool;
	private OnDownloadListener onDownloadListener;

	private int fileSize;
	private int downloadedSize = 0;

	@SuppressLint("HandlerLeak")
	private Handler mHandler = new Handler() {

		@Override
		public void handleMessage(Message msg) {
			int length = msg.arg1;
			synchronized (this) {// 加鎖保證已下載的正確性
				downloadedSize += length;

			}
			if (onDownloadListener != null) {
				onDownloadListener.downloadProgress(downloadedSize);
			}
			if (downloadedSize >= fileSize) {
				mDownloadHttpTool.compelete();
				if (onDownloadListener != null) {
					onDownloadListener.downloadEnd();
				}
			}
		}

	};

	public DownloadUtil(int threadCount, String filePath, String filename,
			String urlString, Context context) {

		mDownloadHttpTool = new DownloadHttpTool(threadCount, urlString,
				filePath, filename, context, mHandler);
	}

	// 下載之前首先非同步執行緒呼叫ready方法獲得檔案大小資訊,之後呼叫開始方法
	public void start() {
		new AsyncTask<Void, Void, Void>() {

			@Override
			protected Void doInBackground(Void... arg0) {
				mDownloadHttpTool.ready();
				return null;
			}
    
			@Override
			protected void onPostExecute(Void result) {
				fileSize = mDownloadHttpTool.getFileSize();
				downloadedSize = mDownloadHttpTool.getCompeleteSize();
				Log.w("Tag", "downloadedSize::" + downloadedSize);
				if (onDownloadListener != null) {
					onDownloadListener.downloadStart(fileSize);
				}
				mDownloadHttpTool.start();
			}
		}.execute();
	}

	public void pause() {
		mDownloadHttpTool.pause();
	}

	public void delete() {
		mDownloadHttpTool.delete();
	}

	public void reset() {
		mDownloadHttpTool.delete();
		start();
	}

	public void setOnDownloadListener(OnDownloadListener onDownloadListener) {
		this.onDownloadListener = onDownloadListener;
	}

	// 下載回撥介面
	public interface OnDownloadListener {
		public void downloadStart(int fileSize);

		public void downloadProgress(int downloadedSize);

		public void downloadEnd();
	}
}

      通過對外暴露介面,我們可以實現下載進度的監聽了!

    用的時候也很簡單,像下面這樣就ok了

String urlString = "http://bbra.cn/Uploadfiles/imgs/20110303/fengjin/013.jpg";
		final String localPath = Environment.getExternalStorageDirectory()
				.getAbsolutePath() + "/ADownLoadTest";
		mDownloadUtil = new DownloadUtil(2, localPath, "abc.jpg", urlString,
				this);
		mDownloadUtil.setOnDownloadListener(new OnDownloadListener() {

			@Override
			public void downloadStart(int fileSize) {
				max = fileSize;
				mProgressBar.setMax(fileSize);
			}

			@Override
			public void downloadProgress(int downloadedSize) {
				mProgressBar.setProgress(downloadedSize);
				total.setText((int) downloadedSize * 100 / max + "%");
			}

			@Override
			public void downloadEnd() {
				Bitmap bitmap = decodeSampledBitmapFromResource(localPath
						+ File.separator + "abc.jpg", 200, 200);
				image.setImageBitmap(bitmap);
			}
		});