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

多线程同时下载文件即:在同一时间内通过多个线程对同一个请求地址发起多个请求,将需要下载的数据分割成多个部分,同时下载,每个线程只负责下载其中的一部分,最后将每一个线程下载的部分组装起来即可。
涉及的知识及问题
一、请求的数据如何分段
首先通过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只需记住一下五点:
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()需要注意的是
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();
}
}
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
# android
# 多线程下载
# android多线程断点续传
# android实现断点续传
# Android多线程+单线程+断点续传+进度条显示下载功能
# Android多线程断点续传下载功能实现代码
# Android多线程断点续传下载示例详解
# Android 使用AsyncTask实现多任务多线程断点续传下载
# Android实现网络多线程断点续传下载实例
# Android编程开发实现多线程断点续传下载器实例
# PC版与Android手机版带断点续传的多线程下载
# Android 使用AsyncTask实现多线程断点续传
# android原生实现多线程断点续传功能
# 多线程
# 多个
# 该文件
# 完成后
# 存储设备
# 的是
# 断点续传
# 时间内
# 大了
# 都将
# 后将
# 不完全
# 跳到
# 必须先
# 创建一个
# 抛出
# 尚不
# 下载点
# 下载量
# 客户端
相关文章:
建站之星下载版如何获取与安装?
专业网站建设制作报价,网页设计制作要考什么证?
如何通过建站之星自助学习解决操作问题?
网站制作话术技巧,网站推广做的好怎么话术?
活动邀请函制作网站有哪些,活动邀请函文案?
如何在云主机上快速搭建多站点网站?
如何在万网自助建站中设置域名及备案?
网页制作模板网站推荐,网页设计海报之类的素材哪里好?
如何配置FTP站点权限与安全设置?
零服务器AI建站解决方案:快速部署与云端平台低成本实践
建站之星如何配置系统实现高效建站?
Swift中swift中的switch 语句
微信小程序 五星评分(包括半颗星评分)实例代码
建站之星后台管理系统如何操作?
如何通过免费商城建站系统源码自定义网站主题与功能?
制作网站的模板软件,网站怎么建设?
惠州网站建设制作推广,惠州市华视达文化传媒有限公司怎么样?
昆明网站制作哪家好,昆明公租房申请网上登录入口?
淘宝制作网站有哪些,淘宝网官网主页?
如何获取开源自助建站系统免费下载链接?
表情包在线制作网站免费,表情包怎么弄?
,石家庄四十八中学官网?
如何在阿里云部署织梦网站?
如何在阿里云高效完成企业建站全流程?
如何通过西部建站助手安装IIS服务器?
网站制作免费,什么网站能看正片电影?
模具网站制作流程,如何找模具客户?
如何制作一个表白网站视频,关于勇敢表白的小标题?
专业公司网站制作公司,用什么语言做企业网站比较好?
如何用wdcp快速搭建高效网站?
建站之星2.7模板:企业网站建设与h5定制设计专题
图册素材网站设计制作软件,图册的导出方式有几种?
制作网站的网址是什么,请问后缀为.com和.com.cn还有.cn的这三种网站是分别是什么类型的网站?
建站之星备案流程有哪些注意事项?
北京建设网站制作公司,北京古代建筑博物馆预约官网?
建站之星安装步骤有哪些常见问题?
如何在阿里云虚拟服务器快速搭建网站?
安云自助建站系统如何快速提升SEO排名?
制作旅游网站html,怎样注册旅游网站?
如何快速生成凡客建站的专业级图册?
北京网页设计制作网站有哪些,继续教育自动播放怎么设置?
如何快速查询网站的真实建站时间?
微信小程序 input输入框控件详解及实例(多种示例)
青岛网站建设如何选择本地服务器?
如何获取上海专业网站定制建站电话?
怎么制作网站设计模板图片,有电商商品详情页面的免费模板素材网站推荐吗?
建站之星在线版空间:自助建站+智能模板一键生成方案
巅云智能建站系统:可视化拖拽+多端适配+免费模板一键生成
盐城做公司网站,江苏电子版退休证办理流程?
早安海报制作网站推荐大全,企业早安海报怎么每天更换?
*请认真填写需求信息,我们会在24小时内与您取得联系。