android实现多线程下载文件(支持暂停、取消、断点续传)
多线程下载文件(支持暂停、取消、断点续传)
多线程同时下载文件即:在同一时间内通过多个线程对同一个请求地址发起多个请求,将需要下载的数据分割成多个部分,同时下载,每个线程只负责下载其中的一部分,最后将每一个线程下载的部分组装起来即可。
涉及的知识及问题
- 请求的数据如何分段
- 分段完成后如何下载和下载完成后如何组装到一起
- 暂停下载和继续下载的实现(wait()、notifyAll()、synchronized的使用)
- 取消下载和断点续传的实现
一、请求的数据如何分段
首先通过HttpURLConnection请求总文件大小,而后根据线程数计算每一个线程的下载量,在分配给每一个线程去下载
fileLength=conn.getContentLength(); //根据文件大小,先创建一个空文件 //“r“——以只读方式打开。调用结果对象的任何write方法都将导致抛出IOException。 //“rw“——打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。 //“rws“——打开以便读取和写入,对于“rw”,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。 //“rwd“——打开以便读取和写入,对于“rw”,还要求对文件内容的每个更新都同步写入到底层存储设备。 RandomAccessFileraf=newRandomAccessFile(filePath,"rwd"); raf.setLength(fileLength); raf.close(); //计算各个线程下载的数据段 intblockLength=fileLength/threadCount;
二、分段完成后如何下载和下载完成后如何组装到一起
分段完成后给每一个线程的请求头设置Range参数,他允许客户端只请求文件的一部分数据,每一个线程只请求下载相应范围内的数据,使用RandomAccessFile(可随机读写的文件)写入到同一个文件里即可组装成目标文件Range,是在HTTP/1.1里新增的一个headerfield,它允许客户端实际上只请求文档的一部分(范围可以相互重叠)
Range的使用形式:
属性
解释
bytes=0-499
表示头500个字节
bytes=500-999
表示第二个500字节
bytes=-500
表示最后500个字节
bytes=500-
表示500字节以后的范围
bytes=0-0,-1
第一个和最后一个字节
HttpUrlConnection中设置请求头
URLurl=newURL(loadUrl);
HttpURLConnectionconn=(HttpURLConnection)url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Range","bytes="+startPosition+"-"+endPosition);
conn.setConnectTimeout(5000);
//若请求头加上Range这个参数,则返回状态码为206,而不是200
if(conn.getResponseCode()==206){
InputStreamis=conn.getInputStream();
RandomAccessFileraf=newRandomAccessFile(filePath,"rwd");
raf.seek(startPosition);//跳到指定位置开始写数据
}
三、暂停下载和继续下载的实现(wait()、notifyAll()、synchronized的使用)
关于synchronized只需记住一下五点:
- 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
- 然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
- 尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
- 第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
- 以上规则对其它对象锁同样适用.
protectedvoidonPause(){
if(mThreads!=null)
stateDownload=DOWNLOAD_PAUSE;
}
protectedvoidonStart(){
if(mThreads!=null)
synchronized(DOWNLOAD_PAUSE){
stateDownload=DOWNLOAD_ING;
DOWNLOAD_PAUSE.notifyAll();
}
}
对于wait()、notify()、notifyAll()需要注意的是
- 调用任何对象的wait()方法时,都必须先获得该对象的锁,即调用的wait()方法必须得写在synchronized(obj){…}之内
- 当调用对象的wait()方法后,该线程若想继续执行,必须得再次获得该对象的锁才可以
- 如果A1,A2,A3线程都在obj.wait(),则B调用object.notify()只能唤醒A1,A2,A3中的一个(具体哪一个由JVM决定)
- 当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;
}
断点续传即其实和重新下载是一样的,不过文件的大小和每一个线程下载时的起始位置和结束位置都不是重新计算的。而是上次取消下载时,每一个线程保存的当前位置和结束位置,让每一个线程接着上次的地方继续下载即可
SharedPreferencessp=mContext.getSharedPreferences(SP_NAME,Context.MODE_PRIVATE);
//获取上次取消下载的进度,若没有则返回0
currLength=sp.getInt(CURR_LENGTH,0);
for(inti=0;i<threadCount;i++){
//开始位置,获取上次取消下载的进度,默认返回i*blockLength,即第i个线程开始下载的位置
intstartPosition=sp.getInt(SP_NAME+(i+1),i*blockLength);
//结束位置,-1是为了防止上一个线程和下一个线程重复下载衔接处数据
intendPosition=(i+1)*blockLength-1;
//将最后一个线程结束位置扩大,防止文件下载不完全,大了不影响,小了文件失效
if((i+1)==threadCount)
endPosition=endPosition*2;
mThreads[i]=newDownThread(i+1,startPosition,endPosition);
mThreads[i].start();
}
网络获取和读写SD卡都需要添加相应权限
<uses-permissionandroid:name="android.permission.INTERNET"/> <uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
下面贴上全部的代码,里面有详细的注释DownLoadFile.Java
importandroid.content.Context;
importandroid.content.SharedPreferences;
importandroid.os.Handler;
importandroid.os.Message;
importjava.io.InputStream;
importjava.io.RandomAccessFile;
importjava.net.HttpURLConnection;
importjava.net.URL;
/**
*Createdbytianzhaoon2017/2/2109:25.
*多线程下载文件
*/
publicclassDownLoadFile{
privatestaticfinalStringSP_NAME="download_file";
privatestaticfinalStringCURR_LENGTH="curr_length";
privatestaticfinalintDEFAULT_THREAD_COUNT=4;//默认下载线程数
//以下为线程状态
privatestaticfinalStringDOWNLOAD_INIT="1";
privatestaticfinalStringDOWNLOAD_ING="2";
privatestaticfinalStringDOWNLOAD_PAUSE="3";
privateContextmContext;
privateStringloadUrl;//网络获取的url
privateStringfilePath;//下载到本地的path
privateintthreadCount=DEFAULT_THREAD_COUNT;//下载线程数
privateintfileLength;//文件总大小
//使用volatile防止多线程不安全
privatevolatileintcurrLength;//当前总共下载的大小
privatevolatileintrunningThreadCount;//正在运行的线程数
privateThread[]mThreads;
privateStringstateDownload=DOWNLOAD_INIT;//当前线程状态
privateDownLoadListenermDownLoadListener;
publicvoidsetOnDownLoadListener(DownLoadListenermDownLoadListener){
this.mDownLoadListener=mDownLoadListener;
}
interfaceDownLoadListener{
//返回当前下载进度的百分比
voidgetProgress(intprogress);
voidonComplete();
voidonFailure();
}
publicDownLoadFile(ContextmContext,StringloadUrl,StringfilePath){
this(mContext,loadUrl,filePath,DEFAULT_THREAD_COUNT,null);
}
publicDownLoadFile(ContextmContext,StringloadUrl,StringfilePath,DownLoadListenermDownLoadListener){
this(mContext,loadUrl,filePath,DEFAULT_THREAD_COUNT,mDownLoadListener);
}
publicDownLoadFile(ContextmContext,StringloadUrl,StringfilePath,intthreadCount){
this(mContext,loadUrl,filePath,threadCount,null);
}
publicDownLoadFile(ContextmContext,StringloadUrl,StringfilePath,intthreadCount,DownLoadListenermDownLoadListener){
this.mContext=mContext;
this.loadUrl=loadUrl;
this.filePath=filePath;
this.threadCount=threadCount;
runningThreadCount=0;
this.mDownLoadListener=mDownLoadListener;
}
/**
*开始下载
*/
protectedvoiddownLoad(){
//在线程中运行,防止anr
newThread(newRunnable(){
@Override
publicvoidrun(){
try{
//初始化数据
if(mThreads==null)
mThreads=newThread[threadCount];
//建立连接请求
URLurl=newURL(loadUrl);
HttpURLConnectionconn=(HttpURLConnection)url.openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
intcode=conn.getResponseCode();//获取返回码
if(code==200){//请求成功,根据文件大小开始分多线程下载
fileLength=conn.getContentLength();
//根据文件大小,先创建一个空文件
//“r“——以只读方式打开。调用结果对象的任何write方法都将导致抛出IOException。
//“rw“——打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。
//“rws“——打开以便读取和写入,对于“rw”,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
//“rwd“——打开以便读取和写入,对于“rw”,还要求对文件内容的每个更新都同步写入到底层存储设备。
RandomAccessFileraf=newRandomAccessFile(filePath,"rwd");
raf.setLength(fileLength);
raf.close();
//计算各个线程下载的数据段
intblockLength=fileLength/threadCount;
SharedPreferencessp=mContext.getSharedPreferences(SP_NAME,Context.MODE_PRIVATE);
//获取上次取消下载的进度,若没有则返回0
currLength=sp.getInt(CURR_LENGTH,0);
for(inti=0;i<threadCount;i++){
//开始位置,获取上次取消下载的进度,默认返回i*blockLength,即第i个线程开始下载的位置
intstartPosition=sp.getInt(SP_NAME+(i+1),i*blockLength);
//结束位置,-1是为了防止上一个线程和下一个线程重复下载衔接处数据
intendPosition=(i+1)*blockLength-1;
//将最后一个线程结束位置扩大,防止文件下载不完全,大了不影响,小了文件失效
if((i+1)==threadCount)
endPosition=endPosition*2;
mThreads[i]=newDownThread(i+1,startPosition,endPosition);
mThreads[i].start();
}
}else{
handler.sendEmptyMessage(FAILURE);
}
}catch(Exceptione){
e.printStackTrace();
handler.sendEmptyMessage(FAILURE);
}
}
}).start();
}
/**
*取消下载
*/
protectedvoidcancel(){
if(mThreads!=null){
//若线程处于等待状态,则while循环处于阻塞状态,无法跳出循环,必须先唤醒线程,才能执行取消任务
if(stateDownload.equals(DOWNLOAD_PAUSE))
onStart();
for(Threaddt:mThreads){
((DownThread)dt).cancel();
}
}
}
/**
*暂停下载
*/
protectedvoidonPause(){
if(mThreads!=null)
stateDownload=DOWNLOAD_PAUSE;
}
/**
*继续下载
*/
protectedvoidonStart(){
if(mThreads!=null)
synchronized(DOWNLOAD_PAUSE){
stateDownload=DOWNLOAD_ING;
DOWNLOAD_PAUSE.notifyAll();
}
}
protectedvoidonDestroy(){
if(mThreads!=null)
mThreads=null;
}
privateclassDownThreadextendsThread{
privatebooleanisGoOn=true;//是否继续下载
privateintthreadId;
privateintstartPosition;//开始下载点
privateintendPosition;//结束下载点
privateintcurrPosition;//当前线程的下载进度
privateDownThread(intthreadId,intstartPosition,intendPosition){
this.threadId=threadId;
this.startPosition=startPosition;
currPosition=startPosition;
this.endPosition=endPosition;
runningThreadCount++;
}
@Override
publicvoidrun(){
SharedPreferencessp=mContext.getSharedPreferences(SP_NAME,Context.MODE_PRIVATE);
try{
URLurl=newURL(loadUrl);
HttpURLConnectionconn=(HttpURLConnection)url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Range","bytes="+startPosition+"-"+endPosition);
conn.setConnectTimeout(5000);
//若请求头加上Range这个参数,则返回状态码为206,而不是200
if(conn.getResponseCode()==206){
InputStreamis=conn.getInputStream();
RandomAccessFileraf=newRandomAccessFile(filePath,"rwd");
raf.seek(startPosition);//跳到指定位置开始写数据
intlen;
byte[]buffer=newbyte[1024];
while((len=is.read(buffer))!=-1){
//是否继续下载
if(!isGoOn)
break;
//回调当前进度
if(mDownLoadListener!=null){
currLength+=len;
intprogress=(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(Exceptione){
sp.edit().clear().apply();
e.printStackTrace();
handler.sendEmptyMessage(FAILURE);
}
}
publicvoidcancel(){
isGoOn=false;
}
}
privatefinalintSUCCESS=0x00000101;
privatefinalintFAILURE=0x00000102;
privateHandlerhandler=newHandler(){
@Override
publicvoidhandleMessage(Messagemsg){
if(mDownLoadListener!=null){
if(msg.what==SUCCESS){
mDownLoadListener.onComplete();
}elseif(msg.what==FAILURE){
mDownLoadListener.onFailure();
}else{
mDownLoadListener.getProgress(msg.what);
}
}
}
};
}
在MainActivity中的使用
importandroid.os.Bundle;
importandroid.os.Environment;
importandroid.support.v7.app.AppCompatActivity;
importandroid.view.View;
importandroid.widget.TextView;
importandroid.widget.Toast;
publicclassMainActivityextendsAppCompatActivity{
DownLoadFiledownLoadFile;
privateStringloadUrl="http://gdown.baidu.com/data/wisegame/d2fbbc8e64990454/wangyiyunyinle_87.apk";
privateStringfilePath=Environment.getExternalStorageDirectory()+"/"+"网易云音乐.apk";
@Override
protectedvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
finalTextViewtvprogress=(TextView)findViewById(R.id.tv_progress);
downLoadFile=newDownLoadFile(this,loadUrl,filePath,3);
downLoadFile.setOnDownLoadListener(newDownLoadFile.DownLoadListener(){
@Override
publicvoidgetProgress(intprogress){
tvprogress.setText("当前进度:"+progress+"%");
}
@Override
publicvoidonComplete(){
Toast.makeText(MainActivity.this,"下载完成",Toast.LENGTH_SHORT).show();
}
@Override
publicvoidonFailure(){
Toast.makeText(MainActivity.this,"下载失败",Toast.LENGTH_SHORT).show();
}
});
findViewById(R.id.bt).setOnClickListener(newView.OnClickListener(){
@Override
publicvoidonClick(Viewv){
downLoadFile.downLoad();
}
});
findViewById(R.id.bt_pause).setOnClickListener(newView.OnClickListener(){
@Override
publicvoidonClick(Viewv){
downLoadFile.onPause();
}
});
findViewById(R.id.bt_start).setOnClickListener(newView.OnClickListener(){
@Override
publicvoidonClick(Viewv){
downLoadFile.onStart();
}
});
findViewById(R.id.bt_cancel).setOnClickListener(newView.OnClickListener(){
@Override
publicvoidonClick(Viewv){
downLoadFile.cancel();
}
});
}
@Override
protectedvoidonDestroy(){
downLoadFile.onDestroy();
super.onDestroy();
}
}
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。