Android实现多线程断点续传

时间:2022-09-04 18:54:28

前言: 项目都快交付阶段了,客户说要改个需求,添加一个断点续传功能。在版本更新,杂志下载或者视频下载的时候实现断点续传。由于时间紧迫,想起了之前研究过一个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实现下载,断点续传。