多线程断点续传(简单demo)——从无到有

复杂功能总是由许多小功能组合在一起完成的,一步一步完成多线程断点续传,可以从以下几个方面来考虑。
第一,实现简单的下载;
第二,打断下载线程,实现暂停功能;
第三,从已经下载点进行续传;
第四,引入多线程。

整个项目请点击:github下载地址

截图:


多线程断点续传(简单demo)——从无到有_第1张图片
demo截图.png

简单的下载

下载代码
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链接

你可能感兴趣的:(多线程断点续传(简单demo)——从无到有)