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

來源:互聯網
上載者:User

標籤:多線程   斷點續傳   下載   

    轉載請註明出處:http://blog.csdn.net/zhaokaiqiang1992

    上班第一天,在技術群裡面和大家閑扯,無意中談到了關於架構的使用,一個同學說為了用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表格儲存體下載資訊 */@Overridepublic 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)");}@Overridepublic 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;// 下載器idprivate 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;}@Overridepublic 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;}@Overridepublic 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() {@Overridepublic 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>() {@Overrideprotected Void doInBackground(Void... arg0) {mDownloadHttpTool.ready();return null;}    @Overrideprotected 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() {@Overridepublic void downloadStart(int fileSize) {max = fileSize;mProgressBar.setMax(fileSize);}@Overridepublic void downloadProgress(int downloadedSize) {mProgressBar.setProgress(downloadedSize);total.setText((int) downloadedSize * 100 / max + "%");}@Overridepublic void downloadEnd() {Bitmap bitmap = decodeSampledBitmapFromResource(localPath+ File.separator + "abc.jpg", 200, 200);image.setImageBitmap(bitmap);}});


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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.