由于一篇blog写不完,这里是接着上一篇blog的。
写完了MVC中的View,写着我们需要考虑Control层了,他的任务是在后台利用多线程实现断点下载。
先看源码:
public class FileDownloader { /* TAG,便于调试 */ private static final String TAG = "FileDownloader"; /* 上下文 */ private Context context; /* 用于对数据库的操作 */ private FileService fileService; /* 停止下载 */ private boolean exit; /* 已下载文件长度 */ private int downloadSize = 0; /* 原始文件长度 */ private int fileSize = 0; /* 线程数 */ private DownloadThread[] threads; /* 本地保存文件 */ private File saveFile; /* 缓存各线程下载的长度 */ private Map<Integer, Integer> data = new ConcurrentHashMap<Integer, Integer>(); /* 每条线程下载的长度 */ private int block; /* 下载路径 */ private String downloadUrl; /** * 获取线程数 */ public int getThreadSize() { return threads.length; } /** * 退出下载 */ public void exit() { this.exit = true; } public boolean getExit() { return this.exit; } /** * 获取文件大小 * @return 文件大小 */ public int getFileSize() { return fileSize; } /** * 累计已下载大小 * @param size */ protected synchronized void append(int size) { downloadSize += size; } /** * 更新指定线程最后下载的位置 * @param threadId 线程id * @param pos 最后下载的位置 */ protected synchronized void update(int threadId, int pos) { this.data.put(threadId, pos); this.fileService.update(this.downloadUrl,threadId, pos); } /** * 构建文件下载器 * @param downloadUrl 下载路径 * @param fileSaveDir 文件保存目录 * @param threadNum 下载线程数 */ public FileDownloader (Context context ,String downloadUrl ,File fileSaveDir , int threadNum) { try { this.context = context; this.downloadUrl = downloadUrl; fileService = new FileService(this.context); URL url = new URL(this.downloadUrl); if (!fileSaveDir.exists()) fileSaveDir.mkdirs();// 不存在目录则创建 this.threads = new DownloadThread[threadNum]; HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(5 * 1000); conn.setRequestMethod("GET"); conn.setDoInput(true); conn.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg, 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, */*"); conn.setRequestProperty("Accept-Language", "zh-CN"); conn.setRequestProperty("Referer",downloadUrl); conn.setRequestProperty("Charset","UTF-8"); conn.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)"); conn.setRequestProperty("Connection","Keep-Alive"); /* * connect()方法的作用 首先创建对象,然后建立连接。 在创建对象之后,建立连接之前,可指定各种选项(例如,doInput 和 * UseCaches)。 连接后再进行设置就会发生错误。 连接后才能进行的操作(例如 * getContentLength),如有必要,将隐式执行连接。 */ conn.connect(); if (conn.getResponseCode() == 200) { this.fileSize = conn.getContentLength();// 根据响应获取文件总大小 if (this.fileSize <= 0) throw new RuntimeException("Unkown file size "); String filename = getFileName(conn);// 获取文件名称 this.saveFile = new File(fileSaveDir, filename);// 构建保存文件 Map<Integer, Integer> logdata = fileService.getData(downloadUrl);// 获取下载记录 if (logdata.size() > 0) { // 如果存在下载记录,则把各条线程已经下载的数据长度放入data中 //这一步很重要,比如当用户由于某一种原因关闭退出了应用, //那么他再次进入应用的时候,应该是从上次未完成的地方开始下载而不是从新开始下载 for (Map.Entry<Integer, Integer> entry : logdata.entrySet()) data.put(entry.getKey(),entry.getValue()); } if (this.data.size() == this.threads.length) {// 下面计算所有线程已经下载的数据总长度 for (int i = 0; i < this.threads.length; i++) { this.downloadSize += this.data.get(i + 1); } } // 计算每条线程下载的数据长度 this.block = (this.fileSize % this.threads.length) == 0 ? this.fileSize / this.threads.length : this.fileSize / this.threads.length + 1; } else { throw new RuntimeException( "server no response "); } } catch (Exception e) { throw new RuntimeException( "don't connection this url"); } } /** * 获取文件名 */ private String getFileName(HttpURLConnection conn) { /* * 以URL地址中的后缀作为文件名, 例如URL为:http://192.162.1.1:8080/web/lenver.exe, * 那么他的名字就是lenver.exe */ String filename = this.downloadUrl.substring(this.downloadUrl .lastIndexOf('/') + 1); if (filename == null|| "".equals(filename.trim())) {// 如果获取不到文件名称 for (int i = 0;; i++) { String mine = conn .getHeaderField(i); if (mine == null) break; /* * 当所请求的路径所得的name不合法的shih * Content-disposition其实可以控制用户请求所得的内容存为一个文件的时候提供一个默认的文件名, * 文件直接在浏览器上显示或者在访问时弹出文件下载对话框。 */ if ("content-disposition" .equals(conn .getHeaderFieldKey( i) .toLowerCase())) { Matcher m = Pattern .compile( ".*filename=(.*)") .matcher( mine.toLowerCase()); if (m.find()) return m.group(1); } } filename = UUID.randomUUID() + ".tmp";// 默认取一个文件名 } return filename; } /** * 开始下载文件 * * @param listener * 监听下载数量的变化,如果不需要了解实时下载的数量,可以设置为null * @return 已下载文件大小 * @throws Exception */ public int download(DownloadProgressListener listener) throws Exception { try { RandomAccessFile randOut = new RandomAccessFile( this.saveFile, "rw"); if (this.fileSize > 0) randOut.setLength(this.fileSize); randOut.close(); URL url = new URL(this.downloadUrl); if (this.data.size() != this.threads.length) {// 如果原先未曾下载或者原先的下载线程数与现在的线程数不一致,这里都是三 this.data.clear(); for (int i = 0; i < this.threads.length; i++) { this.data.put(i + 1, 0);// 初始化每条线程已经下载的数据长度为0 } this.downloadSize = 0; } for (int i = 0; i < this.threads.length; i++) {// 开启线程进行下载 int downLength = this.data.get(i + 1);// 从数据库中取出某一条线程下载的长度 if (downLength < this.block && this.downloadSize < this.fileSize) {// 判断线程是否已经完成下载,否则继续下载 this.threads[i] = new DownloadThread( this, url, this.saveFile, this.block, this.data.get(i + 1), i + 1); this.threads[i] .setPriority(7);// 设置优先级 this.threads[i].start();// 启动线程 } else { this.threads[i] = null; } } fileService.delete(this.downloadUrl);// 如果存在下载记录,删除它们,然后重新添加 fileService.save(this.downloadUrl, this.data); boolean notFinish = true;// 下载未完成 while (notFinish)//这个循环很关键,他是可以维持他的调用者也就是DownloadTask这个线程一直运行下去,然后还就可以不断的发消息给UI线程 {// 循环判断所有线程是否完成下载 Thread.sleep(900); notFinish = false;// 假定全部线程下载完成 for (int i = 0; i < this.threads.length; i++) { if (this.threads[i] != null && !this.threads[i] .isFinish()) {// 如果发现线程未完成下载 notFinish = true;// 设置标志为下载没有完成 if (this.threads[i] .getDownLength() == -1) {// 如果下载失败,再重新下载 this.threads[i] = new DownloadThread( this, url, this.saveFile, this.block, this.data .get(i + 1), i + 1); this.threads[i] .setPriority(7); this.threads[i] .start(); } } } if (listener != null) listener.onDownloadSize(this.downloadSize);// 通知目前已经下载完成的数据长度 } if (downloadSize == this.fileSize) fileService .delete(this.downloadUrl);// 下载完成删除记录 } catch (Exception e) { throw new Exception( "file download error"); } return this.downloadSize; } /** * 获取Http响应头字段 * * @param http * @return */ public static Map<String, String> getHttpResponseHeader( HttpURLConnection http) { Map<String, String> header = new LinkedHashMap<String, String>(); for (int i = 0;; i++) { String mine = http.getHeaderField(i); if (mine == null) break; header.put(http.getHeaderFieldKey(i), mine); } return header; } }
其中FileService类是操作数据库的一个类,下下一篇blog讲到,就是对数据库各种增删操作。
关键代码分析:
其中private Map<Integer, Integer> data = new ConcurrentHashMap<Integer, Integer>();这个参数,他的作用是每一次都从数据库中读取每一条线程的停止所下载时候的值,当下载完成之后就会删除该记录。
我们是把下载文件的位置存放到了SD卡的根目录地下,大家可以看到我们是把SD卡的位置作为参数传进FIleDownloader中,然后通过获取URL中最后‘\‘的字符串作为文件名称。文件名称不存在的时候则使用服务器提供给我们的默认名称,大家可以看getName()那个方法。
然后,在MainActivity中调用了FileDownlaoder中的download方法,可以看到他是开了三条线程去完成下载功能的。然后大家可以看到每一次下载之前都会从数据库中读取相对应线程的下载已经下载的长度,然后去跟他本应该下载的长度去对比,如果该线程完成任务,即从数据库中读取到的长度等于他应该下载的长度,则不用去下载,否则继续。然后会有一个循环,默认是死循环,他的作用是没经过900毫秒去检测下载是否完成和下载是否失败,以及发送最新的下载进度给UI线程。大家可以把那个sleep时间设置为任意值,但是要合适,因为用户一般需要经过某一段时间就要查看下载进度,我们尽可能及时的更新UI给用户一个更好的体验,但是又不能太【频繁,因为这样执行的次数太多有损性能。大家可以看自己下载的文件的大小去衡量,大一点的可以久一点时间在去更新Ui,但是小的就快一点更新UI。
然后,FIleDownloader实际上是一个控制真正去下载文件的线程的一个类。而真正下载的类是DownloadThread。下一篇blog就跟大家分享他的使用。
原文地址:http://blog.csdn.net/liweijie_chengxuyuan/article/details/45031215