一、首先写这篇文章之前,要了解实现该Android多线程断点下载器的几个知识点

1.多线程下载的原理,如下图所示

注意:由于Android移动设备和PC机的处理器还是不能相比,所以开辟的子线程建议不要多于5条。当然现在某些高端机子的处理器能力比较强了,就可以多开辟几条子线程。

2、为了实现断点下载,采用数据库方式记录下载的进度,这样当你将该应用退出后,下次点击下载的时候,程序会去查看该下载链接是否存在下载记录,如果存在下载记录就会判断下载的进度,如何从上次下载的进度继续开始下载。

3、特别注意在主线程里不能执行一件比较耗时的工作,否则会因主线程阻塞而无法处理用户的输入事件,导致“应用无响应”错误的出现。耗时的工作应该在子线程里执行。

4、UI控件画面的重绘(更新)是由主线程负责处理的,不能在子线程中更新UI控件的值。可以采用Handler机制,在主线程创建Handler对象,在子线程发送消息给主线程所绑定的消息队列,从消息中获取UI控件的值,然后在主线程中进行UI控件的重绘(更新)工作。
5、了解HTTP协议各个头字段的含义

二、将该下载器的具体实现代码展现出来

step1、首先查看整个Android项目的结构图

               
                

step2:设计应用的UI界面   /layout/activity_main.xml

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="vertical" android:layout_width="fill_parent"
  3. android:layout_height="fill_parent">
  4.  
  5. <TextView
  6. android:layout_width="fill_parent"
  7. android:layout_height="wrap_content"
  8. android:text="@string/path" />
  9.  
  10. <EditText
  11. android:id="@+id/path"
  12. android:layout_width="fill_parent"
  13. android:layout_height="wrap_content"
  14. android:text="http://192.168.1.100:8080/Hello/a.mp4" />
  15.  
  16. <LinearLayout
  17. android:layout_width="fill_parent"
  18. android:layout_height="wrap_content"
  19. android:orientation="horizontal" >
  20.  
  21. <Button
  22. android:id="@+id/downloadbutton"
  23. android:layout_width="wrap_content"
  24. android:layout_height="wrap_content"
  25. android:text="@string/startbutton" />
  26.  
  27. <Button
  28. android:id="@+id/stopbutton"
  29. android:layout_width="wrap_content"
  30. android:layout_height="wrap_content"
  31. android:enabled="false"
  32. android:text="@string/stopbutton" />
  33. </LinearLayout>
  34.  
  35. <ProgressBar
  36. android:id="@+id/progressBar"
  37. style="?android:attr/progressBarStyleHorizontal"
  38. android:layout_width="fill_parent"
  39. android:layout_height="18dp" />
  40.  
  41. <TextView
  42. android:id="@+id/resultView"
  43. android:layout_width="fill_parent"
  44. android:layout_height="wrap_content"
  45. android:gravity="center" />
  46. </LinearLayout>

/values/string.xml

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <resources>
  3. <string name="action_settings">Settings</string>
  4. <string name="hello_world">Hello world!</string>
  5. <string name="app_name">多线程断点下载器_欧阳鹏编写</string>
  6. <string name="path">下载路径</string>
  7. <string name="startbutton">开始下载</string>
  8. <string name="success">下载完成</string>
  9. <string name="error">下载失败</string>
  10. <string name="stopbutton">停止下载</string>
  11. <string name="sdcarderror">SDCard不存在或者写保护</string>
  12. </resources>

step3、程序主应用 cn.oyp.download.MainActivity.java文件

  1. package cn.oyp.download;
  2.  
  3. import java.io.File;
  4.  
  5. import android.app.Activity;
  6. import android.os.Bundle;
  7. import android.os.Environment;
  8. import android.os.Handler;
  9. import android.os.Message;
  10. import android.view.View;
  11. import android.widget.Button;
  12. import android.widget.EditText;
  13. import android.widget.ProgressBar;
  14. import android.widget.TextView;
  15. import android.widget.Toast;
  16. import cn.oyp.download.downloader.DownloadProgressListener;
  17. import cn.oyp.download.downloader.FileDownloader;
  18.  
  19. public class MainActivity extends Activity {
  20.  
  21. /** 下载路径文本框 **/
  22. private EditText pathText;
  23. /** 下载按钮 **/
  24. private Button downloadButton;
  25. /** 停止下载按钮 **/
  26. private Button stopbutton;
  27. /** 下载进度条 **/
  28. private ProgressBar progressBar;
  29. /** 下载结果文本框,显示下载的进度值 **/
  30. private TextView resultView;
  31.  
  32. /** Hanlder的作用是用于往创建Hander对象所在的线程所绑定的消息队列发送消息 **/
  33. private Handler handler = new UIHander();
  34.  
  35. @Override
  36. protected void onCreate(Bundle savedInstanceState) {
  37. super.onCreate(savedInstanceState);
  38. setContentView(R.layout.activity_main);
  39.  
  40. /** 初始化各控件 **/
  41. pathText = (EditText) this.findViewById(R.id.path);
  42. downloadButton = (Button) this.findViewById(R.id.downloadbutton);
  43. stopbutton = (Button) this.findViewById(R.id.stopbutton);
  44. progressBar = (ProgressBar) this.findViewById(R.id.progressBar);
  45. resultView = (TextView) this.findViewById(R.id.resultView);
  46. /** 设置按钮的监听 **/
  47. ButtonClickListener listener = new ButtonClickListener();
  48. downloadButton.setOnClickListener(listener);
  49. stopbutton.setOnClickListener(listener);
  50. }
  51.  
  52. /**
  53. * Hanlder的作用是用于往创建Hander对象所在的线程所绑定的消息队列发送消息
  54. */
  55. private final class UIHander extends Handler {
  56. public void handleMessage(Message msg) {
  57. switch (msg.what) {
  58. case 1:
  59. int size = msg.getData().getInt("size"); // 获取下载的进度值
  60. progressBar.setProgress(size); // 实时更新,设置下载进度值
  61. /** 计算下载的进度百分比 */
  62. float num = (float) progressBar.getProgress()
  63. / (float) progressBar.getMax();
  64. int result = (int) (num * 100);
  65. resultView.setText(result + "%"); // 设置下载结果文本框显示下载的进度值
  66. // 如果进度达到了进度最大值,即下载完毕
  67. if (progressBar.getProgress() == progressBar.getMax()) {
  68. Toast.makeText(getApplicationContext(), R.string.success, 1)
  69. .show();// 下载成功
  70. }
  71. break;
  72. case -1:
  73. Toast.makeText(getApplicationContext(), R.string.error, 1)
  74. .show();// 下载出错
  75. break;
  76. }
  77. }
  78. }
  79.  
  80. /**
  81. * 按钮监听类
  82. */
  83. private final class ButtonClickListener implements View.OnClickListener {
  84. public void onClick(View v) {
  85. switch (v.getId()) {
  86. /** 如果是下载按钮 */
  87. case R.id.downloadbutton:
  88. String path = pathText.getText().toString();// 获取下载路径
  89. // 判断SD卡是否存在并且可写
  90. if (Environment.getExternalStorageState().equals(
  91. Environment.MEDIA_MOUNTED)) {
  92. // 获取SD卡的路径
  93. File saveDir = Environment.getExternalStorageDirectory();
  94. // 开始下载的相关操作
  95. download(path, saveDir);
  96. } else {
  97. Toast.makeText(getApplicationContext(),
  98. R.string.sdcarderror, 1).show();
  99. }
  100. downloadButton.setEnabled(false);
  101. stopbutton.setEnabled(true);
  102. break;
  103. /** 如果是停止下载按钮 */
  104. case R.id.stopbutton:
  105. exit();// 退出下载
  106. downloadButton.setEnabled(true);
  107. stopbutton.setEnabled(false);
  108. break;
  109. }
  110. }
  111.  
  112. /**
  113. * UI控件画面的重绘(更新)是由主线程负责处理的,如果在子线程中更新UI控件的值,更新后的值不会重绘到屏幕上
  114. * 一定要在主线程里更新UI控件的值,这样才能在屏幕上显示出来,不能在子线程中更新UI控件的值
  115. * 借用Handler来传送UI控件的值到主线程去,在主线程更新UI控件的值
  116. */
  117. private final class DownloadTask implements Runnable {
  118. /** 下载路径 */
  119. private String path;
  120. /** 保存路径 */
  121. private File saveDir;
  122. /** 文件下载器 */
  123. private FileDownloader loader;
  124.  
  125. /**
  126. * DownloadTask的构造函数
  127. *
  128. * @param path
  129. * 下载路径
  130. * @param saveDir
  131. * 保存路径
  132. */
  133. public DownloadTask(String path, File saveDir) {
  134. this.path = path;
  135. this.saveDir = saveDir;
  136. }
  137.  
  138. /**
  139. * 线程主方法
  140. */
  141. public void run() {
  142. try {
  143. /**
  144. * 构建文件下载器 将下载路径,文件保存目录,下载线程数指定好
  145. */
  146. loader = new FileDownloader(getApplicationContext(), path,
  147. saveDir, 5);
  148. progressBar.setMax(loader.getFileSize());// 设置进度条的最大刻度(即文件的总长度)
  149. /**
  150. * DownloadProgressListener是一个接口,onDownloadSize()为未实现的方法。
  151. * onDownloadSize()方法会在download方法内部被动态赋值
  152. * 监听下载数量的变化,如果不需要了解实时下载的数量,可以设置为null
  153. */
  154. loader.download(new DownloadProgressListener() {
  155. public void onDownloadSize(int size) {
  156. // 借用Handler来传送UI控件的值到主线程去,在主线程更新UI控件的值
  157. Message msg = new Message();
  158. msg.what = 1; // 对应UIHander 获得的msg.what
  159. msg.getData().putInt("size", size); // 将获取的值发送给handler,用于动态更新进度
  160. handler.sendMessage(msg);
  161. }
  162. });
  163. } catch (Exception e) {
  164. e.printStackTrace();
  165. // 对应UIHander 获得的msg.what
  166. handler.sendMessage(handler.obtainMessage(-1));
  167. }
  168. }
  169.  
  170. /**
  171. * 退出下载
  172. */
  173. public void exit() {
  174. if (loader != null)
  175. loader.exit();
  176. }
  177. }
  178.  
  179. /** end of DownloadTask */
  180.  
  181. /**
  182. * 由于用户的输入事件(点击button, 触摸屏幕....)是由主线程负责处理的,如果主线程处于工作状态,
  183. * 此时用户产生的输入事件如果没能在5秒内得到处理,系统就会报“应用无响应”错误。
  184. * 所以在主线程里不能执行一件比较耗时的工作,否则会因主线程阻塞而无法处理用户的输入事件,
  185. * 导致“应用无响应”错误的出现。耗时的工作应该在子线程里执行。
  186. */
  187. private DownloadTask task;
  188.  
  189. /**
  190. * 退出下载
  191. */
  192. public void exit() {
  193. if (task != null)
  194. task.exit();
  195. }
  196.  
  197. /**
  198. * 下载方法,运行在主线程,负责开辟子线程完成下载操作,这操作耗时不超过1秒
  199. *
  200. * @param path
  201. * 下载路径
  202. * @param saveDir
  203. * 保存路径
  204. */
  205. private void download(String path, File saveDir) {
  206. task = new DownloadTask(path, saveDir);
  207. new Thread(task).start();// 开辟子线程完成下载操作
  208. }
  209. }
  210. /** end of ButtonClickListener **/
  211. }

文件下载器cn.oyp.download.downloader.FileDownloader.java文件

  1. package cn.oyp.download.downloader;
  2.  
  3. import java.io.File;
  4. import java.io.RandomAccessFile;
  5. import java.net.HttpURLConnection;
  6. import java.net.URL;
  7. import java.util.LinkedHashMap;
  8. import java.util.Map;
  9. import java.util.UUID;
  10. import java.util.concurrent.ConcurrentHashMap;
  11. import java.util.regex.Matcher;
  12. import java.util.regex.Pattern;
  13.  
  14. import cn.oyp.download.service.FileService;
  15.  
  16. import android.content.Context;
  17. import android.util.Log;
  18.  
  19. /**
  20. * 文件下载器
  21. */
  22. public class FileDownloader {
  23. private static final String TAG = "FileDownloader";
  24. /** 上下文 */
  25. private Context context;
  26. /** 文件下载服务类 */
  27. private FileService fileService;
  28. /** 是否停止下载 */
  29. private boolean exit;
  30. /** 已下载文件长度 */
  31. private int downloadSize = 0;
  32. /** 原始文件长度 */
  33. private int fileSize = 0;
  34. /** 用于下载的线程数组 */
  35. private DownloadThread[] threads;
  36. /** 本地保存文件 */
  37. private File saveFile;
  38. /** 缓存各线程下载的长度 */
  39. private Map<Integer, Integer> data = new ConcurrentHashMap<Integer, Integer>();
  40. /** 每条线程下载的长度 */
  41. private int block;
  42. /** 下载路径 */
  43. private String downloadUrl;
  44.  
  45. /**
  46. * 获取线程数
  47. */
  48. public int getThreadSize() {
  49. return threads.length;
  50. }
  51.  
  52. /**
  53. * 退出下载
  54. */
  55. public void exit() {
  56. this.exit = true;
  57. }
  58.  
  59. /**
  60. * 是否退出下载
  61. */
  62. public boolean getExit() {
  63. return this.exit;
  64. }
  65.  
  66. /**
  67. * 获取文件大小
  68. */
  69. public int getFileSize() {
  70. return fileSize;
  71. }
  72.  
  73. /**
  74. * 累计已下载大小
  75. * 该方法在具体某个线程下载的时候会被调用
  76. */
  77. protected synchronized void append(int size) {
  78. downloadSize += size;
  79. }
  80.  
  81. /**
  82. * 更新指定线程最后下载的位置
  83. * 该方法在具体某个线程下载的时候会被调用
  84. * @param threadId
  85. * 线程id
  86. * @param pos
  87. * 最后下载的位置
  88. */
  89. protected synchronized void update(int threadId, int pos) {
  90. // 缓存各线程下载的长度
  91. this.data.put(threadId, pos);
  92. // 更新数据库中的各线程下载的长度
  93. this.fileService.update(this.downloadUrl, threadId, pos);
  94. }
  95.  
  96. /**
  97. * 构建文件下载器
  98. *
  99. * @param downloadUrl
  100. * 下载路径
  101. * @param fileSaveDir
  102. * 文件保存目录
  103. * @param threadNum
  104. * 下载线程数
  105. */
  106. public FileDownloader(Context context, String downloadUrl,
  107. File fileSaveDir, int threadNum) {
  108. try {
  109. this.context = context;
  110. this.downloadUrl = downloadUrl;
  111. fileService = new FileService(this.context);
  112. // 根据指定的下载路径,生成URL
  113. URL url = new URL(this.downloadUrl);
  114. if (!fileSaveDir.exists())
  115. fileSaveDir.mkdirs();// 如果保存路径不存在,则新建一个目录
  116. // 根据指定的线程数来新建线程数组
  117. this.threads = new DownloadThread[threadNum];
  118. // 打开HttpURLConnection
  119. HttpURLConnection conn = (HttpURLConnection) url.openConnection();
  120. // 设置 HttpURLConnection的断开时间
  121. conn.setConnectTimeout(5 * 1000);
  122. // 设置 HttpURLConnection的请求方式
  123. conn.setRequestMethod("GET");
  124. // 设置 HttpURLConnection的接收的文件类型
  125. conn.setRequestProperty(
  126. "Accept",
  127. "image/gif, image/jpeg, image/pjpeg, image/pjpeg, "
  128. + "application/x-shockwave-flash, application/xaml+xml, "
  129. + "application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, "
  130. + "application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*");
  131. // 设置 HttpURLConnection的接收语音
  132. conn.setRequestProperty("Accept-Language", "zh-CN");
  133. // 指定请求uri的源资源地址
  134. conn.setRequestProperty("Referer", downloadUrl);
  135. // 设置 HttpURLConnection的字符编码
  136. conn.setRequestProperty("Charset", "UTF-8");
  137. // 检查浏览页面的访问者在用什么操作系统(包括版本号)浏览器(包括版本号)和用户个人偏好
  138. conn.setRequestProperty(
  139. "User-Agent",
  140. "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2;"
  141. + " Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; "
  142. + ".NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152;"
  143. + " .NET CLR 3.5.30729)");
  144. conn.setRequestProperty("Connection", "Keep-Alive");
  145. conn.connect();
  146. // 打印Http协议头
  147. printResponseHeader(conn);
  148. // 如果返回的状态码为200表示正常
  149. if (conn.getResponseCode() == 200) {
  150. this.fileSize = conn.getContentLength();// 根据响应获取文件大小
  151. if (this.fileSize <= 0)
  152. throw new RuntimeException("Unkown file size ");
  153.  
  154. String filename = getFileName(conn);// 获取文件名称
  155. this.saveFile = new File(fileSaveDir, filename);// 构建保存文件
  156. Map<Integer, Integer> logdata = fileService
  157. .getData(downloadUrl);// 获取下载记录
  158. if (logdata.size() > 0) {// 如果存在下载记录
  159. for (Map.Entry<Integer, Integer> entry : logdata.entrySet())
  160. data.put(entry.getKey(), entry.getValue());// 把各条线程已经下载的数据长度放入data中
  161. }
  162. if (this.data.size() == this.threads.length) {// 下面计算所有线程已经下载的数据总长度
  163. for (int i = 0; i < this.threads.length; i++) {
  164. this.downloadSize += this.data.get(i + 1);
  165. }
  166. print("已经下载的长度" + this.downloadSize);
  167. }
  168. // 计算每条线程下载的数据长度
  169. this.block = (this.fileSize % this.threads.length) == 0 ? this.fileSize
  170. / this.threads.length
  171. : this.fileSize / this.threads.length + 1;
  172. } else {
  173. throw new RuntimeException("server no response ");
  174. }
  175. } catch (Exception e) {
  176. print(e.toString());
  177. throw new RuntimeException("don't connection this url");
  178. }
  179. }
  180.  
  181. /**
  182. * 获取文件名
  183. *
  184. * @param conn
  185. * Http连接
  186. */
  187. private String getFileName(HttpURLConnection conn) {
  188. String filename = this.downloadUrl.substring(this.downloadUrl
  189. .lastIndexOf('/') + 1);// 截取下载路径中的文件名
  190. // 如果获取不到文件名称
  191. if (filename == null || "".equals(filename.trim())) {
  192. // 通过截取Http协议头分析下载的文件名
  193. for (int i = 0;; i++) {
  194. String mine = conn.getHeaderField(i);
  195. if (mine == null)
  196. break;
  197. /**
  198. * Content-disposition 是 MIME 协议的扩展,MIME 协议指示 MIME
  199. * 用户代理如何显示附加的文件。
  200. * Content-Disposition就是当用户想把请求所得的内容存为一个文件的时候提供一个默认的文件名
  201. * 协议头中的Content-Disposition格式如下:
  202. * Content-Disposition","attachment;filename=FileName.txt");
  203. */
  204. if ("content-disposition".equals(conn.getHeaderFieldKey(i)
  205. .toLowerCase())) {
  206. // 通过正则表达式匹配出文件名
  207. Matcher m = Pattern.compile(".*filename=(.*)").matcher(
  208. mine.toLowerCase());
  209. // 如果匹配到了文件名
  210. if (m.find())
  211. return m.group(1);// 返回匹配到的文件名
  212. }
  213. }
  214. // 如果还是匹配不到文件名,则默认取一个随机数文件名
  215. filename = UUID.randomUUID() + ".tmp";
  216. }
  217. return filename;
  218. }
  219.  
  220. /**
  221. * 开始下载文件
  222. *
  223. * @param listener
  224. * 监听下载数量的变化,如果不需要了解实时下载的数量,可以设置为null
  225. * @return 已下载文件大小
  226. * @throws Exception
  227. */
  228. public int download(DownloadProgressListener listener) throws Exception {
  229. try {
  230. RandomAccessFile randOut = new RandomAccessFile(this.saveFile, "rw");
  231. if (this.fileSize > 0)
  232. randOut.setLength(this.fileSize);
  233. randOut.close();
  234. URL url = new URL(this.downloadUrl);
  235. // 如果原先未曾下载或者原先的下载线程数与现在的线程数不一致
  236. if (this.data.size() != this.threads.length) {
  237. this.data.clear();// 清除原来的线程数组
  238. for (int i = 0; i < this.threads.length; i++) {
  239. this.data.put(i + 1, 0);// 初始化每条线程已经下载的数据长度为0
  240. }
  241. this.downloadSize = 0;
  242. }
  243. //循环遍历线程数组
  244. for (int i = 0; i < this.threads.length; i++) {
  245. int downLength = this.data.get(i + 1); // 获取当前线程下载的文件长度
  246. // 判断线程是否已经完成下载,否则继续下载
  247. if (downLength < this.block
  248. && this.downloadSize < this.fileSize) {
  249. //启动线程开始下载
  250. this.threads[i] = new DownloadThread(this, url,
  251. this.saveFile, this.block, this.data.get(i + 1),
  252. i + 1);
  253. this.threads[i].setPriority(7);
  254. this.threads[i].start();
  255. } else {
  256. this.threads[i] = null;
  257. }
  258. }
  259. //如果存在下载记录,从数据库中删除它们
  260. fileService.delete(this.downloadUrl);
  261. //重新保存下载的进度到数据库
  262. fileService.save(this.downloadUrl, this.data);
  263. boolean notFinish = true;// 下载未完成
  264. while (notFinish) {// 循环判断所有线程是否完成下载
  265. Thread.sleep(900);
  266. notFinish = false;// 假定全部线程下载完成
  267. for (int i = 0; i < this.threads.length; i++) {
  268. if (this.threads[i] != null && !this.threads[i].isFinish()) {// 如果发现线程未完成下载
  269. notFinish = true;// 设置标志为下载没有完成
  270. // 如果下载失败,再重新下载
  271. if (this.threads[i].getDownLength() == -1) {
  272. this.threads[i] = new DownloadThread(this, url,
  273. this.saveFile, this.block,
  274. this.data.get(i + 1), i + 1);
  275. this.threads[i].setPriority(7);
  276. this.threads[i].start();
  277. }
  278. }
  279. }
  280. if (listener != null)
  281. listener.onDownloadSize(this.downloadSize);// 通知目前已经下载完成的数据长度
  282. }
  283. // 如果下载完成
  284. if (downloadSize == this.fileSize)
  285. fileService.delete(this.downloadUrl);// 下载完成删除记录
  286. } catch (Exception e) {
  287. print(e.toString());
  288. throw new Exception("file download error");
  289. }
  290. return this.downloadSize;
  291. }
  292.  
  293. /**
  294. * 获取Http响应头字段
  295. * @param http
  296. * @return
  297. */
  298. public static Map<String, String> getHttpResponseHeader(
  299. HttpURLConnection http) {
  300. Map<String, String> header = new LinkedHashMap<String, String>();
  301. for (int i = 0;; i++) {
  302. String mine = http.getHeaderField(i);
  303. if (mine == null)
  304. break;
  305. header.put(http.getHeaderFieldKey(i), mine);
  306. }
  307. return header;
  308. }
  309.  
  310. /**
  311. * 打印Http头字段
  312. *
  313. * @param http
  314. */
  315. public static void printResponseHeader(HttpURLConnection http) {
  316. Map<String, String> header = getHttpResponseHeader(http);
  317. for (Map.Entry<String, String> entry : header.entrySet()) {
  318. String key = entry.getKey() != null ? entry.getKey() + ":" : "";
  319. print(key + entry.getValue());
  320. }
  321. }
  322. /**
  323. * 打印信息
  324. * @param msg 信息
  325. */
  326. private static void print(String msg) {
  327. Log.i(TAG, msg);
  328. }
  329. }

文件下载线程 cn.oyp.download.downloader.DownloadThread.java文件

  1. package cn.oyp.download.downloader;
  2.  
  3. import java.io.File;
  4. import java.io.InputStream;
  5. import java.io.RandomAccessFile;
  6. import java.net.HttpURLConnection;
  7. import java.net.URL;
  8.  
  9. import android.util.Log;
  10.  
  11. public class DownloadThread extends Thread {
  12. private static final String TAG = "DownloadThread";
  13. /** 本地保存文件 */
  14. private File saveFile;
  15. /** 下载路径 */
  16. private URL downUrl;
  17. /** 该线程要下载的长度 */
  18. private int block;
  19. /** 线程ID */
  20. private int threadId = -1;
  21. /** 该线程已经下载的长度 */
  22. private int downLength;
  23. /** 是否下载完成*/
  24. private boolean finish = false;
  25. /** 文件下载器 */
  26. private FileDownloader downloader;
  27. /***
  28. * 构造方法
  29. */
  30. public DownloadThread(FileDownloader downloader, URL downUrl,
  31. File saveFile, int block, int downLength, int threadId) {
  32. this.downUrl = downUrl;
  33. this.saveFile = saveFile;
  34. this.block = block;
  35. this.downloader = downloader;
  36. this.threadId = threadId;
  37. this.downLength = downLength;
  38. }
  39. /**
  40. * 线程主方法
  41. */
  42. @Override
  43. public void run() {
  44. if (downLength < block) {// 未下载完成
  45. try {
  46. HttpURLConnection http = (HttpURLConnection) downUrl
  47. .openConnection();
  48. http.setConnectTimeout(5 * 1000);
  49. http.setRequestMethod("GET");
  50. http.setRequestProperty(
  51. "Accept",
  52. "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash,"
  53. + " application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, "
  54. + "application/x-ms-application, application/vnd.ms-excel,"
  55. + " application/vnd.ms-powerpoint, application/msword, */*");
  56. http.setRequestProperty("Accept-Language", "zh-CN");
  57. http.setRequestProperty("Referer", downUrl.toString());
  58. http.setRequestProperty("Charset", "UTF-8");
  59. // 该线程开始下载位置
  60. int startPos = block * (threadId - 1) + downLength;
  61. // 该线程下载结束位置
  62. int endPos = block * threadId - 1;
  63. // 设置获取实体数据的范围
  64. http.setRequestProperty("Range", "bytes=" + startPos + "-"
  65. + endPos);
  66. http.setRequestProperty(
  67. "User-Agent",
  68. "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0;"
  69. + " .NET CLR 1.1.4322; .NET CLR 2.0.50727; "
  70. + ".NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
  71. http.setRequestProperty("Connection", "Keep-Alive");
  72. //获取输入流
  73. InputStream inStream = http.getInputStream();
  74. byte[] buffer = new byte[1024];
  75. int offset = 0;
  76. print("Thread " + this.threadId
  77. + " start download from position " + startPos);
  78. /**
  79. * rwd: 打开以便读取和写入,对于 "rw",还要求对文件内容的每个更新都同步写入到基础存储设备。
  80. * 对于Android移动设备一定要注意同步,否则当移动设备断电的话会丢失数据
  81. */
  82. RandomAccessFile threadfile = new RandomAccessFile(
  83. this.saveFile, "rwd");
  84. //直接移动到文件开始位置下载的
  85. threadfile.seek(startPos);
  86. while (!downloader.getExit()
  87. && (offset = inStream.read(buffer, 0, 1024)) != -1) {
  88. threadfile.write(buffer, 0, offset);//开始写入数据到文件
  89. downLength += offset; //该线程以及下载的长度增加
  90. downloader.update(this.threadId, downLength);//修改数据库中该线程已经下载的数据长度
  91. downloader.append(offset);//文件下载器已经下载的总长度增加
  92. }
  93. threadfile.close();
  94. inStream.close();
  95. print("Thread " + this.threadId + " download finish");
  96. this.finish = true;
  97. } catch (Exception e) {
  98. this.downLength = -1;
  99. print("Thread " + this.threadId + ":" + e);
  100. }
  101. }
  102. }
  103.  
  104. private static void print(String msg) {
  105. Log.i(TAG, msg);
  106. }
  107.  
  108. /**
  109. * 下载是否完成
  110. *
  111. * @return
  112. */
  113. public boolean isFinish() {
  114. return finish;
  115. }
  116.  
  117. /**
  118. * 已经下载的内容大小
  119. *
  120. * @return 如果返回值为-1,代表下载失败
  121. */
  122. public long getDownLength() {
  123. return downLength;
  124. }
  125. }

下载进度监听接口cn.oyp.download.downloader.DownloadProgressListener.java文件

  1. package cn.oyp.download.downloader;
  2.  
  3. /**
  4. * 下载进度监听接口
  5. */
  6. public interface DownloadProgressListener {
  7. /**
  8. *下载的进度
  9. */
  10. public void onDownloadSize(int size);
  11. }

数据库操作类 cn.oyp.download.service.DBOpenHelper.java类

  1. package cn.oyp.download.service;
  2.  
  3. import android.content.Context;
  4. import android.database.sqlite.SQLiteDatabase;
  5. import android.database.sqlite.SQLiteOpenHelper;
  6.  
  7. public class DBOpenHelper extends SQLiteOpenHelper {
  8. // 数据库文件的文件名
  9. private static final String DBNAME = "download.db";
  10. // 数据库的版本号
  11. private static final int VERSION = 1;
  12.  
  13. public DBOpenHelper(Context context) {
  14. super(context, DBNAME, null, VERSION);
  15. }
  16.  
  17. @Override
  18. public void onCreate(SQLiteDatabase db) {
  19. db.execSQL("CREATE TABLE IF NOT EXISTS filedownlog (id integer primary key autoincrement, downpath varchar(100), threadid INTEGER, downlength INTEGER)");
  20. }
  21.  
  22. @Override
  23. public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
  24. db.execSQL("DROP TABLE IF EXISTS filedownlog");
  25. onCreate(db);
  26. }
  27. }

文件下载服务类cn.oyp.download.service.FileService

  1. package cn.oyp.download.service;
  2.  
  3. import java.util.HashMap;
  4. import java.util.Map;
  5.  
  6. import android.content.Context;
  7. import android.database.Cursor;
  8. import android.database.sqlite.SQLiteDatabase;
  9.  
  10. /**
  11. * 文件下载服务类
  12. */
  13. public class FileService {
  14. private DBOpenHelper openHelper;
  15.  
  16. public FileService(Context context) {
  17. openHelper = new DBOpenHelper(context);
  18. }
  19.  
  20. /**
  21. * 获取每条线程已经下载的文件长度
  22. *
  23. * @param path
  24. * @return
  25. */
  26. public Map<Integer, Integer> getData(String path) {
  27. SQLiteDatabase db = openHelper.getReadableDatabase();
  28. Cursor cursor = db
  29. .rawQuery(
  30. "select threadid, downlength from filedownlog where downpath=?",
  31. new String[] { path });
  32. Map<Integer, Integer> data = new HashMap<Integer, Integer>();
  33. while (cursor.moveToNext()) {
  34. data.put(cursor.getInt(0), cursor.getInt(1));
  35. }
  36. cursor.close();
  37. db.close();
  38. return data;
  39. }
  40.  
  41. /**
  42. * 保存每条线程已经下载的文件长度
  43. *
  44. * @param path
  45. * @param map
  46. */
  47. public void save(String path, Map<Integer, Integer> map) {// int threadid,
  48. // int position
  49. SQLiteDatabase db = openHelper.getWritableDatabase();
  50. db.beginTransaction();
  51. try {
  52. for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
  53. db.execSQL(
  54. "insert into filedownlog(downpath, threadid, downlength) values(?,?,?)",
  55. new Object[] { path, entry.getKey(), entry.getValue() });
  56. }
  57. db.setTransactionSuccessful();
  58. } finally {
  59. db.endTransaction();
  60. }
  61. db.close();
  62. }
  63.  
  64. /**
  65. * 实时更新每条线程已经下载的文件长度
  66. *
  67. * @param path
  68. * @param map
  69. */
  70. public void update(String path, int threadId, int pos) {
  71. SQLiteDatabase db = openHelper.getWritableDatabase();
  72. db.execSQL(
  73. "update filedownlog set downlength=? where downpath=? and threadid=?",
  74. new Object[] { pos, path, threadId });
  75. db.close();
  76. }
  77.  
  78. /**
  79. * 当文件下载完成后,删除对应的下载记录
  80. *
  81. * @param path
  82. */
  83. public void delete(String path) {
  84. SQLiteDatabase db = openHelper.getWritableDatabase();
  85. db.execSQL("delete from filedownlog where downpath=?",
  86. new Object[] { path });
  87. db.close();
  88. }
  89. }

step4:AndroidManifest.xml

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
  3. package="cn.oyp.download"
  4. android:versionCode="1"
  5. android:versionName="1.0" >
  6.  
  7. <uses-sdk
  8. android:minSdkVersion="8"
  9. android:targetSdkVersion="17" />
  10. <!-- 访问Internet权限 -->
  11. <uses-permission android:name="android.permission.INTERNET" />
  12. <!-- 在SDCard中创建与删除文件权限 -->
  13. <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
  14. <!-- 往SDCard写入数据权限 -->
  15. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  16.  
  17. <application
  18. android:allowBackup="true"
  19. android:icon="@drawable/icon"
  20. android:label="@string/app_name"
  21. android:theme="@style/AppTheme" >
  22. <activity
  23. android:name="cn.oyp.download.MainActivity"
  24. android:label="@string/app_name" >
  25. <intent-filter>
  26. <action android:name="android.intent.action.MAIN" />
  27.  
  28. <category android:name="android.intent.category.LAUNCHER" />
  29. </intent-filter>
  30. </activity>
  31. </application>
  32.  
  33. </manifest>

step5:由于便于本项目的展示,所以新建一个JSP项目,部署到Tomcat服务器上,以供下载。

step6:部署应用,观看运行效果

1、打开应用

2、点击“开始下载”

3.点击“停止下载”

4.点击“开始下载”   会继续上一次的下载进度继续下载

5.退出应用,再进应用

6、点击“开始下载”,会继续上一次退出应用的时候的下载进度继续下载,完成断点下载

7.当下载完成的时候

==================================================================================================

  作者:欧阳鹏  欢迎转载,与人分享是进步的源泉!

  转载请保留原文地址:http://blog.csdn.net/ouyang_peng

==================================================================================================

读者下载源码后,会发现下载速度特别慢,有以下两种原因:

1、由于本身的网络速度的原因,不会特别快。

2、由于使用RandomAccessFile的原因,对IO操作太过于频繁。因此,我修改了DownloadThread类,修改代码如下,修改之后对速度有了点提升。在此特别感谢 pobi 读者的意见

  1. package cn.oyp.download.downloader;
  2.  
  3. import java.io.BufferedInputStream;
  4. import java.io.File;
  5. import java.io.FileInputStream;
  6. import java.io.IOException;
  7. import java.io.InputStream;
  8. import java.io.RandomAccessFile;
  9. import java.net.HttpURLConnection;
  10. import java.net.URL;
  11. import java.nio.ByteBuffer;
  12. import java.nio.channels.FileChannel;
  13.  
  14. import android.util.Log;
  15.  
  16. public class DownloadThread extends Thread {
  17. private static final String TAG = "DownloadThread";
  18. /** 本地保存文件 */
  19. private File saveFile;
  20. /** 下载路径 */
  21. private URL downUrl;
  22. /** 该线程要下载的长度 */
  23. private int block;
  24. /** 线程ID */
  25. private int threadId = -1;
  26. /** 该线程已经下载的长度 */
  27. private int downLength;
  28. /** 是否下载完成 */
  29. private boolean finish = false;
  30. /** 文件下载器 */
  31. private FileDownloader downloader;
  32.  
  33. /***
  34. * 构造方法
  35. */
  36. public DownloadThread(FileDownloader downloader, URL downUrl,
  37. File saveFile, int block, int downLength, int threadId) {
  38. this.downUrl = downUrl;
  39. this.saveFile = saveFile;
  40. this.block = block;
  41. this.downloader = downloader;
  42. this.threadId = threadId;
  43. this.downLength = downLength;
  44. }
  45.  
  46. /**
  47. * 线程主方法
  48. */
  49. @Override
  50. public void run() {
  51. if (downLength < block) {// 未下载完成
  52. try {
  53. HttpURLConnection http = (HttpURLConnection) downUrl
  54. .openConnection();
  55. http.setConnectTimeout(5 * 1000);
  56. http.setRequestMethod("GET");
  57. http.setRequestProperty(
  58. "Accept",
  59. "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash,"
  60. + " application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, "
  61. + "application/x-ms-application, application/vnd.ms-excel,"
  62. + " application/vnd.ms-powerpoint, application/msword, */*");
  63. http.setRequestProperty("Accept-Language", "zh-CN");
  64. http.setRequestProperty("Referer", downUrl.toString());
  65. http.setRequestProperty("Charset", "UTF-8");
  66. // 该线程开始下载位置
  67. int startPos = block * (threadId - 1) + downLength;
  68. // 该线程下载结束位置
  69. int endPos = block * threadId - 1;
  70. // 设置获取实体数据的范围
  71. http.setRequestProperty("Range", "bytes=" + startPos + "-"
  72. + endPos);
  73. http.setRequestProperty(
  74. "User-Agent",
  75. "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0;"
  76. + " .NET CLR 1.1.4322; .NET CLR 2.0.50727; "
  77. + ".NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
  78. http.setRequestProperty("Connection", "Keep-Alive");
  79. /****/
  80. System.out.println("DownloadThread http.getResponseCode():"
  81. + http.getResponseCode());
  82. if (http.getResponseCode() == 206) {
  83. /***
  84. * //获取输入流 InputStream inStream = http.getInputStream();
  85. * byte[] buffer = new byte[1024]; int offset = 0;
  86. * print("Thread " + this.threadId +
  87. * " start download from position " + startPos);
  88. *
  89. * // rwd: 打开以便读取和写入,对于 "rw",还要求对文件内容的每个更新都同步写入到基础存储设备。
  90. * //对于Android移动设备一定要注意同步,否则当移动设备断电的话会丢失数据 RandomAccessFile
  91. * threadfile = new RandomAccessFile( this.saveFile, "rwd");
  92. * //直接移动到文件开始位置下载的 threadfile.seek(startPos); while
  93. * (!downloader.getExit() && (offset = inStream.read(buffer,
  94. * 0, 1024)) != -1) { threadfile.write(buffer, 0,
  95. * offset);//开始写入数据到文件 downLength += offset; //该线程以及下载的长度增加
  96. * downloader.update(this.threadId,
  97. * downLength);//修改数据库中该线程已经下载的数据长度
  98. * downloader.append(offset);//文件下载器已经下载的总长度增加 }
  99. * threadfile.close();
  100. *
  101. * print("Thread " + this.threadId + " download finish");
  102. * this.finish = true;
  103. **/
  104. // 获取输入流
  105. InputStream inStream = http.getInputStream();
  106. BufferedInputStream bis = new BufferedInputStream(inStream);
  107. byte[] buffer = new byte[1024 * 4];
  108. int offset = 0;
  109. RandomAccessFile threadfile = new RandomAccessFile(
  110. this.saveFile, "rwd");
  111. // 获取RandomAccessFile的FileChannel
  112. FileChannel outFileChannel = threadfile.getChannel();
  113. // 直接移动到文件开始位置下载的
  114. outFileChannel.position(startPos);
  115. // 分配缓冲区的大小
  116. while (!downloader.getExit()
  117. && (offset = bis.read(buffer)) != -1) {
  118. outFileChannel
  119. .write(ByteBuffer.wrap(buffer, 0, offset));// 开始写入数据到文件
  120. downLength += offset; // 该线程以及下载的长度增加
  121. downloader.update(this.threadId, downLength);// 修改数据库中该线程已经下载的数据长度
  122. downloader.append(offset);// 文件下载器已经下载的总长度增加
  123. }
  124. outFileChannel.close();
  125. threadfile.close();
  126. inStream.close();
  127. print("Thread " + this.threadId + " download finish");
  128. this.finish = true;
  129. }
  130. } catch (Exception e) {
  131. this.downLength = -1;
  132. print("Thread " + this.threadId + ":" + e);
  133. }
  134. }
  135. }
  136.  
  137. private static void print(String msg) {
  138. Log.i(TAG, msg);
  139. }
  140.  
  141. /**
  142. * 下载是否完成
  143. *
  144. * @return
  145. */
  146. public boolean isFinish() {
  147. return finish;
  148. }
  149.  
  150. /**
  151. * 已经下载的内容大小
  152. *
  153. * @return 如果返回值为-1,代表下载失败
  154. */
  155. public long getDownLength() {
  156. return downLength;
  157. }
  158. }

==================================下面看一个gif动画===========================================

可以查看log日志,查看多线程下载的情况

==================================================================================================

  作者:欧阳鹏  欢迎转载,与人分享是进步的源泉!

  转载请保留原文地址:http://blog.csdn.net/ouyang_peng

==================================================================================================

博客写完后,又有读者提出了修改意见,在这儿特别感谢热心的读者给的建议,下面是读者JavaLover00000 给的建议:

我修改了部分代码后,具体的优化效果如下所示,修改后下载速度确实变快了很多。

修改前的效果:

修改后的效果:

具体修改的代码可以到以下地址进行下载:Android基于HTTP协议的多线程断点下载器的实现源码_第二次优化之后

主要是将1、buffer改为8k  
2、因为发现花费在更新数据库的时间比 read和write加起来的时间都要多一点,所以将更新数据库进度改为下载线程出现异常的时候更新单个线程进度和FileDownloader中的exit()中更新所有线程进度

代码修改的地方具体可以查看源代码中的FileDownloader.java和DownloadThread.java

==================================================================================================

  作者:欧阳鹏  欢迎转载,与人分享是进步的源泉!

  转载请保留原文地址:http://blog.csdn.net/ouyang_peng

==================================================================================================

我的Android进阶之旅------>Android基于HTTP协议的多线程断点下载器的实现的更多相关文章

  1. 我的Android进阶之旅------>Android颜色值(#AARRGGBB)透明度百分比和十六进制对应关系以及计算方法

    我的Android进阶之旅-->Android颜色值(RGB)所支持的四种常见形式 透明度百分比和十六进制对应关系表格 透明度 十六进制 100% FF 99% FC 98% FA 97% F7 ...

  2. 我的Android进阶之旅------>Android中查看应用签名信息

    一.查看自己的证书签名信息 如上一篇文章<我的Android进阶之旅------>Android中制作和查看自定义的Debug版本Android签名证书>地址:http://blog ...

  3. 我的Android进阶之旅------>Android利用温度传感器实现带动画效果的电子温度计

    要想实现带动画效果的电子温度计,需要以下几个知识点: 1.温度传感器相关知识. 2.ScaleAnimation动画相关知识,来进行水印刻度的缩放效果. 3.android:layout_weight ...

  4. 我的Android进阶之旅------>Android实现用Android手机控制PC端的关机和重启的功能(三)Android客户端功能实现

    我的Android进阶之旅------>Android实现用Android手机控制PC端的关机和重启的功能(一)PC服务器端(地址:http://blog.csdn.net/ouyang_pen ...

  5. 我的Android进阶之旅------> Android为TextView组件中显示的文本添加背景色

    通过上一篇文章 我的Android进阶之旅------> Android在TextView中显示图片方法 (地址:http://blog.csdn.net/ouyang_peng/article ...

  6. 我的Android进阶之旅------> Android在TextView中显示图片方法

    面试题:请说出Android SDK支持哪些方式显示富文本信息(不同颜色.大小.并包含图像的文本信息),并简要说明实现方法. 答案:Android SDK支持如下显示富文本信息的方式. 1.使用Tex ...

  7. 我的Android进阶之旅------>Android疯狂连连看游戏的实现之实现游戏逻辑(五)

    在上一篇<我的Android进阶之旅------>Android疯狂连连看游戏的实现之加载界面图片和实现游戏Activity(四)>中提到的两个类: GameConf:负责管理游戏的 ...

  8. 我的Android进阶之旅------>Android疯狂连连看游戏的实现之加载界面图片和实现游戏Activity(四)

    正如在<我的Android进阶之旅------>Android疯狂连连看游戏的实现之状态数据模型(三)>一文中看到的,在AbstractBoard的代码中,当程序需要创建N个Piec ...

  9. 我的Android进阶之旅------>Android疯狂连连看游戏的实现之状态数据模型(三)

    对于游戏玩家而言,游戏界面上看到的"元素"千变万化:但是对于游戏开发者而言,游戏界面上的元素在底层都是一些数据,不同数据所绘制的图片有所差异而已.因此建立游戏的状态数据模型是实现游 ...

随机推荐

  1. 转:大数据 2016 landscape

    如图:

  2. 如何看一个VC工程具体是什么工程?

    VC6等可以创建MFC, Win32等工程,拿到一个工程,怎么判断是什么工程呢? VC6全文检索看看有没有theApp 如果有一般就是MFC的 (VS?)看看工程设置,入口点函数写的是什么,/subs ...

  3. 系统封装 如何为原生PE集成软件

    1 我们首先集成Explorer.老外的BSExplorer比较好用,下载之后得到这些文件,不算太大.   2 这里需要注意,前一章讲解如何打造原生PE已经制作成了ISO,这里想要集成软件还需要回到刚 ...

  4. C中的C文件与h文件辨析

    简单的说其实要理解C文件与头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程:       1.预处理阶段 2.词法与语法分析阶段 .编译阶段,首先编译成纯 ...

  5. Eclipse 使用 SVN 插件后改动用户方法汇总

    判定 SVN 插件是哪个 JavaH 的处理方法 SVNKit 的处理方法 工具自带改动功能 删除缓存的秘钥文件 其他发表地点 判定 SVN 插件是哪个 常见的 Eclipse SVN 插件我知道的一 ...

  6. 【Excle数据透视表】如何让字段标题不显示“求和项”

    我们做好了数据透视表之后是下面这个样子的 这个样子一点都不好看,那么如何去掉"求和项"呢? 步骤 方法① 单击B3单元格→编辑区域输入"数量 "→Enter(也 ...

  7. Android 使用SwipeBackLayout实现滑动返回上一级页面——实战来袭

    我们知道.APP在设计上习惯性的把返回button放在屏幕的左上角,那么,在非常多时候(尤其是大屏幕手机),操作改返回button,就会有诸多不便了.为了更加方便实现"返回"功能. ...

  8. 压力测试工具集合(ab,webbench,Siege,http_load,Web Application Stress)

    压力测试工具集合(ab,webbench,Siege,http_load,Web Application Stress) 1 Apache附带的工具ab ab的全称是ApacheBench,是Apac ...

  9. mysql时间操作(时间差和时间戳和时间字符串的互转)

    mysql时间操作(时间差和时间戳和时间字符串的互转) 两个时间差: MySQL datediff(date1,date2):两个日期相减 date1 - date2,返回天数. select dat ...

  10. Atitit.异步编程的发展历史 1.1. TAP & async/await

    Atitit.异步编程的发展历史 1.1. TAP & async/await 1. 异步编程的发展历史1 1.1. Thread1 1.2. Task1 1.3. Async await2 ...