一、原理

其实断点续传的原理很简单,从字面上理解,所谓断点续传就是从停止的地方重新下载。
断点:线程停止的位置。
续传:从停止的位置重新下载。

用代码解析就是:
断点 : 当前线程已经下载完成的数据长度。
续传 : 向服务器请求上次线程停止位置之后的数据。
原理知道了,功能实现起来也简单。每当线程停止时就把已下载的数据长度写入记录文件,当重新下载时,从记录文件读取已经下载了的长度。而这个长度就是所需要的断点。

续传的实现也简单,可以通过设置网络请求参数,请求服务器从指定的位置开始读取数据。
而要实现这两个功能只需要使用到httpURLconnection里面的setRequestProperty方法便可以实现.

  1. public void setRequestProperty(String field, String newValue)

如下所示,便是向服务器请求500-1000之间的500个byte:

  1. conn.setRequestProperty("Range", "bytes=" + 500 + "-" + 1000);

以上只是续传的一部分需求,当我们获取到下载数据时,还需要将数据写入文件,而普通发File对象并不提供从指定位置写入数据的功能,这个时候,就需要使用到RandomAccessFile来实现从指定位置给文件写入数据的功能。

  1. public void seek(long offset)

如下所示,便是从文件的的第100个byte后开始写入数据。

  1. raFile.seek(100);

而开始写入数据时还需要用到RandomAccessFile里面的另外一个方法

  1. public void write(byte[] buffer, int byteOffset, int byteCount)

该方法的使用和OutputStream的write的使用一模一样…

以上便是断点续传的原理。

二、多线程断点续传

多线程断点续传便是在单线程的断点续传上延伸的。多线程断点续传是把整个文件分割成几个部分,每个部分由一条线程执行下载,而每一条下载线程都要实现断点续传功能。
为了实现文件分割功能,我们需要使用到httpURLconnection的另外一个方法:

  1. public int getContentLength()

当请求成功时,可以通过该方法获取到文件的总长度。
每一条线程下载大小 = fileLength / THREAD_NUM

如下图所示,描述的便是多线程的下载模型:

Android多线程断点续传 - 图1

在多线程断点续传下载中,有一点需要特别注意:
由于文件是分成多个部分是被不同的线程的同时下载的,这就需要,每一条线程都分别需要有一个断点记录,和一个线程完成状态的记录;

Android多线程断点续传 - 图2

只有所有线程的下载状态都处于完成状态时,才能表示文件已经下载完成。
实现记录的方法多种多样,我这里采用的是JDK自带的Properties类来记录下载参数。

三、断点续传结构

通过原理的了解,便可以很快的设计出断点续传工具类的基本结构图

Android多线程断点续传 - 图3

IDownloadListener.java

  1. package com.arialyy.frame.http.inf;
  2. import java.net.HttpURLConnection;
  3. /**
  4. * 在这里面编写你的业务逻辑
  5. */
  6. public interface IDownloadListener {
  7. /**
  8. * 取消下载
  9. */
  10. public void onCancel();
  11. /**
  12. * 下载失败
  13. */
  14. public void onFail();
  15. /**
  16. * 下载预处理,可通过HttpURLConnection获取文件长度
  17. */
  18. public void onPreDownload(HttpURLConnection connection);
  19. /**
  20. * 下载监听
  21. */
  22. public void onProgress(long currentLocation);
  23. /**
  24. * 单一线程的结束位置
  25. */
  26. public void onChildComplete(long finishLocation);
  27. /**
  28. * 开始
  29. */
  30. public void onStart(long startLocation);
  31. /**
  32. * 子程恢复下载的位置
  33. */
  34. public void onChildResume(long resumeLocation);
  35. /**
  36. * 恢复位置
  37. */
  38. public void onResume(long resumeLocation);
  39. /**
  40. * 停止
  41. */
  42. public void onStop(long stopLocation);
  43. /**
  44. * 下载完成
  45. */
  46. public void onComplete();
  47. }

该类是下载监听接口

DownloadListener.java

  1. import java.net.HttpURLConnection;
  2. /**
  3. * 下载监听
  4. */
  5. public class DownloadListener implements IDownloadListener {
  6. @Override
  7. public void onResume(long resumeLocation) {
  8. }
  9. @Override
  10. public void onCancel() {
  11. }
  12. @Override
  13. public void onFail() {
  14. }
  15. @Override
  16. public void onPreDownload(HttpURLConnection connection) {
  17. }
  18. @Override
  19. public void onProgress(long currentLocation) {
  20. }
  21. @Override
  22. public void onChildComplete(long finishLocation) {
  23. }
  24. @Override
  25. public void onStart(long startLocation) {
  26. }
  27. @Override
  28. public void onChildResume(long resumeLocation) {
  29. }
  30. @Override
  31. public void onStop(long stopLocation) {
  32. }
  33. @Override
  34. public void onComplete() {
  35. }
  36. }

下载参数实体

  1. /**
  2. * 子线程下载信息类
  3. */
  4. private class DownloadEntity {
  5. //文件总长度
  6. long fileSize;
  7. //下载链接
  8. String downloadUrl;
  9. //线程Id
  10. int threadId;
  11. //起始下载位置
  12. long startLocation;
  13. //结束下载的文章
  14. long endLocation;
  15. //下载文件
  16. File tempFile;
  17. Context context;
  18. public DownloadEntity(Context context, long fileSize, String downloadUrl, File file, int threadId, long startLocation, long endLocation) {
  19. this.fileSize = fileSize;
  20. this.downloadUrl = downloadUrl;
  21. this.tempFile = file;
  22. this.threadId = threadId;
  23. this.startLocation = startLocation;
  24. this.endLocation = endLocation;
  25. this.context = context;
  26. }
  27. }

该类是下载信息配置类,每一条子线程的下载都需要一个下载实体来配置下载信息。

下载任务线程

  1. /**
  2. * 多线程下载任务类
  3. */
  4. private class DownLoadTask implements Runnable {
  5. private static final String TAG = "DownLoadTask";
  6. private DownloadEntity dEntity;
  7. private String configFPath;
  8. public DownLoadTask(DownloadEntity downloadInfo) {
  9. this.dEntity = downloadInfo;
  10. configFPath = dEntity.context.getFilesDir().getPath() + "/temp/" + dEntity.tempFile.getName() + ".properties";
  11. }
  12. @Override
  13. public void run() {
  14. try {
  15. L.d(TAG, "线程_" + dEntity.threadId + "_正在下载【" + "开始位置 : " + dEntity.startLocation + ",结束位置:" + dEntity.endLocation + "】");
  16. URL url = new URL(dEntity.downloadUrl);
  17. HttpURLConnection conn = (HttpURLConnection) url.openConnection();
  18. //在头里面请求下载开始位置和结束位置
  19. conn.setRequestProperty("Range", "bytes=" + dEntity.startLocation + "-" + dEntity.endLocation);
  20. conn.setRequestMethod("GET");
  21. conn.setRequestProperty("Charset", "UTF-8");
  22. conn.setConnectTimeout(TIME_OUT);
  23. 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)");
  24. 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, */*");
  25. conn.setReadTimeout(2000); //设置读取流的等待时间,必须设置该参数
  26. InputStream is = conn.getInputStream();
  27. //创建可设置位置的文件
  28. RandomAccessFile file = new RandomAccessFile(dEntity.tempFile, "rwd");
  29. //设置每条线程写入文件的位置
  30. file.seek(dEntity.startLocation);
  31. byte[] buffer = new byte[1024];
  32. int len;
  33. //当前子线程的下载位置
  34. long currentLocation = dEntity.startLocation;
  35. while ((len = is.read(buffer)) != -1) {
  36. if (isCancel) {
  37. L.d(TAG, "++++++++++ thread_" + dEntity.threadId + "_cancel ++++++++++");
  38. break;
  39. }
  40. if (isStop) {
  41. break;
  42. }
  43. //把下载数据数据写入文件
  44. file.write(buffer, 0, len);
  45. synchronized (DownLoadUtil.this) {
  46. mCurrentLocation += len;
  47. mListener.onProgress(mCurrentLocation);
  48. }
  49. currentLocation += len;
  50. }
  51. file.close();
  52. is.close();
  53. if (isCancel) {
  54. synchronized (DownLoadUtil.this) {
  55. mCancelNum++;
  56. if (mCancelNum == THREAD_NUM) {
  57. File configFile = new File(configFPath);
  58. if (configFile.exists()) {
  59. configFile.delete();
  60. }
  61. if (dEntity.tempFile.exists()) {
  62. dEntity.tempFile.delete();
  63. }
  64. L.d(TAG, "++++++++++++++++ onCancel +++++++++++++++++");
  65. isDownloading = false;
  66. mListener.onCancel();
  67. System.gc();
  68. }
  69. }
  70. return;
  71. }
  72. //停止状态不需要删除记录文件
  73. if (isStop) {
  74. synchronized (DownLoadUtil.this) {
  75. mStopNum++;
  76. String location = String.valueOf(currentLocation);
  77. L.i(TAG, "thread_" + dEntity.threadId + "_stop, stop location ==> " + currentLocation);
  78. writeConfig(dEntity.tempFile.getName() + "_record_" + dEntity.threadId, location);
  79. if (mStopNum == THREAD_NUM) {
  80. L.d(TAG, "++++++++++++++++ onStop +++++++++++++++++");
  81. isDownloading = false;
  82. mListener.onStop(mCurrentLocation);
  83. System.gc();
  84. }
  85. }
  86. return;
  87. }
  88. L.i(TAG, "线程【" + dEntity.threadId + "】下载完毕");
  89. writeConfig(dEntity.tempFile.getName() + "_state_" + dEntity.threadId, 1 + "");
  90. mListener.onChildComplete(dEntity.endLocation);
  91. mCompleteThreadNum++;
  92. if (mCompleteThreadNum == THREAD_NUM) {
  93. File configFile = new File(configFPath);
  94. if (configFile.exists()) {
  95. configFile.delete();
  96. }
  97. mListener.onComplete();
  98. isDownloading = false;
  99. System.gc();
  100. }
  101. } catch (MalformedURLException e) {
  102. e.printStackTrace();
  103. isDownloading = false;
  104. mListener.onFail();
  105. } catch (IOException e) {
  106. FL.e(this, "下载失败【" + dEntity.downloadUrl + "】" + FL.getPrintException(e));
  107. isDownloading = false;
  108. mListener.onFail();
  109. } catch (Exception e) {
  110. FL.e(this, "获取流失败" + FL.getPrintException(e));
  111. isDownloading = false;
  112. mListener.onFail();
  113. }
  114. }

这个是每条下载子线程的下载任务类,子线程通过下载实体对每一条线程进行下载配置,由于在多断点续传的概念里,停止表示的是暂停状态,而恢复表示的是线程从记录的断点重新进行下载,所以,线程处于停止状态时是不能删除记录文件的。

下载入口

  1. /**
  2. * 多线程断点续传下载文件,暂停和继续
  3. *
  4. * @param context 必须添加该参数,不能使用全局变量的context
  5. * @param downloadUrl 下载路径
  6. * @param filePath 保存路径
  7. * @param downloadListener 下载进度监听 {@link DownloadListener}
  8. */
  9. public void download(final Context context, @NonNull final String downloadUrl, @NonNull final String filePath,
  10. @NonNull final DownloadListener downloadListener) {
  11. isDownloading = true;
  12. mCurrentLocation = 0;
  13. isStop = false;
  14. isCancel = false;
  15. mCancelNum = 0;
  16. mStopNum = 0;
  17. final File dFile = new File(filePath);
  18. //读取已完成的线程数
  19. final File configFile = new File(context.getFilesDir().getPath() + "/temp/" + dFile.getName() + ".properties");
  20. try {
  21. if (!configFile.exists()) { //记录文件被删除,则重新下载
  22. newTask = true;
  23. FileUtil.createFile(configFile.getPath());
  24. } else {
  25. newTask = false;
  26. }
  27. } catch (Exception e) {
  28. e.printStackTrace();
  29. mListener.onFail();
  30. return;
  31. }
  32. newTask = !dFile.exists();
  33. new Thread(new Runnable() {
  34. @Override
  35. public void run() {
  36. try {
  37. mListener = downloadListener;
  38. URL url = new URL(downloadUrl);
  39. HttpURLConnection conn = (HttpURLConnection) url.openConnection();
  40. conn.setRequestMethod("GET");
  41. conn.setRequestProperty("Charset", "UTF-8");
  42. conn.setConnectTimeout(TIME_OUT);
  43. 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)");
  44. 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, */*");
  45. conn.connect();
  46. int len = conn.getContentLength();
  47. if (len < 0) { //网络被劫持时会出现这个问题
  48. mListener.onFail();
  49. return;
  50. }
  51. int code = conn.getResponseCode();
  52. if (code == 200) {
  53. int fileLength = conn.getContentLength();
  54. //必须建一个文件
  55. FileUtil.createFile(filePath);
  56. RandomAccessFile file = new RandomAccessFile(filePath, "rwd");
  57. //设置文件长度
  58. file.setLength(fileLength);
  59. mListener.onPreDownload(conn);
  60. //分配每条线程的下载区间
  61. Properties pro = null;
  62. pro = Util.loadConfig(configFile);
  63. int blockSize = fileLength / THREAD_NUM;
  64. SparseArray<Thread> tasks = new SparseArray<>();
  65. for (int i = 0; i < THREAD_NUM; i++) {
  66. long startL = i * blockSize, endL = (i + 1) * blockSize;
  67. Object state = pro.getProperty(dFile.getName() + "_state_" + i);
  68. if (state != null && Integer.parseInt(state + "") == 1) { //该线程已经完成
  69. mCurrentLocation += endL - startL;
  70. L.d(TAG, "++++++++++ 线程_" + i + "_已经下载完成 ++++++++++");
  71. mCompleteThreadNum++;
  72. if (mCompleteThreadNum == THREAD_NUM) {
  73. if (configFile.exists()) {
  74. configFile.delete();
  75. }
  76. mListener.onComplete();
  77. isDownloading = false;
  78. System.gc();
  79. return;
  80. }
  81. continue;
  82. }
  83. //分配下载位置
  84. Object record = pro.getProperty(dFile.getName() + "_record_" + i);
  85. if (!newTask && record != null && Long.parseLong(record + "") > 0) { //如果有记录,则恢复下载
  86. Long r = Long.parseLong(record + "");
  87. mCurrentLocation += r - startL;
  88. L.d(TAG, "++++++++++ 线程_" + i + "_恢复下载 ++++++++++");
  89. mListener.onChildResume(r);
  90. startL = r;
  91. }
  92. if (i == (THREAD_NUM - 1)) {
  93. endL = fileLength;//如果整个文件的大小不为线程个数的整数倍,则最后一个线程的结束位置即为文件的总长度
  94. }
  95. DownloadEntity entity = new DownloadEntity(context, fileLength, downloadUrl, dFile, i, startL, endL);
  96. DownLoadTask task = new DownLoadTask(entity);
  97. tasks.put(i, new Thread(task));
  98. }
  99. if (mCurrentLocation > 0) {
  100. mListener.onResume(mCurrentLocation);
  101. } else {
  102. mListener.onStart(mCurrentLocation);
  103. }
  104. for (int i = 0, count = tasks.size(); i < count; i++) {
  105. Thread task = tasks.get(i);
  106. if (task != null) {
  107. task.start();
  108. }
  109. }
  110. } else {
  111. FL.e(TAG, "下载失败,返回码:" + code);
  112. isDownloading = false;
  113. System.gc();
  114. mListener.onFail();
  115. }
  116. } catch (IOException e) {
  117. FL.e(this, "下载失败【downloadUrl:" + downloadUrl + "】\n【filePath:" + filePath + "】" + FL.getPrintException(e));
  118. isDownloading = false;
  119. mListener.onFail();
  120. }
  121. }
  122. }).start();
  123. }

其实也没啥好说的,注释已经很完整了,需要注意两点
1、恢复下载时:已下载的文件大小 = 该线程的上一次断点的位置 - 该线程起始下载位置
2、为了保证下载文件的完整性,只要记录文件不存在就需要重新进行下载;

四、最终效果

Android多线程断点续传 - 图4

Demo点我