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

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

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

截图:


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<Runnable>());
    }

    @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链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容