AsyncTask实现多线程断点续传
前面一篇博客《AsyncTask实现断点续传》讲解了如何实现单线程下的断点续传,也就是一个文件只有一个线程进行下载。
对于大文件而言,使用多线程下载就会比单线程下载要快一些。多线程下载相比单线程下载要稍微复杂一点,本博文将详细讲解如何使用AsyncTask来实现多线程的断点续传下载。
一、实现原理
多线程下载首先要通过每个文件总的下载线程数(我这里设定5个)来确定每个线程所负责下载的起止位置。
long blockLength = mFileLength / DEFAULT_POOL_SIZE;
for (int i = 0; i < DEFAULT_POOL_SIZE; i++) {
long beginPosition = i * blockLength;//每条线程下载的开始位置
long endPosition = (i + 1) * blockLength;//每条线程下载的结束位置
if (i == (DEFAULT_POOL_SIZE - 1)) {
endPosition = mFileLength;//如果整个文件的大小不为线程个数的整数倍,则最后一个线程的结束位置即为文件的总长度
}
......
}
这里需要注意的是,文件大小往往不是线程个数的整数倍,所以最后一个线程的结束位置需要设置为文件长度。
确定好每个线程的下载起止位置之后,需要设置http请求头来下载文件的指定位置:
//设置下载的数据位置beginPosition字节到endPosition字节
Header header_size = new BasicHeader("Range", "bytes=" + beginPosition + "-" + endPosition);
request.addHeader(header_size);
以上是多线程下载的原理,但是还要实现断点续传需要在每次暂停之后记录每个线程已下载的大小,下次继续下载时从上次下载后的位置开始下载。一般项目中都会存数据库中,我这里为了简单起见直接存在了SharedPreferences中,已下载url和线程编号作为key值。
@Override
protected void onPostExecute(Long aLong) {
Log.i(TAG, "download success ");
//下载完成移除记录
mSharedPreferences.edit().remove(currentThreadIndex).commit();
} @Override
protected void onCancelled() {
Log.i(TAG, "download cancelled ");
//记录已下载大小current
mSharedPreferences.edit().putLong(currentThreadIndex, current).commit();
}
下载的时候,首先获取已下载位置,如果已经下载过,就从上次下载后的位置开始下载:
//获取之前下载保存的信息,从之前结束的位置继续下载
//这里加了判断file.exists(),判断是否被用户删除了,如果文件没有下载完,但是已经被用户删除了,则重新下载
long downedPosition = mSharedPreferences.getLong(currentThreadIndex, 0);
if(file.exists() && downedPosition != 0) {
beginPosition = beginPosition + downedPosition;
current = downedPosition;
synchronized (mCurrentLength) {
mCurrentLength += downedPosition;
}
}
二、完整代码
package com.bbk.lling.multithreaddownload; import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast; import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHeader; import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors; public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private static final int DEFAULT_POOL_SIZE = 5;
private static final int GET_LENGTH_SUCCESS = 1;
//下载路径
private String downloadPath = Environment.getExternalStorageDirectory() +
File.separator + "download"; // private String mUrl = "http://ftp.neu.edu.cn/mirrors/eclipse/technology/epp/downloads/release/juno/SR2/eclipse-java-juno-SR2-linux-gtk-x86_64.tar.gz";
private String mUrl = "http://p.gdown.baidu.com/c4cb746699b92c9b6565cc65aa2e086552651f73c5d0e634a51f028e32af6abf3d68079eeb75401c76c9bb301e5fb71c144a704cb1a2f527a2e8ca3d6fe561dc5eaf6538e5b3ab0699308d13fe0b711a817c88b0f85a01a248df82824ace3cd7f2832c7c19173236";
private ProgressBar mProgressBar;
private TextView mPercentTV;
SharedPreferences mSharedPreferences = null;
long mFileLength = 0;
Long mCurrentLength = 0L; private InnerHandler mHandler = new InnerHandler(); //创建线程池
private Executor mExecutor = Executors.newCachedThreadPool(); private List<DownloadAsyncTask> mTaskList = new ArrayList<DownloadAsyncTask>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mProgressBar = (ProgressBar) findViewById(R.id.progressbar);
mPercentTV = (TextView) findViewById(R.id.percent_tv);
mSharedPreferences = getSharedPreferences("download", Context.MODE_PRIVATE);
//开始下载
findViewById(R.id.begin).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread() {
@Override
public void run() {
//创建存储文件夹
File dir = new File(downloadPath);
if (!dir.exists()) {
dir.mkdir();
}
//获取文件大小
HttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet(mUrl);
HttpResponse response = null; try {
response = client.execute(request);
mFileLength = response.getEntity().getContentLength();
} catch (Exception e) {
Log.e(TAG, e.getMessage());
} finally {
if (request != null) {
request.abort();
}
}
Message.obtain(mHandler, GET_LENGTH_SUCCESS).sendToTarget();
}
}.start();
}
}); //暂停下载
findViewById(R.id.end).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
for (DownloadAsyncTask task : mTaskList) {
if (task != null && (task.getStatus() == AsyncTask.Status.RUNNING || !task.isCancelled())) {
task.cancel(true);
}
}
mTaskList.clear();
}
});
} /**
* 开始下载
* 根据待下载文件大小计算每个线程下载位置,并创建AsyncTask
*/
private void beginDownload() {
mCurrentLength = 0L;
mPercentTV.setVisibility(View.VISIBLE);
mProgressBar.setProgress(0);
long blockLength = mFileLength / DEFAULT_POOL_SIZE;
for (int i = 0; i < DEFAULT_POOL_SIZE; i++) {
long beginPosition = i * blockLength;//每条线程下载的开始位置
long endPosition = (i + 1) * blockLength;//每条线程下载的结束位置
if (i == (DEFAULT_POOL_SIZE - 1)) {
endPosition = mFileLength;//如果整个文件的大小不为线程个数的整数倍,则最后一个线程的结束位置即为文件的总长度
}
DownloadAsyncTask task = new DownloadAsyncTask(beginPosition, endPosition);
mTaskList.add(task);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mUrl, String.valueOf(i));
}
} /**
* 更新进度条
*/
synchronized public void updateProgress() {
int percent = (int) Math.ceil((float)mCurrentLength / (float)mFileLength * 100);
// Log.i(TAG, "downloading " + mCurrentLength + "," + mFileLength + "," + percent);
if(percent > mProgressBar.getProgress()) {
mProgressBar.setProgress(percent);
mPercentTV.setText("下载进度:" + percent + "%");
if (mProgressBar.getProgress() == mProgressBar.getMax()) {
Toast.makeText(MainActivity.this, "下载结束", Toast.LENGTH_SHORT).show();
}
}
} @Override
protected void onDestroy() {
for(DownloadAsyncTask task: mTaskList) {
if(task != null && task.getStatus() == AsyncTask.Status.RUNNING) {
task.cancel(true);
}
mTaskList.clear();
}
super.onDestroy();
} /**
* 下载的AsyncTask
*/
private class DownloadAsyncTask extends AsyncTask<String, Integer , Long> {
private static final String TAG = "DownloadAsyncTask";
private long beginPosition = 0;
private long endPosition = 0; private long current = 0; private String currentThreadIndex; public DownloadAsyncTask(long beginPosition, long endPosition) {
this.beginPosition = beginPosition;
this.endPosition = endPosition;
} @Override
protected Long doInBackground(String... params) {
Log.i(TAG, "downloading");
String url = params[0];
currentThreadIndex = url + params[1];
if(url == null) {
return null;
}
HttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet(url);
HttpResponse response = null;
InputStream is = null;
RandomAccessFile fos = null;
OutputStream output = null; try {
//本地文件
File file = new File(downloadPath + File.separator + url.substring(url.lastIndexOf("/") + 1)); //获取之前下载保存的信息,从之前结束的位置继续下载
//这里加了判断file.exists(),判断是否被用户删除了,如果文件没有下载完,但是已经被用户删除了,则重新下载
long downedPosition = mSharedPreferences.getLong(currentThreadIndex, 0);
if(file.exists() && downedPosition != 0) {
beginPosition = beginPosition + downedPosition;
current = downedPosition;
synchronized (mCurrentLength) {
mCurrentLength += downedPosition;
}
} //设置下载的数据位置beginPosition字节到endPosition字节
Header header_size = new BasicHeader("Range", "bytes=" + beginPosition + "-" + endPosition);
request.addHeader(header_size);
//执行请求获取下载输入流
response = client.execute(request);
is = response.getEntity().getContent(); //创建文件输出流
fos = new RandomAccessFile(file, "rw");
//从文件的size以后的位置开始写入,其实也不用,直接往后写就可以。有时候多线程下载需要用
fos.seek(beginPosition); byte buffer [] = new byte[1024];
int inputSize = -1;
while((inputSize = is.read(buffer)) != -1) {
fos.write(buffer, 0, inputSize);
current += inputSize;
synchronized (mCurrentLength) {
mCurrentLength += inputSize;
}
this.publishProgress();
if (isCancelled()) {
return null;
}
}
} catch (MalformedURLException e) {
Log.e(TAG, e.getMessage());
} catch (IOException e) {
Log.e(TAG, e.getMessage());
} finally{
try{
/*if(is != null) {
is.close();
}*/
if (request != null) {
request.abort();
}
if(output != null) {
output.close();
}
if(fos != null) {
fos.close();
}
} catch(Exception e) {
e.printStackTrace();
}
}
return null;
} @Override
protected void onPreExecute() {
Log.i(TAG, "download begin ");
super.onPreExecute();
} @Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
//更新界面进度条
updateProgress();
} @Override
protected void onPostExecute(Long aLong) {
Log.i(TAG, "download success ");
//下载完成移除记录
mSharedPreferences.edit().remove(currentThreadIndex).commit();
} @Override
protected void onCancelled() {
Log.i(TAG, "download cancelled ");
//记录已下载大小current
mSharedPreferences.edit().putLong(currentThreadIndex, current).commit();
} @Override
protected void onCancelled(Long aLong) {
Log.i(TAG, "download cancelled(Long aLong)");
super.onCancelled(aLong);
mSharedPreferences.edit().putLong(currentThreadIndex, current).commit();
}
} private class InnerHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case GET_LENGTH_SUCCESS :
beginDownload();
break;
}
super.handleMessage(msg);
}
} }
布局文件和前面一篇博客《AsyncTask实现断点续传》布局文件是一样的,这里就不贴代码了。
以上代码亲测可用,几百M大文件也没问题。
三、遇到的坑
问题描述:在使用上面代码下载http://ftp.neu.edu.cn/mirrors/eclipse/technology/epp/downloads/release/juno/SR2/eclipse-java-juno-SR2-linux-gtk-x86_64.tar.gz文件的时候,不知道为什么暂停时候执行AsyncTask.cancel(true)来取消下载任务,不执行onCancel()函数,也就没有记录该线程下载的位置。并且再次点击下载的时候,5个Task都只执行了onPreEexcute()方法,压根就不执行doInBackground()方法。而下载其他文件没有这个问题。
这个问题折腾了我好久,它又没有报任何异常,调试又调试不出来。看AsyncTask的源码、上stackoverflow也没有找到原因。看到这个网站(https://groups.google.com/forum/#!topic/android-developers/B-oBiS7npfQ)时,我还真以为是AsyncTask的一个bug。
百番周折,问题居然出现在上面代码239行(这里已注释)。不知道为什么,执行这一句的时候,线程就阻塞在那里了,所以doInBackground()方法一直没有结束,onCancel()方法当然也不会执行了。同时,因为使用的是线程池Executor,线程数为5个,点击取消之后5个线程都阻塞了,所以再次点击下载的时候只执行了onPreEexcute()方法,没有空闲的线程去执行doInBackground()方法。真是巨坑无比有木有。。。
虽然问题解决了,但是为什么有的文件下载执行到is.close()的时候线程会阻塞而有的不会?这还是个谜。如果哪位大神知道是什么原因,还望指点指点!
源码下载:https://github.com/liuling07/MultiTaskAndThreadDownload
AsyncTask实现多线程断点续传的更多相关文章
- AsyncTask实现多任务多线程断点续传下载
这篇博客是AsyncTask下载系列的最后一篇文章,前面写了关于断点续传的和多线程下载的博客,这篇是在前两篇的基础上面实现的,有兴趣的可以去看下. 一.AsyncTask实现断点续传 二.AsyncT ...
- android 多线程断点续传下载
今天跟大家一起分享下Android开发中比较难的一个环节,可能很多人看到这个标题就会感觉头很大,的确如果没有良好的编码能力和逻辑思维,这块是很难搞明白的,前面2次总结中已经为大家分享过有关技术的一些基 ...
- C#基础-FileStream实现多线程断点续传
一.前言 网上有许多的多线程断点续传操作,但总是写的很云里雾里,或者写的比较坑长.由于这几个月要负责公司的在线升级项目,所以正好顺便写了一下 代码如下: using System; using Sys ...
- 实现android支持多线程断点续传下载器功能
多线程断点下载流程图: 多线程断点续传下载原理介绍: 在下载的时候多个线程并发可以占用服务器端更多资源,从而加快下载速度手机端下载数据时难免会出现无信号断线.电量不足等情况,所以需要断点续传功能根据下 ...
- Android开发多线程断点续传下载器
使用多线程断点续传下载器在下载的时候多个线程并发可以占用服务器端更多资源,从而加快下载速度,在下载过程中记录每个线程已拷贝数据的数量,如果下载中断,比如无信号断线.电量不足等情况下,这就需要使用到断点 ...
- Android多线程断点续传下载
这个月接到一个项目.要写一个像360助手一样的对于软件管理的APP:当中.遇到了一个问题:多线程断点下载 这个 ,因为之前没有写过这方面的应用功能.所以.不免要自学了. 然后就在各个昂站上收索并整理了 ...
- Android 多线程断点续传同时下载多个大文件
最近学习在Android环境中一些网络请求方面的知识,其中有一部分是关于网络下载方面的知识.在这里解析一下自己写的demo,总结一下自己所学的知识.下图为demo的效果图,仿照一些应用下载商城在Lis ...
- android多线程断点续传下载文件
一.目标 1.多线程抢占服务器资源下载. 2.断点续传. 二.实现思路. 假设分为三个线程: 1.各个线程分别向服务器请求文件的不同部分. 这个涉及Http协议,可以在Header中使用Range参数 ...
- [转载]http协议 文件下载原理及多线程断点续传
最近研究了一下关于文件下载的相关内容,觉得还是写些东西记下来比较好.起初只是想研究研究,但后来发现写个可重用性比较高的模块还是很有必要的,我想这也是大多数开发人员的习惯吧.对于HTTP协议,向服务器请 ...
随机推荐
- gawk快速入门
基本定义: gawk 的主要功能是针对文本的每一行执行被指定的 actions. 命令格式: gawk option program file option: -F 指定的分隔符,默认的分隔符是空格, ...
- [转]理解dropout
理解dropout 原文地址:http://blog.csdn.net/stdcoutzyx/article/details/49022443 理解dropout 注意:图片都在github上 ...
- ubuntu下完全安装mcrypt
源文章: ubuntu下安装mcrypt 1.首先要下载三个软件 0libmcrypt-2.5.8.tar.gz 下载地址:http://sourceforge.net/project/showfil ...
- (Python )控制流语句if、for、while
这一节,我们将学习Python的控制流语句,主要包括if.for.while.break.continue 和pass语句 1. If语句 if语句也许是我们最熟悉的语句.其使用方法如下: x=inp ...
- 怎么把U盘启动改为硬盘启动(适用于U盘安装系统时)
两种方法: 一:安装时: 在自定义创建分区后,如图: 选择系统的启动程序安装的位置,在change device 里设置第一启动装置,和第二启动装置! 二:安装后: 开机未进入系统按F2,进入BIO ...
- php 获取代码执行时间和消耗的内存
做一些php性能测试的时候,要获取代码执行时间和消耗的内存,查了一下资料,发现php有自带的函数可以实现这个功能,具体实现代码如下: $t1 = microtime(true);// ... 执行代码 ...
- vsftpd的主动模式与被动模式
好不容易配置好了vsftpd服务,在CentOS本机测试没有问题,但是在我的Windows机器上使用FlashFXP连接的时候却出问题了: 我虽然知道FTP存在两种模式:PORT(主动)模式.PASV ...
- linux httpd 服务的安装
1.查看是否安装了httpd rpm -qa|grep httpd 2.安装httpd 使用yum 安装 yum -y install httpd 3.关闭防火墙和selinxu 4.使用fz软件或者 ...
- Java中获取路径的各种方法
1. java文件中获得路径 Thread.currentThread().getContextClassLoader().getResource("") //获得资源文件(.cl ...
- 记一次SQLServer数据库误删数据找回
昨天 同事在本机清理数据库表时,连接到了生产机,误删了二十几张表,幸好是晚上加班的时候删除的,生产机上当时是一天一备份,还原备份是最后的策略,最关键的还是要找回数据. ...