• 一、原理
  • 二、多线程断点续传
  • 三、断点续传结构
    • IDownloadListener.java
    • DownloadListener.java
  • 下载参数实体
    • 下载任务线程
    • 下载入口
  • 四、最终效果

    一、原理

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

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

    续传的实现也简单,可以通过设置网络请求参数,请求服务器从指定的位置开始读取数据。
    而要实现这两个功能只需要使用到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点我