复杂功能总是由许多小功能组合在一起完成的,一步一步完成多线程断点续传,可以从以下几个方面来考虑。
第一,实现简单的下载;
第二,打断下载线程,实现暂停功能;
第三,从已经下载点进行续传;
第四,引入多线程。
整个项目请点击:github下载地址
截图:
简单的下载
下载代码
InputStream is = null;
OutputStream os = null;
try {
HttpURLConnection urlConnection = createConnection();
is = urlConnection.getInputStream();
// 获取输出流,注意检查文件夹和文件是否存在
os = new FileOutputStream(
createFile(FileUtil.getExternalCacheDir(),fileName));
// 获取文件大小,用于百分比的计算
int contentSize = urlConnection.getContentLength();
byte[] buffer = new byte[BUFFER_SIZE];
int length;
while ((length = is.read(buffer)) != -1){
os.write(buffer,0,length);
currentLength += length;
os.flush();
Message message = Message.obtain();
// 这个百分比的计算方式有问题,待会儿讲。
message.arg1 = currentLength * 100 / contentSize;
handler.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
IOCloseUtil.inputClose(is);
IOCloseUtil.outputClose(os);
}
注意点
相信以上的代码大家早已烂熟于心了。不过还是有几个注意点:
第一点:创建下载文件夹时
private File createFile(String fileDir, String fileName){
File dir = new File(fileDir);
// 注意要先检查文件夹是否存在并且创建
if (!dir.exists())
dir.mkdirs();
// 然后检查文件是否存在
File file = new File(dir,fileName);
if (!file.exists()){
try {
file.createNewFile();
} catch (IOException e) {
Log.e("zp_test","文件创建失败!");
}
}
return file;
}
第二点:
message.arg1 = currentLength * 100 / contentSize;
currentLength为当前下载的大小,如果文件较大,比如超过int的最大值个字节,也就是超过20.48M。那么这种计算方式就会导致错误。具体做法待会儿下面会讲到。
线程打断
打断代码
while (!Thread.interrupted()){
byte[] buffer = new byte[BUFFER_SIZE];
int length;
if ((length = mStream.read(buffer)) != -1){
mAccessFile.write(buffer,0,length);
currentLength += length;
Message message = Message.obtain();
message.arg1 = (int) (currentLength / totalLength * 100);
message.obj = progressBar;
handler.sendMessage(message);
Log.d("zp_test","rate: " + message.arg1);
if (message.arg1 == 100) {
DatabaseManager.getInstance().updateStart(url,currentLength);
break;
}
}
}
我所使用的打断代码,没错,就是!Thread.interrupted()
,这个用线程的打断方法就可以打断,而我用的是线程池返回的future对象的cancel()方法进行打断。注意,打断标记在Thread.interrupted()后会迅速置回原值。这样写还有一个好处,就是不会cancel()方法不会被read()方法导致的IO阻塞给截住,而导致不会退出while循环。
另外,message.arg1 = (int) (currentLength / totalLength * 100);
把totalLength 变量变为float类型,这样相除后就变成了带小数点的float类型,就不会出现上面int型溢出的问题。
断点续传
断点续传代码
HttpURLConnection urlConnection = createConnection();
File file = new File(FileUtil.getExternalCacheDir(),fileName);
// 判断下载文件是否存在
if (file != null && file.length() > 0) {
// 判断url是否存在本地数据库中
DownloadInfo info = DatabaseManager.getInstance().isExistUrl(url);
if (info != null) {
totalLength = info.getContentSize();
Log.d("zp_test","start: " + info.getStart() + " end: " + info.getContentSize());
urlConnection.setRequestProperty("Range","bytes=" +
info.getStart() + "-" + info.getContentSize());
// 设置range后,content length的值会发生变化,变成没有下载的内容长度
// setRequestProperty这个方法必须在连接发生前进行调用
// if (info.getContentSize() == urlConnection.getContentLength()){
mAccessFile = new RandomAccessFile(file,"rwd");
// 下载文件类移动到指定的指针位置。
mAccessFile.seek(info.getStart());
currentLength = info.getStart();
// 文件已经下载完毕,不需要重新下载
if (info.getContentSize() == currentLength)
return;
} else {
Log.w("zp_test",LOG_TAG + "info is null......");
}
}
注意点
在这里有个问题困恼我了一会儿,最开始我在注掉的代码if (info.getContentSize() == urlConnection.getContentLength())
这句后,进行的urlConnection.setRequestProperty
操作,结果,代码运行到这句set操作后,直接卡死在这里,也没有报出任何错误。
最后想起,像urlConnection.getContentLength() urlConnection.getInputStream();
等等这类操作,会导致流通道建立连接,开始进行数据的交互。这以后是不能进行进行urlConnection.setRequestProperty
这类型操作的。
所以重点注意:** setRequestProperty这个方法必须在连接发生前进行调用 **
引入多线程
public class PegasusExecutors extends ThreadPoolExecutor {
private static final int DEFAULT_THREAD_COUNT = 4;
// 线程池中4条线程,考虑到有可能的复用,每条线程在下载后,还会
// 保留10s钟
public PegasusExecutors() {
super(DEFAULT_THREAD_COUNT, DEFAULT_THREAD_COUNT,
10, TimeUnit.SECONDS, new PriorityBlockingQueue());
}
@Override
public Future> submit(Runnable task) {
PegasusFutureTask futureTask = new PegasusFutureTask((DownloadTaskRunnable) task);
execute(futureTask);
return futureTask;
}
}
其实这里还可以参考picasso的源码进行线程池的编写。不同的网络环境不同的线程的条数。
最后一个问题
当引入listview的时候,最开始想到的更新listview中进度条progressbar的方式是线程中进行本地数据库的更新,然后再在handler处理消息方法中获取本地数据库数据,赋值给listview,刷新适配器。但是这样做有个问题:
刷新适配器在下载中是不断进行,这样会导致停止按钮不断刷新而不能点击。
最后想到的解决方案是给每个任务在构造时传入一个progressbar对象,然后在handler中进行处理更新进度条。
executors.submit(new DownloadTaskRunnable(url, new MyHandler(),viewHolder.pb)));
(若各位有其他的方式方法欢迎一起讨论)数据库的操作也在demo中,如果需要可以下载demo。由于只是一个简单的例子,错误也在所难免,主要是为了多体会从零到一的感觉,让习惯了复制粘贴的我们多发现一些实现细节问题。
最后,由于本人水平有限,如有错误,欢迎指出。谢谢!
欢迎下载
github demo链接