首先来看一下多线程下载的原理。多线程下载就是将同一个网络上的原始文件根据线程个数分成均等份,然后每个单独的线程下载对应的一部分,然后再将下载好的文件按照原始文件的顺序“拼接”起来就构
成了完整的文件了。这样就大大提高了文件的下载效率。对于文件下载来说,多线程下载是必须要考虑的环节。

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

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

二. 在本地创建一个跟原始文件同样大小的文件
             在本地可以通过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();
}
});

Android中的多线程断点下载的更多相关文章

  1. Android(java)学习笔记216:多线程断点下载的原理(Android实现)

    之前在Android(java)学习笔记215中,我们从JavaSE的角度去实现了多线程断点下载,下面从Android角度实现这个断点下载: 1.新建一个Android工程: (1)其中我们先实现布局 ...

  2. 我的Android进阶之旅------>Android基于HTTP协议的多线程断点下载器的实现

    一.首先写这篇文章之前,要了解实现该Android多线程断点下载器的几个知识点 1.多线程下载的原理,如下图所示 注意:由于Android移动设备和PC机的处理器还是不能相比,所以开辟的子线程建议不要 ...

  3. Android(java)学习笔记159:多线程断点下载的原理(Android实现)

    之前在Android(java)学习笔记215中,我们从JavaSE的角度去实现了多线程断点下载,下面从Android角度实现这个断点下载: 1. 新建一个Android工程: (1)其中我们先实现布 ...

  4. Android多线程断点下载

    到华为后,信息管理特别严格,文件不能外发.所以好久都没写博客了,今天周日,老婆非要我学习.就闲来无事,写一篇博客,呵呵-- 前段时间,项目中提到了断点下载apk并静默安装的需求.本打算用应用市场成熟的 ...

  5. Android实现网络多线程断点续传下载(转)

    本示例介绍在Android平台下通过HTTP协议实现断点续传下载. 我们编写的是Andorid的HTTP协议多线程断点下载应用程序.直接使用单线程下载HTTP文件对我们来说是一件非常简单的事.那么,多 ...

  6. Android实现网络多线程断点续传下载

    本示例介绍在Android平台下通过HTTP协议实现断点续传下载. 我们编写的是Andorid的HTTP协议多线程断点下载应用程序.直接使用单线程下载HTTP文件对我们来说是一件非常简单的事.那么,多 ...

  7. andoid 多线程断点下载

    本示例介绍在Android平台下通过HTTP协议实现断点续传下载. 我们编写的是Andorid的HTTP协议多线程断点下载应用程序.直接使用单线程下载HTTP文件对我们来说是一件非常简单的事.那么,多 ...

  8. Android中的多线程断点续传

    Android多线程断点下载的代码流程解析: 运行效果图: 实现流程全解析: Step 1:创建一个用来记录线程下载信息的表 创建数据库表,于是乎我们创建一个数据库的管理器类,继承SQLiteOpen ...

  9. java多线程断点下载原理(代码实例演示)

    原文:http://www.open-open.com/lib/view/open1423214229232.html 其实多线程断点下载原理,很简单的,那么我们就来先了解下,如何实现多线程的断点下载 ...

随机推荐

  1. 【AR实验室】ARToolKit之制作自己的Marker/NFT

    0x00 - 前言 看过example后,就会想自己动动手,这里改改那里修修.我们先试着添加自己喜欢的marker/nft进行识别. 比如我做了一个法拉利的marker: 还有网上找了一个法拉利log ...

  2. .NET Core全新路线图

    .NET Core / ASP.NET Core 1 RTM发布两周后,社区也很积极,收到了非常多的反馈,上周五微软的scott Hunter 在dotnet团队官方博客上发布了.NET Core全新 ...

  3. 领域驱动和MVVM应用于UWP开发的一些思考

    领域驱动和MVVM应用于UWP开发的一些思考 0x00 起因 有段时间没写博客了,其实最近本来是根据梳理的MSDN上的资料(UWP开发目录整理)有条不紊的进行UWP学习的.学习中有了心得体会或遇到了问 ...

  4. Response.Redirect引起的性能问题分析

    现象: 最近做的一个系统通过单点登录(SSO) 技术验证用户登录.用户在SSO 系统上通过验证后,跳转到该系统的不同模块.而跳转的时间一直维持子啊几分钟左右. 分析步骤: 在问题复现时抓取Hang d ...

  5. 【转】java通用URL接口地址调用方式GET和POST方式

    java通用URL接口地址调用方式GET和POST方式,包括建立请求和设置请求头部信息等等......... import java.io.ByteArrayOutputStream; import ...

  6. 树莓派 基于Web的温度计

    前言:家里的树莓派吃灰很久,于是拿出来做个室内温度展示也不错. 板子是model b型. 使用Python开发,web框架是flask,温度传感器是ds18b20 1 硬件连接 ds18b20的vcc ...

  7. iOS app内存分析套路

    iOS app内存分析套路 Xcode下查看app内存使用情况有2中方法: Navigator导航栏中的Debug navigator中的Memory Instruments 一.Debug navi ...

  8. MySQL 数据库双向同步复制

    MySQL 复制问题的最后一篇,关于双向同步复制架构设计的一些设计要点与制约. 问题和制约 数据库的双主双写并双向同步场景,主要考虑数据完整性.一致性和避免冲突.对于同一个库,同一张表,同一个记录中的 ...

  9. Linux实战教学笔记01:计算机硬件组成与基本原理

    标签(空格分隔): Linux实战教学笔记 第1章 如何学习Linux 要想学好任何一门学问,不仅要眼睛看,耳朵听,还要动手记,勤思考,多交流甚至尝试着去教会别人. 第2章 服务器 2.1 运维的基本 ...

  10. Mono产品生命周期

    软件生命周期 同任何事物一样,一个软件产品或软件系统也要经历孕育.诞生.成长.成熟.衰亡等阶段,一般称为软件生命周期(软件生存周期) .软件生命周期模型是指人们为开发更好的软件而归纳总结的软件生命周期 ...