前言: 项目都快交付阶段了,客户说要改个需求,添加一个断点续传功能。在版本更新,杂志下载或者视频下载的时候实现断点续传。由于时间紧迫,想起了之前研究过一个demo代码,就直接修改使用了,根据自己的方式实现,但是核心代码没变。以后或许会用到,于是就专门写了个demo。
先看一下项目目录结构:
- db--->操作数据库的(创建数据库表,数据的增删改查。)
- util--->工具类
- download--->实现下载(下载器以及自定义线程。)
这里以易信客户端的下载为例,简要介绍。
String downloadPath = "http://gdown.baidu.com/data/wisegame/653346a13ab69081/yixin_146.apk"; String fileName = "易信.apk";
在这里我添加了通知栏,查看进度。
大致思路:根据需要下载文件的总大小,可以自定义几个线程同时下载动作(在这里我给下载器构造方法重载了,默认不设定线程数量的话默认是3个)。
内部使用map键值对的方式缓存线程下载数据,key是线程id,value:是下载量。同时还有个线程数组,这个线程数组和前面的Map是对应的。
在下载器中,有个循环会不断检查每个线程的下载任务是否完成,完成了则将其T掉,这样到最后就都完成后,退出循环,整个下载任务就完成。删除数据库表中的下载记录。
然后每个线程分配下载文件大小值,执行下载。下载过程中,通过回调接口,回调到MainActivity画面中,利用handler实现更新UI。
......
这里具体的问题细节,啊还有很多,大致思路就是这样了。
代码里面我都写了详细的注释,贴点代码把。希望你也能看懂。
package com.example.download; import java.io.File; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; import com.example.util.Common; import android.util.Log; /** * 用于下载文件的线程。 该线程有被分配的工作量:block * */ public class DownloadThread extends Thread { private static final String TAG = "DownloadThread"; /** * 目标文件 */ private File saveFile; /** * 文件下载路径 */ private URL downUrl; /** * 当前线程需要下载的文件大小 */ private int block; /** * 线程身份识别id */ private int threadId = -1; /** * 之前已经下载的位置 */ private int downLength; /** * 判断当前线程是否结束 */ private boolean isFinished = false; /** * 文件下载器 */ private FileDownloader downloader; /** * @param downloader * 下载器 * @param downUrl * 下载的网络路径 * @param saveFile * 保存的目标文件 * @param block * 下载任务量 * @param downLength * 已经下载的大小 * @param threadId * 线程id */ public DownloadThread(FileDownloader downloader, URL downUrl, File saveFile, int block, int downLength, int threadId) { this.downUrl = downUrl; this.saveFile = saveFile; this.block = block; this.downloader = downloader; this.threadId = threadId; this.downLength = downLength; } @Override public void run() { if (downLength < block) {// 未下载完成 try { // 使用Get方式下载 HttpURLConnection http = Common.getHttpParams(downUrl); // 开始下载位置 int startPosition = block * (threadId - 1) + downLength; // 结束下载位置 int endPosition = block * threadId - 1; // 设置获取实体数据的范围 http.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition); // 获取文件输入流 InputStream inStream = http.getInputStream(); byte[] buffer = new byte[1024]; int offset = 0; print("Thread " + this.threadId + " start download from position " + endPosition); RandomAccessFile threadfile = new RandomAccessFile( this.saveFile, "rwd"); threadfile.seek(endPosition); while ((offset = inStream.read(buffer, 0, 1024)) != -1) { threadfile.write(buffer, 0, offset); // 下载量累加 downLength += offset; // 更新下载数据库 downloader.update(this.threadId, downLength); downloader.append(offset); } threadfile.close(); inStream.close(); print("Thread " + this.threadId + " download finish"); // 设置下载完成标志位。 this.isFinished = true; } catch (Exception e) { this.downLength = -1; print("Thread " + this.threadId + ":" + e); } } } /** * 打印日志信息 * * @param msg */ private static void print(String msg) { Log.i(TAG, msg); } /** * 下载是否完成 * * @return */ public boolean isFinish() { return isFinished; } /** * 已经下载的内容大小 * * @return 如果返回值为-1,代表下载失败 */ public long getDownLength() { return downLength; } }
下载器:
package com.example.download; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import android.content.Context; import android.util.Log; import com.example.db.DBTools; import com.example.util.Common; /** * * 文件下载 * * * */ public class FileDownloader { private static final String TAG = "FileDownloader"; private Context context; /** * 操作数据库的类 */ private DBTools dbTools; /** * 已下载文件长度 */ private int downloadSize = 0; /** * 原始文件长度 */ private int fileSize = 0; /** * 线程数 */ private DownloadThread[] threadArr; /** * 本地保存文件 */ private File saveFile; /** * 默认下载线程数量 */ private static final int defaultThreadSize = 3; /** * 缺省文件下载存放位置 */ private static String defaultDestPath = ""; /** * 用于缓存各个线程下载的长度 * <p> * Key:对应线程id;Value:对应线程已经下载的长度 * </p> */ private Map<Integer, Integer> cacheDownMap = new ConcurrentHashMap<Integer, Integer>(); /** * 每条线程应该下载的长度 */ private int block; /** * 文件下载路径 */ private String downloadUrl; /** * 获取线程数 */ public int getThreadSize() { return threadArr.length; } /** * 获取文件大小 * * @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.cacheDownMap.put(threadId, pos); this.dbTools.update(this.downloadUrl, this.cacheDownMap); } /** * 构建文件下载器 * * @param downloadUrl * 下载路径 * @param fileSaveDir * 文件保存目录 * @param threadNum * 下载线程数 */ public FileDownloader(Context context, String downloadUrl, String destPath, int threadNum) { this.context = context; this.downloadUrl = downloadUrl; dbTools = new DBTools(this.context); checkPath(destPath); this.threadArr = new DownloadThread[threadNum]; setRequest(downloadUrl, destPath); } /** * 检查下载路径是否存在,不存在则创建。。 * @param destPath * @return 是否存在该目录 */ public boolean checkPath(String destPath) { File file = new File(destPath); if (!file.exists()) { boolean bol = file.mkdirs(); return bol; } return true; } /** * @param context * @param downloadUrl */ public FileDownloader(Context context, String downloadUrl) { this.context = context; this.downloadUrl = downloadUrl; this.threadArr = new DownloadThread[defaultThreadSize]; this.dbTools = new DBTools(this.context); dbTools = new DBTools(this.context); checkPath(defaultDestPath); this.threadArr = new DownloadThread[defaultThreadSize]; setRequest(downloadUrl, defaultDestPath); } /** * 封装请求 */ public void setRequest(String downUrl, String destPath) { URL url = null; try { url = new URL(downUrl); HttpURLConnection conn = Common.getHttpParams(url); conn.connect(); // 打印头部信息 printResponseHeader(conn); // 200==》成功连接... if (conn.getResponseCode() == 200) { // 获取文件总大小 this.fileSize = conn.getContentLength(); if (this.fileSize <= 0) throw new RuntimeException("该文件很可能不存在..."); // 获取当前需要下载文件名称 String filename = getFileName(conn); // 目标保存文件 this.saveFile = new File(destPath, filename); // 获取当前路径下载记录包括已经下载数量。 Map<Integer, Integer> logdata = dbTools.getData(downUrl); // 如果存在下载记录把各条线程已经下载的数据长度放入缓存变量中 if (logdata.size() > 0) { for (Map.Entry<Integer, Integer> entry : logdata.entrySet()) cacheDownMap.put(entry.getKey(), entry.getValue()); } // 下面计算所有线程已经下载的数据长度 if (this.cacheDownMap.size() == this.threadArr.length) { for (int i = 0; i < this.threadArr.length; i++) { this.downloadSize += this.cacheDownMap.get(i + 1); } print("已经下载数据的长度" + this.downloadSize); } // 计算分配给每条线程下载的数据长度 this.block = (this.fileSize % this.threadArr.length) == 0 ? this.fileSize / this.threadArr.length : this.fileSize / this.threadArr.length + 1; } else { throw new RuntimeException("无响应......"); } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } /** * 获取文件名 * * @param conn * 本次http连接 * @return */ private String getFileName(HttpURLConnection conn) { // 获取文件名称 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; if ("content-disposition".equals(conn.getHeaderFieldKey(i) .toLowerCase())) { Matcher match = Pattern.compile(".*filename=(.*)").matcher( mine.toLowerCase()); if (match.find()) return match.group(1); } } // 缺省 filename = UUID.randomUUID() + ".tmp";// 默认取一个文件名 } return filename; } /** * 开始下载文件 * * @param listener * 监听下载数量的变化,如果不需要了解实时下载的数量,可以设置为null * @return 已下载文件大小 * @throws Exception */ public int startDownload(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.cacheDownMap.size() != this.threadArr.length) { this.cacheDownMap.clear(); for (int i = 0; i < this.threadArr.length; i++) { this.cacheDownMap.put(i + 1, 0);// 初始化每条线程已经下载的数据长度为0 } } // 开启线程进行下载 for (int i = 0; i < this.threadArr.length; i++) { int downLength = this.cacheDownMap.get(i + 1); // 这里的第一个条件是判断当前该线程还没有下载完分配数量;第二个是判断当前总的下载量要小于需要下载的文件总大小。 if (downLength < this.block && this.downloadSize < this.fileSize) { this.threadArr[i] = new DownloadThread(this, url, this.saveFile, this.block, this.cacheDownMap.get(i + 1), i + 1); // 提高优先级 this.threadArr[i].setPriority(7); this.threadArr[i].start(); } else { // 该线程已经完任务,T掉。 this.threadArr[i] = null; } } // 将下载数据插入数据库 this.dbTools.save(this.downloadUrl, this.cacheDownMap); boolean notFinish = true;// 下载未完成 // 在文件被完全下载完之前是个死循环,判断所有线程是否完成下载 //如果for循环结束了,外层while循环也就结束。 while (notFinish) { Thread.sleep(900); notFinish = false;// 假定全部线程下载完成 for (int i = 0; i < this.threadArr.length; i++) { if (this.threadArr[i] != null && !this.threadArr[i].isFinish()) {// 如果发现线程未完成下载 notFinish = true;// 设置标志为下载没有完成 // 获取当前线程完成任务量,如果为-1则重新下载。 if (this.threadArr[i].getDownLength() == -1) { this.threadArr[i] = new DownloadThread(this, url, this.saveFile, this.block, this.cacheDownMap.get(i + 1), i + 1); this.threadArr[i].setPriority(7); this.threadArr[i].start(); } } } //回调,更新UI if (listener != null) listener.onDownloadSize(this.downloadSize);// 通知目前已经下载完成的数据长度 } //文件下载完成删除下载记录。 dbTools.delete(this.downloadUrl); } catch (Exception e) { print(e.toString()); throw new Exception("file download fail"); } 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; } /** * 打印Http头字段 * * @param http */ public static void printResponseHeader(HttpURLConnection http) { Map<String, String> header = getHttpResponseHeader(http); for (Map.Entry<String, String> entry : header.entrySet()) { String key = entry.getKey() != null ? entry.getKey() + ":" : ""; print(key + entry.getValue()); } } /** * 打印日志信息 * * @param msg * 打印的日志内容 */ private static void print(String msg) { Log.i(TAG, msg); } /** * 下载文件监听 * * 主要用于回调。 */ public interface DownloadProgressListener { public void onDownloadSize(int size); } }
package com.example.util; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; /** * <p> * </p> * * @author xiangxm */ public class Common { /** * 格式化文件大 * * @param volume * 文件大小 * @return 格式化的字符 */ public static String getVolume(long volume) { float num = 1.0F; String str = null; if (volume < 1024) { str = volume + "B"; } else if (volume < 1048576) { num = num * volume / 1024; str = String.format("%.1f", num) + "K"; } else if (volume < 1073741824) { num = num * volume / 1048576; str = String.format("%.1f", num) + "M"; } else if (volume < 1099511627776L) { num = num * volume / 1073741824; str = String.format("%.1f", num) + "G"; } return str; } /** * * 设置网络参数 */ public static HttpURLConnection getHttpParams(URL httpUrl) { HttpURLConnection http; try { http = (HttpURLConnection) httpUrl.openConnection(); http.setConnectTimeout(5 * 1000); http.setRequestMethod("GET"); http.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, */*"); http.setRequestProperty("Accept-Language", "zh-CN"); http.setRequestProperty("Referer", httpUrl.toString()); http.setRequestProperty("Charset", "UTF-8"); http.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)"); http.setRequestProperty("Connection", "Keep-Alive"); return http; } catch (IOException e) { e.printStackTrace(); } return null; } }
源代码下载地址:http://download.csdn.net/detail/xxm282828/7701071
看一下效果图:
下一篇打算: 使用downloadManager实现下载,断点续传。