详解Android中的多线程断点下载

首先来看一下多线程下载的原理。多线程下载就是将同一个网络上的原始文件根据线程个数分成均等份,然后每个单独的线程下载对应的一部分,然后再将下载好的文件按照原始文件的顺序“拼接”起来就构

成了完整的文件了。这样就大大提高了文件的下载效率。对于文件下载来说,多线程下载是必须要考虑的环节。

多线程下载大致可分为以下几个步骤:

一.获取服务器上的目标文件的大小

显然这一步是需要先访问一下网络,只需要获取到目标文件的总大小即可。目的是为了计算每个线程应该分配的下载任务。

二. 在本地创建一个跟原始文件同样大小的文件

在本地可以通过RandomAccessFile 创建一个跟目标文件同样大小的文件,该api 支持文件任意位置的读写操作。这样就给多线程下载提供了方便,每个线程只需在指定起始和结束脚标范围内写数据即可。

三.计算每个线程下载的起始位置和结束位置

我们可以把原始文件当成一个字节数组,每个线程只下载该“数组”的指定起始位置和指定结束位置之间的部分。在第一步中我们已经知道了“数组”的总长度。因此只要再知道总共开启的线程的个数就好计算每个线程要下载的范围了。每个线程需要下载的字节个数(blockSize)=总字节数(totalSize)/线程数(threadCount)。       假设给线程按照0,1,2,3...n 的方式依次进行编号,那么第n 个线程下载文件的范围为:

起始脚标startIndex=n*blockSize。

结束脚标endIndex=(n-1)*blockSize-1。

考虑到totalSize/threadCount 不一定能整除,因此对已最后一个线程应该特殊处理,最后一个线程的起始脚标计算公式不变,但是结束脚标为endIndex=totalSize-1即可。

四.开启多个子线程开始下载

在子线程中实现读流操作,将conn.getInputStream()读到RandomAccessFile中。

五.记录下载进度

为每一个单独的线程创建一个临时文件,用于记录该线程下载的进度。对于单独的一个线程,每下载一部分数据就在本地文件中记录下当前下载的字节数。这样子如果下载任务异常终止了,那么下次重新开始下载时就可以接着上次的进度下载。

六. 删除临时文件

当多个线程都下载完成之后,最后一个下载完的线程将所有的临时文件删除。

Android有界面可以跟用户进行良好的交互,在界面上让用户输入原文件地址、线程个数,然后点击确定开始下载。为了让用户可以清晰的看到每个线程下载的进度根据线程个数动态的生成等量的进度条(ProgressBar)。ProgressBar 是一个进度条控件,用于显示一项任务的完成进度。其有两种样式,一种是圆形的,该种样式是系统默认的,由于无法显示具体的进度值,适合用于不确定要等待多久的情形下;另一种是长条形的,此类进度条有两种颜色,高亮颜色代表任务完成的总进度。对于我们下载任务来说,由于总任务(要下载的字节数)是明确的,当前已经完成的任务(已经下载的字节数)也是明确的,因此特别适合使用后者。由于在我们的需求里ProgressBar 是需要根据线程的个数动态添加的,而且要求是长条形的。因此可以事先在布局文件中编写好ProgressBar 的样式。当需要用到的时候再将该布局填充起来。ProgressBar 的max 属性代表其最大刻度值,progress 属性代表当前进度值。使用方法如下:

ProgressBar.setMax(int max);设置最大刻度值。

ProgressBar.setProgress(int progress);设置当前进度值。

给ProgressBar 设置最大刻度值和修改进度值可以在子线程中操作的,其内部已经特殊处理过了,因此不需要再通过handler发送Message 让主线程修改进度。

下面就给出我们自己写的安卓环境下的多线程。

多线程下载界面布局如下,三个进度条分别表示三个子线程的下载进度。

<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"
android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
 tools:context=".MainActivity" >
 <EditText
 android:id="@+id/et_path"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:hint="请输入要下载的文件资源路径"
android:text="http://192.168.1.104:8080/gg.exe" />
 <Button
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:onClick="download"
 android:text="下载" />
 <ProgressBar
 android:id="@+id/pb0"
 style="?android:attr/progressBarStyleHorizontal"
 android:layout_width="match_parent"
 android:layout_height="wrap_content" />
 <ProgressBar
 android:id="@+id/pb1"
 style="?android:attr/progressBarStyleHorizontal"
 android:layout_width="match_parent"
 android:layout_height="wrap_content" />
 <ProgressBar
 android:id="@+id/pb2"
 style="?android:attr/progressBarStyleHorizontal"
 android:layout_width="match_parent"
 android:layout_height="wrap_content" />
</LinearLayout>

多线程下载的内部逻辑如下,其实这在开头已经有了,只不过是代码的实现了。

 public class MainActivity extends Activity {
 private EditText et_path;
 private ProgressBar pb0;
 private ProgressBar pb1;
 private ProgressBar pb2;
 /**
 * 开启几个线程从服务器下载数据
 */
 public static int threadCount = 3;
 public static int runningThreadCount;
 private String path;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 //初始化控件
 et_path = (EditText) findViewById(R.id.et_path);
 pb0 = (ProgressBar) findViewById(R.id.pb0);
 pb1 = (ProgressBar) findViewById(R.id.pb1);
 pb2 = (ProgressBar) findViewById(R.id.pb2);
 }
 //下载按钮的点击事件
 public void download(View view) {
 path = et_path.getText().toString().trim();
 if (TextUtils.isEmpty(path) || (!path.startsWith("http://"))) {
 Toast.makeText(this, "对不起路径不合法", 0).show();
 return;
 }
 new Thread(){
 public void run() {
 try {
 //1.获取服务器上的目标文件的大小
 URL url = new URL(path);
 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 conn.setConnectTimeout(5000);
 conn.setRequestMethod("GET");
 int code = conn.getResponseCode();
 if (code == 200) {
 int length = conn.getContentLength();
 System.out.println("服务器文件的长度为:" + length);
 //2.在本地创建一个跟原始文件同样大小的文件
 RandomAccessFile raf = new RandomAccessFile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+getFileName(path), "rw");
 raf.setLength(length);
 raf.close();
 //3.计算每个线程下载的起始位置和结束位置
 int blocksize = length / threadCount;
 runningThreadCount = threadCount;
 for (int threadId = 0; threadId < threadCount; threadId++) {
 int startIndex = threadId * blocksize;
 int endIndex = (threadId + 1) * blocksize - 1;
 if (threadId == (threadCount - 1)) {
 endIndex = length - 1;
 }
 //4.开启多个子线程开始下载
 new DownloadThread(threadId, startIndex, endIndex).start();
 }
 }
 } catch (Exception e) {
 e.printStackTrace();
 }
 };
 }.start();
 }
 private class DownloadThread extends Thread {
 /**
 * 线程id
 */
 private int threadId;
 /**
 * 线程下载的理论开始位置
 */
 private int startIndex;
 /**
 * 线程下载的结束位置
 */
 private int endIndex;
 /**
 * 当前线程下载到文件的那一个位置了.
 */
 private int currentPosition;
 public DownloadThread(int threadId, int startIndex, int endIndex) {
 this.threadId = threadId;
 this.startIndex = startIndex;
 this.endIndex = endIndex;
 System.out.println(threadId + "号线程下载的范围为:" + startIndex
 + " ~~ " + endIndex);
 currentPosition = startIndex;
 }
 @Override
 public void run() {
 try {
 URL url = new URL(path);
 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 //检查当前线程是否已经下载过一部分的数据了
 File info = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position");
 RandomAccessFile raf = new RandomAccessFile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+getFileName(path), "rw");
 if(info.exists()&&info.length()>0){
 FileInputStream fis = new FileInputStream(info);
 BufferedReader br = new BufferedReader(new InputStreamReader(fis));
 currentPosition = Integer.valueOf(br.readLine());
 conn.setRequestProperty("Range", "bytes="+currentPosition+"-"+endIndex);
 System.out.println("原来有下载进度,从上一次终止的位置继续下载"+"bytes="+currentPosition+"-"+endIndex);
 fis.close();
 raf.seek(currentPosition);//每个线程写文件的开始位置都是不一样的.
 }else{
 //告诉服务器 只想下载资源的一部分
 conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex);
 System.out.println("原来没有有下载进度,新的下载"+ "bytes="+startIndex+"-"+endIndex);
 raf.seek(startIndex);//每个线程写文件的开始位置都是不一样的.
 }
 InputStream is = conn.getInputStream();
 byte[] buffer = new byte[1024];
 int len = -1;
 while((len = is.read(buffer))!=-1){
 //把每个线程下载的数据放在自己的空间里面.
// System.out.println("线程:"+threadId+"正在下载:"+new String(buffer));
 raf.write(buffer,0, len);
 //5.记录下载进度
 currentPosition+=len;
 File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position");
 RandomAccessFile fos = new RandomAccessFile(file,"rwd");
 //System.out.println("线程:"+threadId+"写到了"+currentPosition);
 fos.write(String.valueOf(currentPosition).getBytes());
 fos.close();//fileoutstream数据是不一定被写入到底层设备里面的,有可能是存储在缓存里面.
 //raf 的 rwd模式,数据是立刻被存储到底层硬盘设备里面.
 //更新进度条的显示
 int max = endIndex - startIndex;
 int progress = currentPosition - startIndex;
 if(threadId==0){
 pb0.setMax(max);
 pb0.setProgress(progress);
 }else if(threadId==1){
 pb1.setMax(max);
 pb1.setProgress(progress);
 }else if(threadId==2){
 pb2.setMax(max);
 pb2.setProgress(progress);
 }
 }
 raf.close();
 is.close();
 System.out.println("线程:"+threadId+"下载完毕了...");
 File f = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position");
 f.renameTo(new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position.finish"));
 synchronized (MainActivity.class) {
 runningThreadCount--;
 //6.删除临时文件
 if(runningThreadCount<=0){
 for(int i=0;i<threadCount;i++){
 File ft = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+i+".position.finish");
 ft.delete();
 }
 }
 }
 } catch (Exception e) {
 e.printStackTrace();
 }
 }
 }
 /**
 * 获取一个文件名称
 * @param path
 * @return
 */
 public String getFileName(String path){
 int start = path.lastIndexOf("/")+1;
 return path.substring(start);
 }
}

最后别忘了添加权限,在该工程中不仅用到了网络访问还用到了sdcard 存储,因此需要添加两个权限。

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

另外,xUtils同样可以实现多线程下载。xUtils 是开源免费的Android 工具包,代码托管在github 上。目前xUtils 主要有四大模块:DbUtils 模块,主要用于操作数据库的框架。ViewUtils 模块,通过注解的方式可以对UI,资源和事件绑定进行管理。HttpUtils 模块,提供了方便的网络访问,断点续传等功能。BitmapUtils 模块,提供了强大的图片处理工具。我们在这里只简单实用xUtils 工具中的HttpUtils 工具。第三方包的使用较为简单,直接拷贝xUtils的jar包到libs目录下,然后添加依赖。

接下来就可以使用xUtils中的httpUtils的功能了:

HttpUtils http = new HttpUtils();
 /**
 * 参数1:原文件网络地址
 * 参数2:本地保存的地址
 * 参数3:是否支持断点续传,true:支持,false:不支持
 * 参数4:回调接口,该接口中的方法都是在主线程中被调用的,
 * 也就是该接口中的方法都可以修改UI
 */
 http.download(path, "/mnt/sdcard/xxx.exe", true, new RequestCallBack<File>() {
 //下载成功后调用一次
 @Override
 public void onSuccess(ResponseInfo<File> arg0) {
 Toast.makeText(MainActivity.this, "下载成功", 0).show();
 }
 /**
 * 每下载一部分就被调用一次,通过该方法可以知道当前下载进度
 * 参数1:原文件总字节数
 * 参数2:当前已经下载好的字节数
 * 参数3:是否在上传,对于下载,该值为false
 */
 @Override
 public void onLoading(long total, long current, boolean isUploading) {
 pb0.setMax((int) total);
 pb0.setProgress((int) current);
 super.onLoading(total, current, isUploading);
 }
 //失败后调用一次
 @Override
 public void onFailure(HttpException arg0, String arg1) {
 Toast.makeText(MainActivity.this, "下载失败"+arg1, 0).show();
 }
 });

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持我们!

(0)

相关推荐

  • Android实现多线程断点下载的方法

    本文实例讲述了Android实现多线程断点下载的方法.分享给大家供大家参考.具体实现方法如下: package cn.itcast.download; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputSt

  • Android入门:多线程断点下载详细介绍

    本案例在于实现文件的多线程断点下载,即文件在下载一部分中断后,可继续接着已有进度下载,并通过进度条显示进度.也就是说在文件开始下载的同时,自动创建每个线程的下载进度的本地文件,下载中断后,重新进入应用点击下载,程序检查有没有本地文件的存在,若存在,获取本地文件中的下载进度,继续进行下载.当下载完成后,自动删除本地文件. 一.多线程断点下载介绍 所谓的多线程断点下载就是利用多线程下载,并且可被中断,如果突然没电了,重启手机后可以继续下载,而不需要重新下载: 利用的技术有:SQLite存储各个线程的

  • Android原生实现多线程断点下载实例代码

    各位父老乡亲,我单汉三又回来了,今天为大家带来一个用原生的安卓写的多线程断点下载Demo. 通过本文你可以学习到: SQLite的基本使用,数据库的增删改查. Handler的消息处理与更新UI. Service(主要用于下载)的进阶与使用. 原生的json文件解析(多层嵌套). RandomAccessFile的基本使用,可以将文件分段. 基于HttpURLConnection的大文件下载. 上面内容结合,实现多线程,断点下载. Demo是在TV上运行的,图片显示的问题不要纠结了. 文件下载的

  • android多线程断点下载-带进度条和百分比进度显示效果

    android多线程断点下载,带进度条和百分比显示,断点下载的临时数据保存到SD卡的文本文档中,建议可以保存到本地数据库中,这样可以提高存取效率,从而提高系统性能. 效果: 打开软件: 下载中: 下载完毕: 附代码如下: package com.yy.multiDownloadOfBreakPoint; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.R

  • 详解Android中的多线程断点下载

    首先来看一下多线程下载的原理.多线程下载就是将同一个网络上的原始文件根据线程个数分成均等份,然后每个单独的线程下载对应的一部分,然后再将下载好的文件按照原始文件的顺序"拼接"起来就构 成了完整的文件了.这样就大大提高了文件的下载效率.对于文件下载来说,多线程下载是必须要考虑的环节. 多线程下载大致可分为以下几个步骤: 一.获取服务器上的目标文件的大小 显然这一步是需要先访问一下网络,只需要获取到目标文件的总大小即可.目的是为了计算每个线程应该分配的下载任务. 二. 在本地创建一个跟原始

  • 详解Android中图片的三级缓存及实例

    详解Android中图片的三级缓存及实例 为什么要使用三级缓存 如今的 Android App 经常会需要网络交互,通过网络获取图片是再正常不过的事了 假如每次启动的时候都从网络拉取图片的话,势必会消耗很多流量.在当前的状况下,对于非wifi用户来说,流量还是很贵的,一个很耗流量的应用,其用户数量级肯定要受到影响 特别是,当我们想要重复浏览一些图片时,如果每一次浏览都需要通过网络获取,流量的浪费可想而知 所以提出三级缓存策略,通过网络.本地.内存三级缓存图片,来减少不必要的网络交互,避免浪费流量

  • 详解Android中Handler的内部实现原理

    本文主要是对Handler和消息循环的实现原理进行源码分析,如果不熟悉Handler可以参见博文<详解Android中Handler的使用方法>,里面对Android为何以引入Handler机制以及如何使用Handler做了讲解. 概括来说,Handler是Android中引入的一种让开发者参与处理线程中消息循环的机制.我们在使用Handler的时候与Message打交道最多,Message是Hanlder机制向开发人员暴露出来的相关类,可以通过Message类完成大部分操作Handler的功

  • 详解Android 中AsyncTask 的使用

    详解Android 中AsyncTask 的使用 1.首先我们来看看AsyncTask 的介绍:   Handler 和 AsyncTask 都是android 中用来实现异步任务处理的方式:其中: Handler 实例向 UI 线程发送消息,完成界面更新, 优点:对整个过程控制的比较精细:         缺点:代码相对臃肿,多个任务同时执行时,不易对线程进行精确的控制: AsyncTask :比Handler 更轻量级一些,适用于简单的异步处理: 优点:简单 | 快捷 | 过程可控:    

  • 详解Android中Service AIDL的使用

    目录 前言 Service基本用法--本地服务 远程服务 -- AIDL 服务端 客户端 前言 有些朋友可能是从事开发工作的时间不是特别的长,所以觉得Service相对与另外两个组件activity.broadcast receiver来说,使用可能并不是特别的多,所以对Service来说,理解不是特别的深入,只是有一个大概的概念,今天就和一块来走一下Service,希望能够帮助到大家对Service有更深入的理解. Service基本用法--本地服务 我们知道服务分为本地服务和远程服务,而本地

  • 详解Android中Intent对象与Intent Filter过滤匹配过程

    如果对Intent不是特别了解,可以参见博文<详解Android中Intent的使用方法>,该文对本文要使用的action.category以及data都进行了详细介绍.如果想了解在开发中常见Intent的使用,可以参见<Android中Intent习惯用法>. 本文内容有点长,希望大家可以耐心读完. 本文在描述组件在manifest中注册的Intent Filter过滤器时,统一用intent-filter表示. 一.概述 我们知道,Intent是分两种的:显式Intent和隐式

  • 详解 Android中Libgdx使用ShapeRenderer自定义Actor解决无法接收到Touch事件的问题

    详解 Android中Libgdx使用ShapeRenderer自定义Actor解决无法接收到Touch事件的问题 今天在项目中实现了一个效果,主要是画一个圆.为了后续使用方便,将这个圆封装在一个自定义Actor(CircleActot)中,后续想显示一个圆的时候,只要创建一个CircleActor中即可. 部分代码如下所示: package com.ef.smallstar.unitmap.widget; import android.content.res.Resources; import

  • 详解Android中的Service

    Service简介: Service是被设计用来在后台执行一些需要长时间运行的操作. Android由于允许Service在后台运行,甚至在结束Activity后,因此相对来说,Service相比Activity拥有更高的优先级. 创建Service: 要创建一个最基本的Service,需要完成以下工作:1)创建一个Java类,并让其继承Service 2)重写onCreate()和onBind()方法 其中,onCreate()方法是当该Service被创建时执行的方法,onBind()是该S

  • 详解Android中获取软键盘状态和软键盘高度

    详解Android中获取软键盘状态和软键盘高度 应用场景 在Android应用中有时会需要获取软键盘的状态(即软键盘是显示还是隐藏)和软键盘的高度.这里列举了一些可能的应用场景. 场景一 当软键盘显示时,按下返回键应当是收起软键盘,而不是回退到上一个界面,但部分机型在返回键处理上有bug,按下返回键后,虽然软键盘会自动收起,但不会消费返回事件,导致Activity还会收到这次返回事件,执行回退操作,这时就需要判断,如果软键盘刚刚由显示变为隐藏状态,就不执行回退操作. 场景二 当软键盘弹出后,会将

  • 详解Android中fragment和viewpager的那点事儿

    在之前的博文<Android 中使用 ViewPager实现屏幕页面切换和页面轮播效果>和<详解Android中Fragment的两种创建方式>以及<Android中fragment与activity之间的交互(两种实现方式)>中我们介绍了ViewPager以及Fragment各自的使用场景以及不同的实现方式. 那如果将他们两结合起来,会不会擦出点火花呢,答案是肯定的.之前在介绍ViewPager时,我们实现了多个ImageView的切换,并配合更新导航原点的状态.那我

随机推荐