android实现多线程下载文件(支持暂停、取消、断点续传)

多线程下载文件(支持暂停、取消、断点续传)

多线程同时下载文件即:在同一时间内通过多个线程对同一个请求地址发起多个请求,将需要下载的数据分割成多个部分,同时下载,每个线程只负责下载其中的一部分,最后将每一个线程下载的部分组装起来即可。

涉及的知识及问题

  • 请求的数据如何分段
  • 分段完成后如何下载和下载完成后如何组装到一起
  • 暂停下载和继续下载的实现(wait()、notifyAll()、synchronized的使用)
  • 取消下载和断点续传的实现

一、请求的数据如何分段

首先通过HttpURLConnection请求总文件大小,而后根据线程数计算每一个线程的下载量,在分配给每一个线程去下载

fileLength = conn.getContentLength();
//根据文件大小,先创建一个空文件
//“r“——以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。
//“rw“——打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。
//“rws“—— 打开以便读取和写入,对于 “rw”,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
//“rwd“——打开以便读取和写入,对于 “rw”,还要求对文件内容的每个更新都同步写入到底层存储设备。
RandomAccessFile raf = new RandomAccessFile(filePath, "rwd");
raf.setLength(fileLength);
raf.close();
//计算各个线程下载的数据段
int blockLength = fileLength / threadCount;

二、分段完成后如何下载和下载完成后如何组装到一起

分段完成后给每一个线程的请求头设置Range参数,他允许客户端只请求文件的一部分数据,每一个线程只请求下载相应范围内的数据,使用RandomAccessFile(可随机读写的文件)写入到同一个文件里即可组装成目标文件Range,是在 HTTP/1.1里新增的一个 header field,它允许客户端实际上只请求文档的一部分(范围可以相互重叠)

Range的使用形式:

属性 解释
bytes=0-499 表示头500个字节
bytes=500-999 表示第二个500字节
bytes=-500 表示最后500个字节
bytes=500- 表示500字节以后的范围
bytes=0-0,-1 第一个和最后一个字节

HttpUrlConnection中设置请求头

URL url = new URL(loadUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition);
conn.setConnectTimeout(5000);
//若请求头加上Range这个参数,则返回状态码为206,而不是200
if (conn.getResponseCode() == 206) {
  InputStream is = conn.getInputStream();
  RandomAccessFile raf = new RandomAccessFile(filePath, "rwd");
  raf.seek(startPosition);//跳到指定位置开始写数据
}

三、暂停下载和继续下载的实现(wait()、notifyAll()、synchronized的使用)

关于synchronized只需记住一下五点:

  1. 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
  2. 然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
  3. 尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
  4. 第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
  5. 以上规则对其它对象锁同样适用.
 protected void onPause() {
    if (mThreads != null)
      stateDownload = DOWNLOAD_PAUSE;
  }
  protected void onStart() {
    if (mThreads != null)
      synchronized (DOWNLOAD_PAUSE) {
        stateDownload = DOWNLOAD_ING;
        DOWNLOAD_PAUSE.notifyAll();
      }
  }

对于wait()、notify()、notifyAll()需要注意的是

  1. 调用任何对象的wait()方法时,都必须先获得该对象的锁,即调用的wait()方法必须得写在synchronized(obj){…}之内
  2. 当调用对象的wait()方法后,该线程若想继续执行,必须得再次获得该对象的锁才可以
  3. 如果A1,A2,A3线程都在obj.wait(),则B调用object.notify()只能唤醒A1,A2,A3中的一个(具体哪一个由JVM决定)
  4. 当B调用object.notify/notifyAll的时候,B正持有object锁,因此,A1,A2,A3虽被唤醒,但是仍无法获得object锁直到B退出synchronized块,释放object锁后,A1,A2,A3中的一个/全部才有机会获得锁继续执行
  synchronized (DOWNLOAD_PAUSE) {
    if (stateDownload.equals(DOWNLOAD_PAUSE)) {
      DOWNLOAD_PAUSE.wait();
    }
  }

四、取消下载和断点续传的实现

取消下载即取消每个线程的执行,不建议直接使用Thread.stop()方法,安全的取消线程即run方法执行结束。只要控制住循环,就可以让run方法结束,也就是线程结束

  while ((len = is.read(buffer)) != -1) {
    //是否继续下载
    if (!isGoOn)
      break;
  }

断点续传即其实和重新下载是一样的,不过文件的大小和每一个线程下载时的起始位置和结束位置都不是重新计算的。而是上次取消下载时,每一个线程保存的当前位置和结束位置,让每一个线程接着上次的地方继续下载即可

  SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
  //获取上次取消下载的进度,若没有则返回0
  currLength = sp.getInt(CURR_LENGTH, 0);
  for (int i = 0; i < threadCount; i++) {
    //开始位置,获取上次取消下载的进度,默认返回i*blockLength,即第i个线程开始下载的位置
    int startPosition = sp.getInt(SP_NAME + (i + 1), i * blockLength);
    //结束位置,-1是为了防止上一个线程和下一个线程重复下载衔接处数据
    int endPosition = (i + 1) * blockLength - 1;
    //将最后一个线程结束位置扩大,防止文件下载不完全,大了不影响,小了文件失效
    if ((i + 1) == threadCount)
    endPosition = endPosition * 2;
    mThreads[i] = new DownThread(i + 1, startPosition, endPosition);
    mThreads[i].start();
  }

网络获取和读写SD卡都需要添加相应权限

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

下面贴上全部的代码,里面有详细的注释DownLoadFile.Java

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Message;

import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;

/**
 * Created by tianzhao on 2017/2/21 09:25.
 * 多线程下载文件
 */

public class DownLoadFile {

  private static final String SP_NAME = "download_file";
  private static final String CURR_LENGTH = "curr_length";
  private static final int DEFAULT_THREAD_COUNT = 4;//默认下载线程数
  //以下为线程状态
  private static final String DOWNLOAD_INIT = "1";
  private static final String DOWNLOAD_ING = "2";
  private static final String DOWNLOAD_PAUSE = "3";

  private Context mContext;

  private String loadUrl;//网络获取的url
  private String filePath;//下载到本地的path
  private int threadCount = DEFAULT_THREAD_COUNT;//下载线程数

  private int fileLength;//文件总大小
  //使用volatile防止多线程不安全
  private volatile int currLength;//当前总共下载的大小
  private volatile int runningThreadCount;//正在运行的线程数
  private Thread[] mThreads;
  private String stateDownload = DOWNLOAD_INIT;//当前线程状态

  private DownLoadListener mDownLoadListener;

  public void setOnDownLoadListener(DownLoadListener mDownLoadListener) {
    this.mDownLoadListener = mDownLoadListener;
  }

  interface DownLoadListener {
    //返回当前下载进度的百分比
    void getProgress(int progress);

    void onComplete();

    void onFailure();
  }

  public DownLoadFile(Context mContext, String loadUrl, String filePath) {
    this(mContext, loadUrl, filePath, DEFAULT_THREAD_COUNT, null);
  }

  public DownLoadFile(Context mContext, String loadUrl, String filePath, DownLoadListener mDownLoadListener) {
    this(mContext, loadUrl, filePath, DEFAULT_THREAD_COUNT, mDownLoadListener);
  }

  public DownLoadFile(Context mContext, String loadUrl, String filePath, int threadCount) {
    this(mContext, loadUrl, filePath, threadCount, null);
  }

  public DownLoadFile(Context mContext, String loadUrl, String filePath, int threadCount, DownLoadListener mDownLoadListener) {
    this.mContext = mContext;
    this.loadUrl = loadUrl;
    this.filePath = filePath;
    this.threadCount = threadCount;
    runningThreadCount = 0;
    this.mDownLoadListener = mDownLoadListener;
  }

  /**
   * 开始下载
   */
  protected void downLoad() {
    //在线程中运行,防止anr
    new Thread(new Runnable() {
      @Override
      public void run() {
        try {
          //初始化数据
          if (mThreads == null)
            mThreads = new Thread[threadCount];

          //建立连接请求
          URL url = new URL(loadUrl);
          HttpURLConnection conn = (HttpURLConnection) url.openConnection();
          conn.setConnectTimeout(5000);
          conn.setRequestMethod("GET");
          int code = conn.getResponseCode();//获取返回码
          if (code == 200) {//请求成功,根据文件大小开始分多线程下载
            fileLength = conn.getContentLength();
            //根据文件大小,先创建一个空文件
            //“r“——以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。
            //“rw“——打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。
            //“rws“—— 打开以便读取和写入,对于 “rw”,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
            //“rwd“——打开以便读取和写入,对于 “rw”,还要求对文件内容的每个更新都同步写入到底层存储设备。
            RandomAccessFile raf = new RandomAccessFile(filePath, "rwd");
            raf.setLength(fileLength);
            raf.close();
            //计算各个线程下载的数据段
            int blockLength = fileLength / threadCount;

            SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
            //获取上次取消下载的进度,若没有则返回0
            currLength = sp.getInt(CURR_LENGTH, 0);
            for (int i = 0; i < threadCount; i++) {
              //开始位置,获取上次取消下载的进度,默认返回i*blockLength,即第i个线程开始下载的位置
              int startPosition = sp.getInt(SP_NAME + (i + 1), i * blockLength);
              //结束位置,-1是为了防止上一个线程和下一个线程重复下载衔接处数据
              int endPosition = (i + 1) * blockLength - 1;
              //将最后一个线程结束位置扩大,防止文件下载不完全,大了不影响,小了文件失效
              if ((i + 1) == threadCount)
                endPosition = endPosition * 2;

              mThreads[i] = new DownThread(i + 1, startPosition, endPosition);
              mThreads[i].start();
            }
          } else {
            handler.sendEmptyMessage(FAILURE);
          }
        } catch (Exception e) {
          e.printStackTrace();
          handler.sendEmptyMessage(FAILURE);
        }
      }
    }).start();
  }

  /**
   * 取消下载
   */
  protected void cancel() {
    if (mThreads != null) {
      //若线程处于等待状态,则while循环处于阻塞状态,无法跳出循环,必须先唤醒线程,才能执行取消任务
      if (stateDownload.equals(DOWNLOAD_PAUSE))
        onStart();
      for (Thread dt : mThreads) {
        ((DownThread) dt).cancel();
      }
    }
  }

  /**
   * 暂停下载
   */
  protected void onPause() {
    if (mThreads != null)
      stateDownload = DOWNLOAD_PAUSE;
  }

  /**
   * 继续下载
   */
  protected void onStart() {
    if (mThreads != null)
      synchronized (DOWNLOAD_PAUSE) {
        stateDownload = DOWNLOAD_ING;
        DOWNLOAD_PAUSE.notifyAll();
      }
  }

  protected void onDestroy() {
    if (mThreads != null)
      mThreads = null;
  }

  private class DownThread extends Thread {

    private boolean isGoOn = true;//是否继续下载
    private int threadId;
    private int startPosition;//开始下载点
    private int endPosition;//结束下载点
    private int currPosition;//当前线程的下载进度

    private DownThread(int threadId, int startPosition, int endPosition) {
      this.threadId = threadId;
      this.startPosition = startPosition;
      currPosition = startPosition;
      this.endPosition = endPosition;
      runningThreadCount++;
    }

    @Override
    public void run() {
      SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
      try {
        URL url = new URL(loadUrl);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition);
        conn.setConnectTimeout(5000);
        //若请求头加上Range这个参数,则返回状态码为206,而不是200
        if (conn.getResponseCode() == 206) {
          InputStream is = conn.getInputStream();
          RandomAccessFile raf = new RandomAccessFile(filePath, "rwd");
          raf.seek(startPosition);//跳到指定位置开始写数据
          int len;
          byte[] buffer = new byte[1024];
          while ((len = is.read(buffer)) != -1) {
            //是否继续下载
            if (!isGoOn)
              break;
            //回调当前进度
            if (mDownLoadListener != null) {
              currLength += len;
              int progress = (int) ((float) currLength / (float) fileLength * 100);
              handler.sendEmptyMessage(progress);
            }

            raf.write(buffer, 0, len);
            //写完后将当前指针后移,为取消下载时保存当前进度做准备
            currPosition += len;
            synchronized (DOWNLOAD_PAUSE) {
              if (stateDownload.equals(DOWNLOAD_PAUSE)) {
                DOWNLOAD_PAUSE.wait();
              }
            }
          }
          is.close();
          raf.close();
          //线程计数器-1
          runningThreadCount--;
          //若取消下载,则直接返回
          if (!isGoOn) {
            //此处采用SharedPreferences保存每个线程的当前进度,和三个线程的总下载进度
            if (currPosition < endPosition) {
              sp.edit().putInt(SP_NAME + threadId, currPosition).apply();
              sp.edit().putInt(CURR_LENGTH, currLength).apply();
            }
            return;
          }
          if (runningThreadCount == 0) {
            sp.edit().clear().apply();
            handler.sendEmptyMessage(SUCCESS);
            handler.sendEmptyMessage(100);
            mThreads = null;
          }
        } else {
          sp.edit().clear().apply();
          handler.sendEmptyMessage(FAILURE);
        }
      } catch (Exception e) {
        sp.edit().clear().apply();
        e.printStackTrace();
        handler.sendEmptyMessage(FAILURE);
      }
    }

    public void cancel() {
      isGoOn = false;
    }
  }

  private final int SUCCESS = 0x00000101;
  private final int FAILURE = 0x00000102;

  private Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {

      if (mDownLoadListener != null) {
        if (msg.what == SUCCESS) {
          mDownLoadListener.onComplete();
        } else if (msg.what == FAILURE) {

          mDownLoadListener.onFailure();
        } else {
          mDownLoadListener.getProgress(msg.what);
        }
      }
    }
  };
}

在MainActivity中的使用

import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
  DownLoadFile downLoadFile;
  private String loadUrl = "http://gdown.baidu.com/data/wisegame/d2fbbc8e64990454/wangyiyunyinle_87.apk";
  private String filePath = Environment.getExternalStorageDirectory()+"/"+"网易云音乐.apk";

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final TextView tvprogress = (TextView) findViewById(R.id.tv_progress);
    downLoadFile = new DownLoadFile(this,loadUrl, filePath, 3);
    downLoadFile.setOnDownLoadListener(new DownLoadFile.DownLoadListener() {
      @Override
      public void getProgress(int progress) {
        tvprogress.setText("当前进度 :"+progress+" %");
      }

      @Override
      public void onComplete() {
        Toast.makeText(MainActivity.this,"下载完成",Toast.LENGTH_SHORT).show();
      }

      @Override
      public void onFailure() {
        Toast.makeText(MainActivity.this,"下载失败",Toast.LENGTH_SHORT).show();
      }
    });
    findViewById(R.id.bt).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        downLoadFile.downLoad();
      }
    });
    findViewById(R.id.bt_pause).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        downLoadFile.onPause();
      }
    });
    findViewById(R.id.bt_start).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        downLoadFile.onStart();
      }
    });
    findViewById(R.id.bt_cancel).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        downLoadFile.cancel();
      }
    });
  }

  @Override
  protected void onDestroy() {
    downLoadFile.onDestroy();
    super.onDestroy();
  }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

时间: 2017-02-22

Android实现自定义轮播图片控件示例

要完成一个轮播图片,首先想到的应该是使用ViewPager来实现.ViewPager已经有了滑动的功能,我们只要让它自己滚动.再加上下方的小圆点就行了.所以我们本次的自定义控件就是由ViewPager和LinearLayout叠加起来组成的. 直接先上效果图: 创建一个自定义的ViewPager 先上完整的代码 package com.kcode.autoscrollviewpager.view; import android.content.Context; import android.os

Android开发在轮播图片上加入点击事件的方法

这是我加在里面的代码,用Switch(position) 来获取当前图片,在相应的图片上加入点击事件, case  0:, case 1: 时代码如下,当点击第一张图片时,想实现case 0里面的代码,但是直接直接报错,退出,当点击第二张实现case 1中的代码时却是没问题,我想知道到底哪里不对啊. 对了,这些代码是在Fragemnt内写的,点击图片时是要从一个Fragment转到一个Activity,求大神指教

Android自定义控件实现简单的轮播图控件

最近要做一个轮播图的效果,网上看了几篇文章,基本上都能找到实现,效果还挺不错,但是在写的时候感觉每次都要单独去重新在Activity里写一堆代码.于是自己封装了一下.本篇轮播图实现原理原文出处:循环广告位组件的实现,这里只是做了下封装成一个控件,不必每次重复写代码了. 效果图: 实现分析 轮播图的功能就是实现左右滑动的广告.图片信息展示,那我们就用ViewPager来实现,由于考虑到用户体验,我们还需要在下面加一个指示器来标示滑动到了第几张轮播图.指示器我们可以用一个线性布局来根据要展示的轮播图

Andriod使用多线程实现轮播图片

效果图: 布局文件里包括了一个ImageView和两个Button控件,而在MainActivity了初始化控件. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk

js实现支持手机滑动切换的轮播图片效果实例

本文实例讲述了js实现支持手机滑动切换的轮播图片效果的方法.分享给大家供大家参考.具体如下: 运行效果如下: 完整实例代码点击此处本站下载. 使用方法案例: <script type="text/javascript" src="../src/zepto.js"></script> <script type="text/javascript" src="../src/carousel-image.js&qu

js实现点击左右按钮轮播图片效果实例

本文实例讲述了js实现点击左右按钮轮播图片效果的方法.分享给大家供大家参考.具体实现方法如下: $(function () { var index = 1; var pPage = 1; var $v_citemss = $(".citemss"); var $v_show = $v_citemss.find("ul"); v_width = $v_citemss.width();//图片展示区外围div的大小 //注:若为整数,前边不能再加var,否则会被提示un

Android实现自定义轮播图片控件详解

首先上效果图 实现原理 要完成一个轮播图片,首先想到的应该是使用ViewPager来实现.ViewPager已经有了滑动的功能,我们只要让它自己滚动.再加上下方的小圆点就行了.所以我们本次的自定义控件就是由ViewPager和LinearLayout叠加起来组成的. 一.创建一个自定义的ViewPager 先上完整的代码 package com.kcode.autoscrollviewpager.view; import android.content.Context; import andro

Android实现轮播图片展示效果

本文实例为大家分享了Android轮播图片的展示,供大家参考,具体内容如下 实现逻辑 1.创建XML布局文件,使用ViewPager完成轮播图片 2.初始化ViewPager控件,然后为控件设置适配器,创建出来的适配器实现里面的四个方法 3.四个方法分别是getCount isViewFromObject instantiateItem destroyItem 4.在onCreat方法中,加载图片资源,将图片ID存在集合中,使图片能够在页面中显示,(图片之所以能够在页面中显示,是因为在适配器中的

JavaScript实现轮播图片完整代码

本文实例为大家分享了JavaScript实现轮播图片的具体代码,供大家参考,具体内容如下 JS代码注释清晰明了,一看就懂! <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>最简单的轮播效果</title> <style> * { margin: 0; padding: 0 } .box { w

原生JS实现无缝轮播图片

本文实例为大家分享了JS实现无缝轮播图的具体代码,供大家参考,具体内容如下 运动插件 function animove(obj,distance,speed,callback) { //调用的变量 目标距离 速度 回调函数 clearInterval(obj.timer); obj.timer = setInterval(function () { let step = (distance - obj.offsetLeft) / 10; step = step > 0 ? Math.ceil(s

jQuery实现友好的轮播图片特效

先上效果图: [处理] 这里的图片滚动轮播,做了点小处理:当在第1页状态时,你点击第5页,图片的滚动是一张滑过,而不是从2-3-4-5(这种的多张滚动,看得头晕眼花): 实现的做法是: 剩下的就是源代码分享了: -------css---------------- 复制代码 代码如下: .gy-slide-scroll {         position: relative;         width: 320px;         height: 200px;         overfl