Android 学习笔记之使用多线程实现断点下载...
PS:莫名其妙的迷茫...
学习内容:
1.使用多线程实现文件下载...
多线程下载是加快下载速度的一种方式..通过开启多个线程去执行一个任务..可以使任务的执行速度变快..多线程的任务下载时常都会使用得到..比如说我们手机内部应用宝的下载机制..一定是通过使用了多线程创建的下载器..并且这个下载器可以实现断点下载..在任务被强行终止之后..下次可以通过触发按钮来完成断点下载...那么如何实现断点下载这就是一个问题了..
首先我们需要明确一点就是多线程下载器通过使用多个线程对同一个任务进行下载..但是这个多线程并不是线程的数目越多,下载的速度就越快..当线程增加的很多的时候,单个线程执行效率也会变慢..因此线程的数目需要有一个限度..经过楼主亲自测试..多线程下载同一个任务时,线程的数目5-8个算是比较高效的..当线程的数量超过10个之后,那么多线程的效率反而就变慢了(前提:网速大体相同的时候..)
那么在实现多线程下载同一个任务的时候我们需要明白其中的道理..下面先看一张附加图..
这个图其实就很简单的说明了其中的原理..我们将一个任务分成多个部分..然后开启多个线程去下载对应的部分就可以了..那么首先要解决的问题就是如何使各自的线程去下载各自对应的任务,不能越界..那么我们来看看具体的实现过程..首先我们使用普通Java代码去实现..最后将Java代码移植到Android就可以了..
public class Download { public static int threadCount = 5; //线程开启的数量..
public static void main(String[] args) {
// TODO Auto-generated method stub String path = "http://192.168.199.172:8080/jdk.exe";
try {
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
int status = conn.getResponseCode(); if(status == 200){
int length = conn.getContentLength(); System.out.println(length); int blocksize = length/threadCount; //将文件长度进行平分.. for(int threadID=1; threadID<=threadCount;threadID++){ int startIndex = (threadID-1)*blocksize; //开始位置的求法..
int endIndex = threadID*blocksize -1; //结束位置的求法.. /**
* 如果一个文件的长度无法整除线程数..
* 那么最后一个线程下载的结束位置需要设置文件末尾..
* */
if(threadID == threadCount){
endIndex = length;
} System.out.println("线程下载位置:"+startIndex+"---"+endIndex);
}
} } catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
这样只是实现了通过连接服务器来获取文件的长度..然后去设置每一个线程下载的开始位置和结束位置..这里只是完成了这些步骤..有了下载的开始位置和结束位置..我们就需要开启线程来完成下载了...因此我们需要自己定义下载的过程...
首先我们需要明确思路:既然是断点下载..那么如果一旦发生了断点情况..我们在下一次进行下载的时候需要从原来断掉的位置进行下载..已经下载过的位置我们就不需要进行下载了..因此我们需要记载每一次的下载记录..那么有了记录之后..一旦文件下载完成之后..这些记录就需要被清除掉...因此明确了这两个地方的思路就很容易书写了..
//下载线程的定义..
public static class DownLoadThread implements Runnable{ private int threadID;
private int startIndex;
private int endIndex;
private String path; public DownLoadThread(int threadID,int startIndex,int endIndex,String path){ this.threadID = threadID;
this.startIndex = startIndex;
this.endIndex = endIndex;
this.path = path; } @Override
public void run() {
// TODO Auto-generated method stub try { //判断上一次是否下载完毕..如果没有下载完毕需要继续进行下载..这个文件记录了上一次的下载位置..
File tempfile =new File(threadID+".txt");
if(tempfile.exists() && tempfile.length()>0){ FileInputStream fis = new FileInputStream(tempfile);
byte buffer[] = new byte[1024];
int leng = fis.read(buffer);
int downlength = Integer.parseInt(new String(buffer,0,leng));//从上次下载后的位置开始下载..重新拟定开始下载的位置..
startIndex = downlength;
fis.close();
} URL url = new URL(path);
HttpURLConnection conn =(HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex); int status = conn.getResponseCode();
//206也表示服务器响应成功..
if(status == 206){ //获取服务器返回的I/O流..然后将数据写入文件当中..
InputStream in = conn.getInputStream(); //文件写入开始..用来保存当前需要下载的文件..
RandomAccessFile raf = new RandomAccessFile("jdk.exe", "rwd");
raf.seek(startIndex); int len = 0;
byte buf[] =new byte[1024]; //记录已经下载的长度..
int total = 0; while((len = in.read(buf))!=-1){ //用于记录当前下载的信息..
RandomAccessFile file =new RandomAccessFile(threadID+".txt", "rwd");
total += len;
file.write((total+startIndex+"").getBytes());
file.close();
//将数据写入文件当中..
raf.write(buf, 0, len); }
in.close();
raf.close(); } } catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
//如果所有的线程全部下载完毕后..也就是任务完成..清除掉所有原来的记录文件..
runningThread -- ;
if(runningThread==0){
for(int i=1;i<threadCount;i++){
File file = new File(i+".txt");
file.delete();
}
}
}
}
}
这样就完成了文件的数据信息的下载..经过测试..一个13M的文件在5个线程共同作用下下载的时间差不多是12秒左右(网速稳定在300k的情况下..带宽越宽..速度就会更快)单个线程下载的时间差不多是15秒左右..这里才缩短了两秒钟的时间..但是我们不要忘记..如果文件过大的话呢?因此楼主亲测了一下一个90M的文件在5个线程同时作用下时间差不多1分20秒左右..而使用一个线程进行下载差不多2分钟左右..这里还是缩短了大量的时间..
因此根据对比,还是使用多个线程来进行下载更加的好一些..虽然Android里的一般应用不会超过50M左右..但是游戏的话一般差不多能达到100-200M左右..因此使用多线程还是能够提高下载的进度和效率..同样我们可以通过使用线程池的方式去开启线程..最后这些线程交给线程池去管理就可以了..
在正常的Java项目中我们书写好了下载代码..就可以移植到我们的Android应用程序当中..但是还是有一些地方需要注意..因此在这里去强调一下..
package com.example.mutithread; import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.app.Activity;
import android.text.TextUtils;
import android.view.Menu;
import android.view.View;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast; public class MainActivity extends Activity { private EditText et;
private ProgressBar pb;
public static int threadCount = 5;
public static int runningThread=5;
public int currentProgress=0; //当前进度值..
private TextView tv; private Handler handler = new Handler(){ @Override
public void handleMessage(Message msg){
switch (msg.what) {
case 1:
Toast.makeText(getApplicationContext(), msg.obj.toString(), 0).show();
break;
case 2:
break;
case 3:
tv.setText("当前进度:"+(pb.getProgress()*100)/pb.getMax());
default:
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
et = (EditText) findViewById(R.id.et);
pb =(ProgressBar) findViewById(R.id.pg);
tv= (TextView) findViewById(R.id.tv);
} public void downLoad(View v){
final String path = et.getText().toString().trim();
if(TextUtils.isEmpty(path)){
Toast.makeText(this, "下载路径错误", Toast.LENGTH_LONG).show();
return ;
} new Thread(){
// String path = "http://192.168.199.172:8080/jdk.exe";
public void run(){
try {
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
int status = conn.getResponseCode(); if(status == 200){
int length = conn.getContentLength();
System.out.println("文件总长度"+length);
pb.setMax(length); RandomAccessFile raf = new RandomAccessFile("/sdcard/setup.exe","rwd");
raf.setLength(length);
raf.close(); //开启5个线程来下载当前资源..
int blockSize = length/threadCount ; for(int threadID=1;threadID<=threadCount;threadID++){ int startIndex = (threadID-1)*blockSize;
int endIndex = threadID*blockSize -1; if(threadID == threadCount){
endIndex = length;
}
System.out.println("线程"+threadID+"下载:---"+startIndex+"--->"+endIndex);
new Thread(new DownLoadThread(threadID, startIndex, endIndex, path)).start() ;
}
} } catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}; }.start();
}
/**
* 下载线程..
* */
public class DownLoadThread implements Runnable{ private int ThreadID;
private int startIndex;
private int endIndex;
private String path; public DownLoadThread(int ThreadID,int startIndex,int endIndex,String path){
this.ThreadID = ThreadID;
this.startIndex = startIndex;
this.endIndex = endIndex;
this.path = path;
}
@Override
public void run() {
// TODO Auto-generated method stub
URL url;
try {
//检查是否存在还未下载完成的文件...
File tempfile = new File("/sdcard/"+ThreadID+".txt");
if(tempfile.exists() && tempfile.length()>0){
FileInputStream fis = new FileInputStream(tempfile);
byte temp[] =new byte[1024];
int leng = fis.read(temp);
int downlength = Integer.parseInt(new String(temp,0,leng)); int alreadydown = downlength -startIndex; currentProgress += alreadydown;//发生断点之后记录下载的文件长度.. startIndex = downlength;
fis.close();
} url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex);
conn.setConnectTimeout(5000); //获取响应码..
int status =conn.getResponseCode(); if(status == 206){
InputStream in = conn.getInputStream();
RandomAccessFile raf =new RandomAccessFile("/sdcard/jdk.exe", "rwd"); //文件开始写入..
raf.seek(startIndex); int len =0;
byte[] buffer =new byte[1024]; //已经下载的数据长度..
int total = 0; while((len = in.read(buffer))!=-1){
//记录当前数据下载的长度...
RandomAccessFile file = new RandomAccessFile("/sdcard/"+ThreadID+".txt", "rwd"); raf.write(buffer, 0, len);
total += len;
System.out.println("线程"+ThreadID+"total:"+total);
file.write((total+startIndex+"").getBytes());
file.close(); synchronized (MainActivity.this) {
currentProgress += len; //获取当前总进度...
//progressBar progressDialog可以直接在子线程内部更新UI..由于源码内部进行了特殊的处理..
pb.setProgress(currentProgress); //更改界面上的进度条进度..
Message msg =Message.obtain(); //复用以前的消息..避免多次new...
msg.what = 3;
handler.sendMessage(msg); }
}
raf.close();
in.close();
System.out.println("线程:"+ThreadID+"下载完毕");
}else{
System.out.println("线程:"+ThreadID+"下载失败");
} } catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
Message msg = new Message();
msg.what = 1;
msg.obj = e;
handler.sendMessage(msg);
}finally{
synchronized (MainActivity.this) {
runningThread--; if(runningThread == 0){
for(int i=1;i<=threadCount;i++){
File file = new File("/sdcard/"+i+".txt");
file.delete();
}
Message msg =new Message();
msg.what = 2;
msg.obj ="下载完毕";
handler.sendMessage(msg);
}
} } } }
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
} }
源代码如上..优化的事情我就不做了..为了方便直接就贴上了..这里定义了一个ProgressBar进度条..一个TextView来同步进度条的下载进度..在Android中我们自然不能够在主线程中去调用耗时间的操作..因此这些耗时的操作我们就通过开启子线程的方式去使用..但是子线程是不能够更新UI界面的..因此我们需要使用到Handler Message机制来完成主界面UI更新的操作.
但是上面的代码当中我们会发现一个问题..在子线程内部居然更新了ProgressBar操作..其实ProgressBar和ProgressDialog是两个特例..我们可以在子线程内部去更新他们的属性..我们来看一下源代码的实现过程..
private synchronized void refreshProgress(int id, int progress, boolean fromUser) {
if (mUiThreadId == Thread.currentThread().getId()) { //如果当前运行的线程和主线程相同..那么更新进度条..
doRefreshProgress(id, progress, fromUser, true);
} else { //如果不满足上面说的情况..
if (mRefreshProgressRunnable == null) {
mRefreshProgressRunnable = new RefreshProgressRunnable();//那么新建立一个线程..然后执行下面的过程..
} final RefreshData rd = RefreshData.obtain(id, progress, fromUser); //获取消息队列中的消息..
mRefreshData.add(rd);
if (mAttached && !mRefreshIsPosted) {
post(mRefreshProgressRunnable); //主要是这个地方..调用了post方法..将当前运行的线程发送到消息队列当中..那么这个线程就可以在UI中运行了..因此这一步是决定因素..
mRefreshIsPosted = true;
}
}
}
正是由于源码内部调用了post方法..将当前的线程放入到消息队列当中..那么UI中的Looper线程就会对这个线程进行处理..那么就表示这个线程是可以被执行在UI当中的..也正是这个因素导致了我们可以在子线程内部更新ProgressBar..但是我们可以看到如果我们想要去更新TextView的时候..我们就需要调用Handler Message机制来完成UI界面的更新了..因此这一块需要我们去注意..
移植之后代码其实并没有发生太大的变化..这样就可以完成一个在Android中的多线程断点下载器了..
Android 学习笔记之使用多线程实现断点下载...的更多相关文章
- Android学习笔记_15_网络通信之文件断点下载
一.断点下载原理: 使用多线程下载文件可以更快完成文件的下载,多线程下载文件之所以快,是因为其抢占的服务器资源多.如:假设服务器同时最多服务100个用户,在服务器中一条线程对应一个用户,100条线程在 ...
- Android学习笔记_43_网络通信之文件断点上传
1.建立服务端,用于接收上传的文件.这里使用Socket,文件可能会比较大.采用多线程编程,防止并发. package com.socket.service; import java.io.File; ...
- 【转】 Pro Android学习笔记(七一):HTTP服务(5):多线程调用HttpClient
目录(?)[-] 应用共享HttpClient对象的同步问题 创建共享HttpClient代码 创建共享对象 创建可共享的HttpClient对象 使用共享HttpClient对象的代码 基础代码 修 ...
- 转:学习笔记:delphi多线程学识
学习笔记:delphi多线程知识 最近一直在温习旧的知识,刚好学习了一下Java的线程安全方面的知识,今天想起之前一直做的Delphi开发,所以还是有必要温习一下,看看这些不同的编程语言有什么不同之处 ...
- Android 学习笔记之Volley(七)实现Json数据加载和解析...
学习内容: 1.使用Volley实现异步加载Json数据... Volley的第二大请求就是通过发送请求异步实现Json数据信息的加载,加载Json数据有两种方式,一种是通过获取Json对象,然后 ...
- Android学习笔记进阶之在图片上涂鸦(能清屏)
Android学习笔记进阶之在图片上涂鸦(能清屏) 2013-11-19 10:52 117人阅读 评论(0) 收藏 举报 HandWritingActivity.java package xiaos ...
- android学习笔记36——使用原始XML文件
XML文件 android中使用XML文件,需要开发者手动创建res/xml文件夹. 实例如下: book.xml==> <?xml version="1.0" enc ...
- Android学习笔记之JSON数据解析
转载:Android学习笔记44:JSON数据解析 JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,采用完全独立于语言的文本格式,为Web应用开发提供了一种 ...
- udacity android 学习笔记: lesson 4 part b
udacity android 学习笔记: lesson 4 part b 作者:干货店打杂的 /titer1 /Archimedes 出处:https://code.csdn.net/titer1 ...
随机推荐
- C# 动态修改dll的签名 以及修改引用该dll文件的签名
在读取RedisSessionStateProvider配置 提到用mono ceil 来修改程序集以及它的签名,里面GetPublicKey 和GetPubliKeyToken 方法里面那个字符串的 ...
- [C] c99int(让VC等编译器自动兼容C99的整数类型)V1.02。源码托管到github、添加CMake编译配置文件、使用doxygen规范注释
新版本—— http://www.cnblogs.com/zyl910/p/zlstdint_v100.html[C] zlstdint(让VC.TC等编译器自动兼容C99的整数类型)V1.0.支持T ...
- Hello Kraken.js!
前言 kraken.js 由paypal 公司开源的一个用于快速开发基于Express.js框架应用的快速开发工具, 因为kraken 并没有在Express.js基础上更改多少东西,只是在原来的 ...
- IE11企业模式介绍及可用性评估
什么是企业模式? 企业模式是可以在 Windows 8.1 和 Windows7 设备上的 Internet Explorer 11 上运行的一种兼容性模式,该模式允许网站使用已修改的浏览器配置来呈现 ...
- 多线程socket编程示例
工程: 代码: package com.my.socket.business; /** * 业务实现类 * * @author ZY * */ public class CoreMisBusiness ...
- 命令行的全文搜索工具--ack
想必大家在命令行环境下工作时候,一定有想要查找当前目录下的源代码文件中的某些字符的需求,这时候如果使用传统方案,你可能需要输入一长串的命令,比如这样: 1. grep -R 'string' dir/ ...
- phpMyAdmin在Mac OS X上的配置和使用
本文主要记录phpMyAdmin在Mac OS X上的配置和使用,避免朋友们走弯路,浪费不必要的时间. 1. 下载: 2. 在"设置"中打开" web shar ...
- 使用 T-SQL 计算当日日期、本周第一天与最后一天
--当日日期 ); SET @Today = DATENAME(YEAR, GETDATE()) + '-' + DATENAME(MONTH, GETDATE()) + '-' + DATENAME ...
- PHP操作MongoDB学习(转)
1 mongodb启动时,设置启动项 C:\>mongodb\bin\mongod --config C:\mongodb.conf 其中mongodb.conf为: dbpath = ...
- HBase - Phoenix剖析
1.概述 在<Hadoop-Drill深度剖析>一文当中,给大家介绍了Drill的相关内容,就实时查询来说,Drill基本能够满足要求,同时还可以做一个简单业务上的聚合,如果在使用Hive ...