本项目完成的功能类似与迅雷等下载工具所实现的功能——实现多线程断点下载。
主要设计的技术有:
1、android中主线程与非主线程通信机制。
2、多线程的编程和管理。
3、android网络编程
4、自己设计实现设计模式-监听器模式
5、Activity、Service、数据库编程
6、android文件系统
7、缓存
博文链接:
Android-多线程断点下载详解及源码下载(一)
Android-多线程断点下载详解及源码下载(二)
Android-多线程断点下载详解及源码下载(四)
本篇接着上篇开始详细讲述客户端代码的具体实现,详细讲述下载器的实现以及多线程的管理工作。
下载器是指本项目中的MultiThreadManager类,该类可以看作是线程池的作用,启动多个线程,同时管理和维护多个线程。既然可以管理多个线程,必然设计多线程的通信、同步、异步的问题。
首先分析下载器的功能:
1、启动多个线程,本项目中的具体实现文件下载的类是DownTaskThread,也就是说MultiThreadManager类要new出来多个下载类,启动线程。
2、线程启动之后,需要不断获取已经下载的长度,并更新已经下载的长度值,则MultiThreadManager类中有这样几个方法,如下代码:
//获取已经下载的长度
public int getDownedLen() {
return downedLen;
}
//追加已经下载的长度
public synchronized void appendSize(int len){
this.downedLen += len;
System.out.println("已经下载的长度="+this.downedLen);
}
3、多个线程同时进行下载,那么每个线程下载的长度也需要维护,因为要实现断点下载,需要保存每个线程已经下载的长度,则有如下方法:
/**
* 设置成synchronized同步!
* 这是因为该项目中有多个线程进行该操作。
* 设计线程同步问题,同时更改一个数据会造成混乱。
* 所以此处必须设置成同步操作。
* @param downedLen
*/
public synchronized void setDownedLen(int threadId,long downedLen) {
this.map.put(threadId, downedLen);
System.out.println("线程"+threadId+"已下载长度="+downedLen+",map数量="+map.size());
this.downDatabaseService.update(this.downPath, this.map);
}
方法设置为synchronized是很好理解的,因为涉及多线程,同时更新一个数据,必然需要同步,不然乱套了!
4、既然是下载管理器,那么是有可能退出下载或者暂停下载的功能的,那么下载管理器可以有一个标记位,标记是下载还是暂停,则有如下方法:
//设置是否退出或者暂停
public boolean isExist() {
return isExist;
}
//获取是否退出或者暂停
public void setExist(boolean isExist) {
this.isExist = isExist;
}
可能大家看到这个方法仅仅是个标记位,如何起到暂停下载的作用呢?其实是这样实现的,每个线程的run方法里面,循环读取输入流的方法中,每读取一次缓存区会判断该标记位是否已经设置为退出或者暂停,这样就可以实现暂停的功能了。
几个主要的功能是这四个方面,下载器的全部代码如下:
public class MultiThreadManager {
private int threadNum;//启动的线程数量
private String downPath;//下载路径
private int downedLen;//已下载的长度
private boolean isExist;//是否已经退出下载或者暂停
//通过该类完成数据库中信息的更新
private DownDatabaseService downDatabaseService;
private DownTaskThread[] downTaskThreads;//线程数组,即线程池
private long fileLen;//文件长度
private File saveDir;//保存路径
private String fileName;//文件名
@SuppressLint("UseSparseArrays")
private Map<Integer, Long> map =
new HashMap<Integer, Long>();//缓存已经下载的各个线程的长度
private long block;//每个线程下载块的大小
public MultiThreadManager(int threadNum,String downPath,
File saveDir,Context context){
this.threadNum = threadNum;
this.downPath = downPath;
downDatabaseService = new DownDatabaseService(context);
downTaskThreads = new DownTaskThread[threadNum];
fileLen = getDownLoaderFileLen(downPath);
this.saveDir = new File(saveDir,this.fileName);
this.block = (fileLen%threadNum==0)?(fileLen/threadNum):(fileLen/threadNum+1);
System.out.println("文件块的大小block="+block);
}
//获取已经下载的长度
public int getDownedLen() {
return downedLen;
}
/**
* 设置成synchronized同步!
* 这是因为该项目中有多个线程进行该操作。
* 设计线程同步问题,同时更改一个数据会造成混乱。
* 所以此处必须设置成同步操作。
* @param downedLen
*/
public synchronized void setDownedLen(int threadId,long downedLen) {
this.map.put(threadId, downedLen);
System.out.println("线程"+threadId+"已下载长度="+downedLen+",map数量="+map.size());
this.downDatabaseService.update(this.downPath, this.map);
}
//追加已经下载的长度
public synchronized void appendSize(int len){
this.downedLen += len;
System.out.println("已经下载的长度="+this.downedLen);
}
//设置是否退出或者暂停
public boolean isExist() {
return isExist;
}
//获取是否退出或者暂停
public void setExist(boolean isExist) {
this.isExist = isExist;
}
//获取线程数量
public int getThreadNum() {
return threadNum;
}
public long getFileLen() {
return fileLen;
}
/**
* 获取下载的文件的长度
* @param url
* @return
*/
private int getDownLoaderFileLen(String url){
int len = 0;
try {
URL path = new URL(url);
HttpURLConnection httpURLConnection = (HttpURLConnection) path.openConnection();
httpURLConnection.setDoOutput(true);
httpURLConnection.setDoInput(true);
httpURLConnection.setConnectTimeout(5*1000);
httpURLConnection.setUseCaches(true);
httpURLConnection.setRequestMethod("GET");
//设置客户端可接受的媒体类型
httpURLConnection.setRequestProperty("Accept", "image/gif,image/jpeg," +
"image/pjpeg,application/x-shockwave-flash,application/xaml+xml," +
"application/vnd.ms-xpsdocument,application/x-ms-xbap," +
"application/x-ms-application,application/vnd.ms-excel," +
"application/vnd.ms-powerpoint,application/msword,*/*");
//设置客户端语言
httpURLConnection.setRequestProperty("Accept-Language", "zh-CN");
//设置请求来源,便于服务器进行来源统计
httpURLConnection.setRequestProperty("Referer", url);
//设置客户端编码
httpURLConnection.setRequestProperty("Charset", "UTF-8");
//设置用户代理
httpURLConnection.setRequestProperty("User-Agent", "Mozilla/4.0(" +
"compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; " +
".NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; " +
".NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
//设置连接方式
httpURLConnection.setRequestProperty("Connetion", "Keep-Alive");
httpURLConnection.connect();
printResponseHeader(httpURLConnection);
if (httpURLConnection.getResponseCode() == 200) {
len = httpURLConnection.getContentLength();
this.fileName = getFileName(httpURLConnection);
if (len<=0) {
System.out.println("文件大小不知");
}
this.map = this.downDatabaseService
.getDownLoadedLen(this.downPath);
if (map.size()>0) {//说明已经有下载数据
System.out.println("已经有下载数据,map的数量为"+map.size());
}else {
System.out.println("无下载数据,map的数量为"+map.size());
}
if (map.size() == this.threadNum) {//如果已经下载的线程数据的数量和
//现有设置的线程数量相同则计算所有线程亿i纪念馆下载的总长度
for (int i = 0; i < this.threadNum; i++) {
//遍历每条线程,计算总下载长度
this.downedLen += this.map.get(i+1);
//通过线程threadId获取每条线程已经下载的长度
//这里的i+1是因为线程threadId从1开始
}
System.out.println("总已下载长度="+downedLen);
}
}else {
System.out.println("服务器响应错误。"+httpURLConnection.getResponseCode()
+httpURLConnection.getResponseMessage());
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("从服务器获取文件的长度="+len);
return len;
}
/**
* 该方法执行线程启动操作
* @param iDownProgressing
* @throws Exception
*/
public int downloader(IDownProgressing iDownProgressing) throws Exception{
RandomAccessFile randomAccessFile = new RandomAccessFile(this.saveDir, "rwd");
if (this.fileLen>0) {
randomAccessFile.setLength(this.fileLen);
}
//close之后就会即可把上面的设置信息提交。并且也必须调用close方法
randomAccessFile.close();
/**
* 如果已经保存的线程数和本次开启的线程数不一致
* 则使用新设置的线程数量重新进行下载
*/
if (this.threadNum != this.map.size()) {
//如果已经保存的线程数和本次开启的线程数不一致
System.out.println("map被清理");
this.map.clear();
for (int i = 0; i < this.threadNum; i++) {
map.put(i+1, 0l);//新开启的每一条线程设置为0
}
this.downedLen = 0;
}
for (int i = 0; i < this.threadNum; i++) {
if (this.map.get(i+1) < this.block && this.downedLen < this.fileLen) {
downTaskThreads[i] = new DownTaskThread
(this, this.downPath, this.block, this.saveDir,
this.map.get(i+1), i+1);
this.downTaskThreads[i].setPriority(Thread.MAX_PRIORITY);
downTaskThreads[i].start();
}else {
downTaskThreads[i] = null;
}
}
this.downDatabaseService.delete(this.downPath);
this.downDatabaseService.setData(this.downPath, this.map);
System.out.println("设置值之后map数量="+this.downDatabaseService.getDownLoadedLen(this.downPath).size());
boolean isFinished = false;
while (!isFinished) {
Thread.sleep(900);
isFinished = true;
for (int i = 0; i <this.threadNum; i++) {
if (this.downTaskThreads[i] != null &&
!this.downTaskThreads[i].isFinished()) {
isFinished = false;
//==-1说明下载失败
if (this.downTaskThreads[i].getDownedLen() == -1) {
this.downTaskThreads[i] =
new DownTaskThread(this, this.downPath,
this.block, saveDir, this.map.get(i+1), i+1);
this.downTaskThreads[i].setPriority(Thread.MAX_PRIORITY);
this.downTaskThreads[i].start();
}
}
}
//更新进度值,iDownProgressing 可以说明不显示进度值
if(iDownProgressing != null){
iDownProgressing.setDownLoaderNum(downedLen);
}
}
if (downedLen >= fileLen) {
//如果已下载完毕,则删除下载记录
downDatabaseService.delete(this.downPath);
}
return this.downedLen;
}
/**
* 打印网络请求响应头信息
* @param connection
*/
private void printResponseHeader(HttpURLConnection connection){
Map<String, List<String>> map = connection.getHeaderFields();
Set<Entry<String,List<String>>> set = map.entrySet();
System.out.println("获取的头字段:");
for (Entry<String, List<String>> entry:set) {
System.out.println(entry.getKey()+"=="+entry.getValue());
}
}
/**
* 获取文件名字
* @param connection
* @return String
*/
private String getFileName(HttpURLConnection connection){
String fileName = this.downPath.substring(this.downPath.lastIndexOf("/")+1);
if (fileName == null || fileName.trim().equals("")) {
fileName = UUID.randomUUID() + ".tmp";
//有网卡上的标识数字(每个网卡都有唯一的标识号)
//及CPU时钟的唯一数字生成的一个16字节的二进制数
//作为文件名
}
System.out.println("从服务器获取的文件名字="+fileName);
return fileName;
}
}
上面的代码中详细给出了注释,所以应该不难理解。
具体下载线程DownTaskThread类作用就是获取服务器的输入流,读取文件,并写入对应的文件当中。同时通过引用下载器MultiThreadManager实现更新已经下载的文件长度、更新进度值等操作。具体代码如下:
public class DownTaskThread extends Thread {
private String url;//下载路径-服务器路径
private long startPos;//下载开始位置
private File saveDir;//保存路径
private long downedLen;//已下载长度
private long block;//下载的长度块
private int threadId;//线程ID值
private MultiThreadManager multiThreadManager;//多线程管理类
private boolean isFinished = false;
public DownTaskThread(MultiThreadManager multiThreadManager,
String url,long block,File saveDir,long downedLen,int threadId){
this.url = url;
this.saveDir = saveDir;
this.downedLen = downedLen;
this.threadId = threadId;
this.block = block;
this.multiThreadManager = multiThreadManager;
this.startPos = block*(threadId-1) + downedLen;
System.out.println("线程"+threadId+"起始位置="+startPos);
}
public boolean isFinished() {
return isFinished;
}
public long getDownedLen() {
return downedLen;
}
@Override
public void run() {
super.run();
try {
URL url = new URL(this.url);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setDoInput(true);
long endPos = block*threadId-1;
connection.setConnectTimeout(5*1000);
connection.setRequestMethod("GET");
//设置客户端可接受的媒体类型
connection.setRequestProperty("Accept", "image/gif,image/jpeg," +
"image/pjpeg,application/x-shockwave-flash,application/xaml+xml," +
"application/vnd.ms-xpsdocument,application/x-ms-xbap," +
"application/x-ms-application,application/vnd.ms-excel," +
"application/vnd.ms-powerpoint,application/msword,*/*");
//设置客户端语言
connection.setRequestProperty("Accept-Language", "zh-CN");
//设置请求来源,便于服务器进行来源统计
connection.setRequestProperty("Referer", this.url);
//设置客户端编码
connection.setRequestProperty("Charset", "UTF-8");
//设置用户代理
connection.setRequestProperty("User-Agent", "Mozilla/4.0(" +
"compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; " +
".NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; " +
".NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
//设置获取实体数据的范围,如果超过了实体数据的大小会自动返回实际的数据的大小
connection.setRequestProperty("Range", "bytes="+this.startPos+"-"+endPos);
//设置连接方式
connection.setRequestProperty("Connection","Keep-Alive");
connection.connect();
InputStream inputStream = connection.getInputStream();
byte[] buffer = new byte[1024];
int len = 0;
RandomAccessFile randomAccessFile = new RandomAccessFile(this.saveDir, "rwd");
randomAccessFile.seek(this.startPos);
while (!this.multiThreadManager.isExist() &&
(len = inputStream.read(buffer, 0, buffer.length))>0) {
randomAccessFile.write(buffer,0,len);
this.downedLen += len;
this.multiThreadManager.setDownedLen(this.threadId, this.downedLen);
this.multiThreadManager.appendSize(len);
}
randomAccessFile.close();
inputStream.close();
if (this.multiThreadManager.isExist()) {
System.out.println("线程"+this.threadId+"已经被暂停");
}else {
System.out.println("线程"+this.threadId+"已经下载完成");
}
this.isFinished = true;
} catch (Exception e) {
e.printStackTrace();
this.downedLen = -1;
System.out.println("线程"+this.threadId+"出现异常");
}
}
}
下载线程类DownTaskThread有一点非常关键,就是这一行代码:
//设置获取实体数据的范围,如果超过了实体数据的大小会自动返回实际的数据的大小
connection.setRequestProperty("Range", "bytes="+this.startPos+"-"+endPos);
这一行代码是进行断点下载的标准代码,获取实体的范围进行下载。
代价有可能可以使用别的方法实现,例如利用下载的代码实现:
。。。。。。。。。。。上面一样。。。。。。。。。。。。
//设置用户代理
connection.setRequestProperty("User-Agent", "Mozilla/4.0(" +
"compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; " +
".NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; " +
".NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
//设置连接方式
connection.setRequestProperty("Connection","Keep-Alive");
connection.connect();
InputStream inputStream = connection.getInputStream();
byte[] buffer = new byte[1024];
int len = 0;
RandomAccessFile randomAccessFile = new RandomAccessFile(this.saveDir, "rwd");
inputStream.skip(this.startPos);//这行代码跳过指定的字节数开始读取数据
randomAccessFile.seek(this.startPos);
while (!this.multiThreadManager.isExist() &&
(len = inputStream.read(buffer, 0, buffer.length))>0) {
randomAccessFile.write(buffer,0,len);
。。。。。。。。。下面一样。。。。。。。。。。。。。。。
这样的方法和上面的代码中的区别仅仅是这一行代码:
inputStream.skip(this.startPos);//这行代码跳过指定的字节数开始读取数据
目的是想利用inputStream跳过指定的字节数后在进行读取,但是想法是对的,没有错!但问题是inputStream的这个方法有问题,达不到想要的效果。
这个问题请参考博文:
Java.IO.InputStream.skip() 错误(跳过字节数和预想的不等)
该博文中详细讲述了这个方法的问题,以及解决办法。
篇幅有些长了,本篇就到此,如果有什么疑问,欢迎大家留言评论。下一篇完结,并进行总结。
博文链接:
Android-多线程断点下载详解及源码下载(一)
Android-多线程断点下载详解及源码下载(二)
Android-多线程断点下载详解及源码下载(四)
原文地址:http://blog.csdn.net/u010156024/article/details/45391165