android开发笔记之多线程下载及断点续传
2016-06-02 16:22
651 查看
今天我们来接触一下多线程下载,当然也包括断点续传,我们可以看到
很多下载器,当开通会员的时候下载东西的速度就变得快了许多,这是为什么呢?这就是跟今天讲的多线程有关系了,其实就是多开了几个线程一起下载罢了。当然真正的多线程下载要比这个复杂,要考虑很多问题。
做个不恰当的比喻:
假如我们把一个服务器上的文件看作是一个水缸里的水的话,那么多线程下载就相当于从水缸上打了多个小孔,然后塞进去小管道进行抽水。呵呵,也许这个比喻不够准确。
效果:
这里下载的是本地服务器上的文件,你们可以下载网络上的一些文件。
先来看看多线程下载的原理吧:
通常服务器同时与多个用户连接,用户之间共享带宽。如果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
Mactivity.java
源码下载:http://download.csdn.net
很多下载器,当开通会员的时候下载东西的速度就变得快了许多,这是为什么呢?这就是跟今天讲的多线程有关系了,其实就是多开了几个线程一起下载罢了。当然真正的多线程下载要比这个复杂,要考虑很多问题。
做个不恰当的比喻:
假如我们把一个服务器上的文件看作是一个水缸里的水的话,那么多线程下载就相当于从水缸上打了多个小孔,然后塞进去小管道进行抽水。呵呵,也许这个比喻不够准确。
效果:
这里下载的是本地服务器上的文件,你们可以下载网络上的一些文件。
先来看看多线程下载的原理吧:
通常服务器同时与多个用户连接,用户之间共享带宽。如果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
相关文章推荐
- Android 图片模糊、高斯模糊、毛玻璃的三种实现方法
- Android基于google Zxing实现二维码的生成
- Android:使用Gson解析复杂的JSON数据
- android球形水波百分比控件代码
- android AsyncTask介绍
- activity 下 点击空白处隐藏键盘
- Android中View的滑动冲突
- Android应用签名
- 【Android基础】六、Fragment
- Android群英传读书笔记第八章(Activity与Activity调用栈分析)
- Android Studio 使用统计代码插件
- Android 快速开发系列 ORMLite 框架最佳实践
- Android Studio如何安装插件
- 对 Android 开发者有益的 40 条优化建议
- Android仿网易客户端顶部导航栏效果
- 【Android】快捷自定义Dialog对话框并获取属性
- android自定义控件系列
- Android 使用EventBus实现菜单
- 重启android盒子
- [Android Studio系列(五)] Android Studio手动配置Gradle的方法