本项目完成的功能类似与迅雷等下载工具所实现的功能——实现多线程断点下载。
主要设计的技术有:
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类
具体下载线程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-多线程断点下载详解及源码下载(四)
源码下载(服务器端和客户端代码)