Android实现多线程断点续传

时间:2022-09-04 18:41:13

前言: 项目都快交付阶段了,客户说要改个需求,添加一个断点续传功能。在版本更新,杂志下载或者视频下载的时候实现断点续传。由于时间紧迫,想起了之前研究过一个demo代码,就直接修改使用了,根据自己的方式实现,但是核心代码没变。以后或许会用到,于是就专门写了个demo。


先看一下项目目录结构:


Android实现多线程断点续传

  1. db--->操作数据库的(创建数据库表,数据的增删改查。)
  2. util--->工具类
  3. 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


看一下效果图:Android实现多线程断点续传



下一篇打算:  使用downloadManager实现下载,断点续传。