android开发笔记之多线程下载及断点续传

今天我们来接触一下多线程下载,当然也包括断点续传,我们可以看到

很多下载器,当开通会员的时候下载东西的速度就变得快了许多,这是为什么呢?这就是跟今天讲的多线程有关系了,其实就是多开了几个线程一起下载罢了。当然真正的多线程下载要比这个复杂,要考虑很多问题。

做个不恰当的比喻:

假如我们把一个服务器上的文件看作是一个水缸里的水的话,那么多线程下载就相当于从水缸上打了多个小孔,然后塞进去小管道进行抽水。呵呵,也许这个比喻不够准确。

效果:

这里下载的是本地服务器上的文件,你们可以下载网络上的一些文件。

先来看看多线程下载的原理吧:

通常服务器同时与多个用户连接,用户之间共享带宽。如果N个用户的优先级都相同,那么每个用户连接到该服务器上的实际带宽就是服务器带宽的N分之一。可以想象,如果用户数目较多,则每个用户只能占有可怜的一点带宽,下载将会是个漫长的过程。

假设服务器的带宽为20M/s,服务器上有很多电影资源,现在有三位同学都想要下载 小泽.avi 这部电影,现在三位同学都在下载,所以每位同学的速度应该为1/3 * 20M/s = 6.7M/s ,但是 小泽.avi 这部电影的大小有 2G左右,这时王五同学可能有点赶时间,等不及,下的这么慢,所以他就使用他所学的多线程的知识多开了几个线程,结果他最先下完。

这次可以看到分给每个线程的带宽为1/5 * 20M/s = 4M/s,但是后面三个线程都是王五同学的,这时王五同学的带宽其实为 12M/s ,没错,王五同学成功运用多线程知识解决了下载慢的问题。(神不知鬼不觉)

看到这里我们可以知道,影响用户带宽的因素:

①服务器的带宽

②线程数

不过笔者认为凡事适可而止,不要做得太绝了。这样让别的用户怎么办呢(不管?不太好吧!)?

好的,那让我们来看下具体如何实现:

要实现这个,需要解决以下几个问题:

问题1:怎么在一个文件里面写数据的时候按照指定的位置写(因为每个线程的下载区间需要不一样,不然数据会覆盖,导致文件下不全)

问题2:如何去获取要下载的文件大小(因为怕下载中途需要下载其他东西,导致本次需要下载的文件内存不足,所以需要先预留一个和要下载的文件大小一样大的空间)

问题3:计算每个子线程的下载区间(因为每个线程的下载区间肯定不一样,不然怎么加快速度呢)

第一个问题的解决办法:

借助RandomAccessFile 随机文件访问类的 seek(long offset)方法,这个方法可以把文件的写入位置移动至offset。

第二个问题的解决办法:

我们可以使用HttpURLConnection 对象的 getContentLength() 方法得到你当前请求文件的大小。

第三个问题的解决办法:

假设下载的文件大小为10B(0-9,数组下标从0开始),线程数为3,那么

线程0的下载区间应该是: 0—2

线程1的下载区间应该是: 3—5

线程2的下载区间应该是: 6—9

每个线程下载文件的大小 = 文件长度 / 线程数 (最后一个线程除外,因为可能不能均分)

那么i线程的下载开始位置: i*每个线程下载文件的大小

i线程的下载结束位置: (i+1)*每个线程下载文件的大小 - 1

最后一个线程的结束位置为:文件长度 - 1

搞清楚以上问题就可以多线程下载了,接下里就是断点续传了。

因为有时我们在下载下到一半的时候突然停电了,等来电时我们应该接到上次下载的地方继续下载。如何实现呢??

我们可以把每个线程下载的进度都存在一个文件里,等来电时我们先去检索有没有进度文件,如果有,说明上次下载过,但没下完,就将次进度取出来继续下载。不过线程下载的开始位置应该是 原来的开始位置+上次的进度,为了用户体验,我们应该在线程全部下载完成之后将保存的下载进度文件删除(因为这个文件对用户也没什么用)。

下面我们来理一下思路:

①请求网络得到需要下载的文件的大小,并生成一个和原文件一样大小的文件(先占空间)(响应码为200)

②确定每个线程的下载区间(最后一个线程的结束位置应该单独考虑)

③先查看有没有进度文件,有则从上次进度开始下载,没有则请求网络获取需要下载区间的数据,并生成下载进度文件以便断点续传。(记住请求的数据不是所有数据,而是各个线程它需要下载的那部分区间,响应码为206)

注:不是所有的服务器都支持断点续传,这取决于服务器那边。

④待各个线程全部下载完成,将进度文件删掉。

⑤开启线程下载

核心代码:

布局文件activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.multithreaddownload.MainActivity" >

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="download"
        android:text="多线程下载及断点续传" />

    <ProgressBar
        android:id="@+id/pb"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        style="@android:style/Widget.ProgressBar.Horizontal"/>
    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>

Mactivity.java

package com.example.multithreaddownload;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;

import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;

public class MainActivity extends Activity {
    //进度条
    private ProgressBar pb;
    //显示进度(百分比)
    private TextView tv;
    //记录当前进度条的下载进度
    private int currentProgress;
    //进行下载的线程数量
    public static final int THREADCOUNT = 3;
    //下载完成的线程数量
    public int finishedThread = 0;
    //下载完成生成的文件名
    public String fileName = "navicat.pdf";
    //请求的文件下载地址(本地文件)
    public String path = "http://192.168.1.100:8089/" + fileName;
    //请求的文件下载地址(网络文件)
//   public String path =
//   "thunder://QUFodHRwOi8vZGw0NS44MHMuaW06OTIwLzE2MDUv6LaF6ISRNDjlsI/ml7Zb5Zu96K+tRFZE54mIXS/otoXohJE0OOWwj+aXtlvlm73or61EVkTniYhdX2JkLm1wNFpa";
     private Handler mHandler = new Handler(){
         public void handleMessage(android.os.Message msg) {
             if (msg.what == 0x1) {
                tv.setText(pb.getProgress()*100/pb.getMax() + "%");
            }
         };
     };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }
    /**
     * 初始化组件
     */
    private void initView() {
        pb = (ProgressBar) findViewById(R.id.pb);
        tv = (TextView) findViewById(R.id.tv);
    }
    /**
     * 点击下载的事件
     * @param view
     */
    public void download(View view) {
        new Thread() {
            public void run() {
                try {
                    URL url = new URL(path);
                    HttpURLConnection conn = (HttpURLConnection) url
                            .openConnection();
                    conn.setRequestMethod("GET");
                    conn.setConnectTimeout(3000);
                    conn.setReadTimeout(8000);
                    //请求成功时的响应码为200(注意响应码为200)
                    if (conn.getResponseCode() == 200) {
                        // 拿到需要下载的文件的大小
                        int length = conn.getContentLength();
                        // 先占个位置,生成临时文件
                        File file = new File(Environment.getExternalStorageDirectory(),fileName);
                        RandomAccessFile raf = new RandomAccessFile(file, "rwd");
                        raf.setLength(length);
                        //设置进度条的最大进度为文件的长度
                        pb.setMax(length);
                        raf.close();
                        //每个线程应该下载的长度(最后一个线程除外,因为不一定能够平分)
                        int size = length / THREADCOUNT;
                        for (int i = 0; i < THREADCOUNT; i++) {
                            // 1.确定每个线程的下载区间
                            // 2.开启对应的子线程
                            int startIndex = i * size;  //开始位置
                            int endIndex = (i + 1) * size - 1;  //结束位置
                            // 最后一个线程
                            if (i == THREADCOUNT - 1) {
                                endIndex = length - 1;
                            }
                            System.out.println("第" + (i + 1) + "个线程的下载区间为:"
                                    + startIndex + "-" + endIndex);
                            new DownloadThread(startIndex, endIndex, path, i)
                                    .start();
                        }
                    }
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            };
        }.start();
    }
    class DownloadThread extends Thread{
        private int lastProgress;
        private int startIndex,endIndex,threadId;
        private String path;
        public DownloadThread(int startIndex,int endIndex,String path,int threadId) {
            this.startIndex = startIndex;
            this.endIndex = endIndex;
            this.path = path;
            this.threadId = threadId;
        }
        @Override
        public void run() {
            try {
                //建立进度临时文件,其实这时还没有创建。当往文件里写东西的时候才创建。
                File progressFile = new File(Environment.getExternalStorageDirectory(), threadId+".txt");
                //判断临时文件是否存在,存在表示已下载过,没下完而已
                if (progressFile.exists()) {
                    FileInputStream fis = new FileInputStream(progressFile);
                    BufferedReader br = new BufferedReader(new InputStreamReader(fis));
                    //从进度临时文件中读取出上一次下载的总进度,然后与原本的开始位置相加,得到新的开始位置
                    lastProgress = Integer.parseInt(br.readLine());
                    startIndex += lastProgress;

                    //断点续传,更新上次下载的进度条
                    currentProgress += lastProgress;
                    pb.setProgress(currentProgress);
                    Message msg = Message.obtain();
                    msg.what = 0x1;
                    mHandler.sendMessage(msg);

                    br.close();
                    fis.close();
                }
                //真正请求数据
                URL url = new URL(path);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                conn.setConnectTimeout(3000);
                conn.setReadTimeout(8000);

                //设置本次http请求所请求的数据的区间(这是需要服务器那边支持断点),格式需要这样写,不能写错
                conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
                //请求部分数据,响应码是206(注意响应码是206)
                if (conn.getResponseCode() == 206) {
                    //此时流中只有1/3原数据
                    InputStream is = conn.getInputStream();
                    File file = new File(Environment.getExternalStorageDirectory(),fileName);
                    RandomAccessFile raf = new RandomAccessFile(file, "rwd");
                    //把文件的写入位置移动至startIndex
                    raf.seek(startIndex);

                    byte[] b = new byte[1024];
                    int len = 0;
                    int total = lastProgress;
                    while ((len = is.read(b)) != -1) {
                        raf.write(b, 0, len);
                        total += len;
                        System.out.println("线程" + threadId + "下载了" + total);
                        //生成一个专门用来记录下载进度的临时文件
                        RandomAccessFile progressRaf = new RandomAccessFile(progressFile, "rwd");
                        //每次读取流里数据之后,同步把当前线程下载的总进度写入进度临时文件中
                        progressRaf.write((total + "").getBytes());
                        progressRaf.close();

                        //下载时更新进度条
                        currentProgress += len;
                        pb.setProgress(currentProgress);
                        Message msg = Message.obtain();
                        msg.what = 0x1;
                        mHandler.sendMessage(msg);
                    }
                    System.out.println("线程" + threadId + "下载完成");
                    raf.close();

                    //每完成一个线程就+1
                    finishedThread ++;
                    //等标志位等于线程数的时候就说明线程全部完成了
                    if (finishedThread == THREADCOUNT) {
                        for (int i = 0; i < finishedThread; i++) {
                            //将生成的进度临时文件删除
                            File f = new File(Environment.getExternalStorageDirectory(),i + ".txt");
                            f.delete();
                        }
                    }
                }
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (ProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

源码下载:http://download.csdn.net

时间: 2024-10-11 07:44:50

android开发笔记之多线程下载及断点续传的相关文章

Android开发笔记(一百零九)利用网盘实现云存储

网盘存储 个人开发者往往没有自己的后台服务器,但同时又想获取app的运行信息,这就要借助于第三方的网络存储(也叫网盘.云盘.微盘等等).通过让app自动在网盘上存取文件,可以间接实现后台服务器的存储功能,同时开发者也能及时找到app的用户信息. 曾几何时,各大公司纷纷推出免费的个人网盘服务,还开放了文件管理api给开发者调用,一时间涌现了网盘提供商的八大金刚:百度网盘.阿里云.华为网盘.腾讯微云.新浪微盘.360云盘.金山快盘.115网盘.可是好景不长,出于盈利.监管等等因素,各大网盘开放平台要

【转】Android开发笔记(序)写在前面的目录

原文:http://blog.csdn.net/aqi00/article/details/50012511 知识点分类 一方面写写自己走过的弯路掉进去的坑,避免以后再犯:另一方面希望通过分享自己的经验教训,与网友互相切磋,从而去芜存菁进一步提升自己的水平.因此博主就想,入门的东西咱就不写了,人不能老停留在入门上:其次是想拾缺补漏,写写虽然小众却又用得着的东西:另外就是想以实用为主,不求大而全,但求小而精:还有就是有的知识点是java的,只是Android开发也会经常遇上,所以蛮记下来.个人的经

【幻化万千戏红尘】qianfengDay27-HttpURLConnection,OkHttpClient,,多线程下载且断点续传基础学习:

课程回顾: Servlet:java语言开发的运行在服务器上的开发步骤:1.创建Servlet类2.重写doGet或doPost方法3.运行在服务器 生命周期:1.初始化2.服务3.销毁 URL:统一资源定位符,网址openConnection 今日内容:Http协议:超文本传输协议常用方式:1.HttpURLConnection2.okHttp HttpURLConnection的使用步骤:1.创建URL对象---URL url=new URL("网址");2.获取连接对象--Htt

Android开发笔记(一百一十六)网络学习资源

知名网站 本系列的开发笔记,对Android开发来说只是沧海一瓢,还有更多的技术等待我们去汲取.下面列出几个常用的开发网站,供初学者上路: 首先当然是国内首屈一指的技术网站csdn啦,csdn提供了众多频道,包括博客.论坛.下载.问答等等,其中博客专栏提供了最新的技术文章,值得推荐.csdn博客专栏的地址是 http://blog.csdn.net/column.html 下面是csdn博客专栏的网页截图: 其次是国外有名的开源网站GitHub,这里有众多的开源项目源码,是开发者分享代码的乐园.

ANDROID开发笔记(一)

manifest, 英['m?n?fest] vt. 显示,表明;证明;使显现 adj. 明白的,明显的 n. 货单,旅客名单 wrap_content, 根据实际内容调整   原来新版的ADB已经支持无线连接了,这样对于我经常使用ADB安装软件的用户可谓是一大福音,这意味着91助手.腕豆夹这类的软件估计在不久的将来也能支持这个功能了,当然前提是你必须下载ADB(PC)软件,另外在手机侧你需要安装一款ADB wireless widget这个插件.使用方法: 1. 先在手机上执行adb wire

Android开发笔记(一百三十四)协调布局CoordinatorLayout

协调布局CoordinatorLayout Android自5.0之后对UI做了较大的提升,一个重大的改进是推出了MaterialDesign库,而该库的基础即为协调布局CoordinatorLayout,几乎所有的design控件都依赖于该布局.协调布局的含义,指的是内部控件互相之前的动作关联,比如在A视图的位置发生变化之时,B视图的位置也按照某种规则来变化,仿佛弹钢琴有了协奏曲一般. 使用CoordinatorLayout时,要注意以下几点:1.导入design库:2.根布局采用androi

Android开发笔记(一百三十二)矢量图形与矢量动画

矢量图形VectorDrawable 与水波图形RippleDrawable一样,矢量图形VectorDrawable也是Android5.0之后新增的图形类.矢量图不同于一般的图形,它是由一系列几何曲线构成的图像,这些曲线以数学上定义的坐标点连接而成.具体到实现上,则需开发者提供一个xml格式的矢量图形定义,然后系统根据矢量定义自动计算该图形的绘制区域.因为绘图结果是动态计算得到,所以不管缩放到多少比例,矢量图形都会一样的清晰,不像位图那样拉大后会变模糊. 矢量图形的xml定义有点复杂,其结构

《ArcGIS Runtime SDK for Android开发笔记》——离在线一体化技术:概述

1.前言 数据生产和数据展示是常见的两大专业级移动GIS应用场景,这里我们针对数据生产环节的ArcGIS的离在线一体化技术给大家做一个基本的介绍和梳理. 使用ArcGIS离在线一体化技术首先需要以下基础环境: ArcGIS for Desktop 10.2.1以上版本 ArcGIS for Server 10.2.1以上版本 使用PostgreSQL.Microsoft SQL Server.或 Oracle 设置企业级地理数据库ArcSDE. 再次在使用同步功能是必须给要素添加GlobleID

Android开发笔记(一百四十一)读取PPT和PDF文件

读取ppt文件 读取纯文本 上一篇博文讲到在Android上如何读取word文件内容,那么office三剑客中还剩ppt文件的读取.前面解析word文件和excel文件时,都用到了poi库读取文件内容,对于ppt一样也可以通过poi读取幻灯片中的文本.HSLFSlideShow类就是poi中专门用于解析幻灯片的工具类,每张幻灯片又分别由单独的HSLFSlide类处理,幻灯片中的具体图文内容则由HSLFTextParagraph和HSLFTextRun进行分辨. 下面是使用poi解析ppt文件(2