Android下实现多线程断点下载
原理
主要通过设置Http的请求头Range字段,然后启动多个线程让每一个线程负责一段数据的下载,然后当所有线程的数据下载完成后,那么整个文件就下载完整了。在下载过程中当每个线程每次写入一段数据后,我们同时还要记录当前线程已经下载的大小(可以通过数据库、文件或者SharedPreferences来持久化),那么当程序异常退出或者用户暂停下载后,用户下次再次下载的时候可以继续从之前下载的基础上继续下载文件而不用整个文件再次下载了。
例如:
http.setRequestProperty("Range", "bytes=" + startPos + "-"+ endPos);
有关http中range字段的介绍请参考其他资料。
实现
线程下载类(负责一段数据的下载)
public class DownloadThread {
private static final String TAG = "DownloadThread";
private static final String THREAD_NAME = "downloader-";
private File saveFile;
private String downloadUrl;
private int block;
private int threadId = -1;
private int downLength;
private boolean finish = false;
private FileDownloader.DownloaderTask task;
private static int BUFFER_SIZE = 8 * 1024;
private int retryCount = 3;
public DownloadThread(FileDownloader.DownloaderTask task, String downloadUrl, File saveFile, int block, int downLength, int threadId) {
this.downloadUrl = downloadUrl;
this.saveFile = saveFile;
this.block = block;
this.task = task;
this.threadId = threadId;
this.downLength = downLength;
}
public void run() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
rangeDownload();
}
});
thread.setPriority(7);
thread.setName(THREAD_NAME + threadId );
thread.start();
}
private void rangeDownload() {
if(downLength < block){
HttpURLConnection http = null;
RandomAccessFile threadFile = null;
InputStream inStream = null;
try {
http = (HttpURLConnection) new URL(downloadUrl).openConnection();
http.setConnectTimeout(15 * 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", downloadUrl);
http.setRequestProperty("Charset", "UTF-8");
final int startPos = block * (threadId - 1) + downLength;
final int endPos = block * threadId -1;
http.setRequestProperty("Range", "bytes=" + startPos + "-"+ endPos);
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");
inStream = http.getInputStream();
byte[] buffer = new byte[BUFFER_SIZE];
int offset = 0;
KSLog2.KSLog().d("Thread " + DownloadThread.this.threadId + " start download from position "+ startPos);
threadFile = new RandomAccessFile(DownloadThread.this.saveFile, "rwd");
threadFile.seek(startPos);
while (!task.getExit() && (offset = inStream.read(buffer, 0, BUFFER_SIZE)) != -1) {
threadFile.write(buffer, 0, offset);
downLength += offset;
task.update(downloadUrl, this.threadId, downLength);
task.append(offset);
}
KSLog2.KSLog().d("Thread " + this.threadId + " download finish" + " exit = "+ task.getExit() + " offset = " + offset);
setFinish(true);
} catch (Exception e) {
e.printStackTrace();
if(NetUtil.isWifiConnected(OfficeApp.getInstance()) && retryCount > 0) {
retryCount--;
run();
}else {
setFinish(true);
}
}finally {
if(null != http) {
http.disconnect();
}
if(null != threadFile) {
try {
threadFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(null != inStream) {
try {
inStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}//end try
}else {
setFinish(true);
}
}
public synchronized boolean isFinish() {
return finish;
}
public synchronized void setFinish(boolean finish) {
this.finish = finish;
}
public long getDownLength() {
return downLength;
}
}
负责管理所有线程的下载器类
public class FileDownloader {
private static final String TAG = "FileDownloader";
private static final String DOWNLOAD_SUFFIX = ".temp";
private Context context;
private FileService fileService;
private File fileSaveDir;
private int threadNum;
private boolean exit = false;
private boolean isDownloading = false;
public FileDownloader(Context context, File fileSaveDir, int threadNum) {
this.context = context;
this.fileService = new FileService(this.context);
this.fileSaveDir = fileSaveDir;
if(threadNum <= 0) {
throw new RuntimeException("threadNum must be greater than 1 ");
}
this.threadNum = threadNum;
if (!fileSaveDir.exists())
fileSaveDir.mkdirs();
}
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
public static long getUsableSpace(File path) {
if (Utils.hasGingerbread()) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
class DownloaderTask extends AsyncTask<String,Integer,String> {
int fileSize;
Map<Integer, Integer> data = new ConcurrentHashMap<Integer, Integer>();
OnDownloadListener listener;
int downloadSize = 0;
boolean isSuccess = false;
String newPath;
private DownloadThread[] threads;
public DownloaderTask(OnDownloadListener listener) {
super();
this.listener = listener;
this.threads = new DownloadThread[threadNum];
}
@Override
protected void onPreExecute() {
super.onPreExecute();
isDownloading = true;
}
@Override
protected String doInBackground(String... params) {
final String downloadUrl = params[0];
final String fileName = params[1];
String reason = _download(downloadUrl,fileName);
return reason;
}
@Override
protected void onPostExecute(String result) {
if (isSuccess ) {
if( null != listener)
listener.onSuccess(newPath, downloadSize);
}else {
if( null != listener)
listener.onFailed(result);
}
isDownloading = false;
}
@Override
protected void onProgressUpdate(Integer... values) {
if (listener != null)
listener.onProcess(values[0]);
}
public String _download(String downloadUrl, String fileName) {
isSuccess = false;
setExit(false);
String result = "success";
if (TextUtils.isEmpty(downloadUrl)) {
result = "error: downloadUrl is empty";
return result;
}
this.fileSize = getFileSize(downloadUrl);
if (this.fileSize <= 0)
throw new RuntimeException("Unknown file size ");
//当可用空间不足时取消下载
if (getUsableSpace(FileDownloader.this.fileSaveDir) < this.fileSize) {
result = "error: Insufficient storage space in system";
return result;
}
initLogData(downloadUrl);
initCurrentDownloadSize();
try {
final File tempFile = new File(fileSaveDir, fileName + DOWNLOAD_SUFFIX);
RandomAccessFile randOut = new RandomAccessFile(tempFile, "rw");
randOut.setLength(this.fileSize);
randOut.close();
fileService.delete(downloadUrl);
fileService.save(downloadUrl, this.data);
final int block = calculateBlock();
for (int i = 0; i < this.threads.length; i++) {
int downLength = this.data.get(i + 1);
if (downLength < block && this.downloadSize < this.fileSize) {
this.threads[i] = new DownloadThread(this, downloadUrl, tempFile, block, downLength, i + 1);
this.threads[i].run();
} else {
this.threads[i] = null;
}
}
boolean notFinish = true;
while (notFinish) {
Thread.sleep(500);
notFinish = false;
for (int i = 0; i < this.threads.length; i++) {
if (this.threads[i] != null && !this.threads[i].isFinish()) {
notFinish = true;
}
}
publishProgress(this.downloadSize);
}
if (downloadSize == this.fileSize) {
fileService.delete(downloadUrl);
final String oldPath = tempFile.getAbsolutePath();
newPath = oldPath.substring(0, oldPath.lastIndexOf(DOWNLOAD_SUFFIX));
if(tempFile.renameTo(new File(newPath))) {
isSuccess = true;
}else {
result = "error: rename file failed";
}
}else {
result = "error: download file size inequality";
}
} catch (Exception e) {
e.printStackTrace();
result = e.getMessage();
}
return result;
}
public void setExit(boolean exit) {
FileDownloader.this.setExit(exit);
}
public boolean getExit() {
return FileDownloader.this.getExit();
}
public int getFileSize() {
return fileSize;
}
protected synchronized void append(int size) {
downloadSize += size;
}
private int calculateBlock() {
return (this.fileSize % this.threads.length) == 0 ? this.fileSize
/ this.threads.length
: this.fileSize / this.threads.length + 1;
}
private int getFileSize(String url) {
HttpURLConnection conn = null;
int fileSize = -1;
try {
conn = (HttpURLConnection) (new URL(url)).openConnection();
conn.setRequestMethod("HEAD");// 避免下载其他的内容
fileSize = conn.getContentLength();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (conn != null)
conn.disconnect();
}
return fileSize;
}
private void initLogData(String downloadUrl) {
this.data.clear();
Map<Integer, Integer> logData = fileService.getData(downloadUrl);
if (logData.size() > 0) {
for (Map.Entry<Integer, Integer> entry : logData.entrySet())
data.put(entry.getKey(), entry.getValue());
}
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);
}
}
}
private void initCurrentDownloadSize() {
this.downloadSize = 0;
if (this.data.size() == this.threads.length) {
for (int i = 0; i < this.threads.length; i++) {
KSLog2.KSLog().d("thread " + (i + 1) + " download size = " + this.data.get(i + 1));
this.downloadSize += this.data.get(i + 1);
}
}
}
protected synchronized void update(String downloadUrl, int threadId, int pos) {
this.data.put(threadId, pos);
FileDownloader.this.fileService.update(downloadUrl, threadId, pos);
}
}
public void download(String downloadUrl, String fileName, OnDownloadListener listener) throws Exception {
if (TextUtils.isEmpty(downloadUrl) || TextUtils.isEmpty(fileName) || isDownloading) return;
DownloaderTask task = new DownloaderTask(listener);
task.execute(downloadUrl,fileName);
}
/**
* 终止下载
*/
public synchronized void exit() {
FileDownloader.this.exit = true;
}
public synchronized void setExit(boolean exit) {
FileDownloader.this.exit = exit;
}
public synchronized boolean getExit() {
return FileDownloader.this.exit;
}
}
下载回调接口
public interface OnDownloadListener {
/**
* 文件正在下载的回调
* @param size 当前已经下载的大小
*/
public void onProcess(int size);
/**
* 下载成功回调
* @param path 文件下载后的路径
* @param fileSize 文件的大小
*/
public void onSuccess(String path, int fileSize);
/**
* 下载失败回调
* @param msg 失败信息
*/
public void onFailed(String msg);
}
数据存储类(保存下载进度)
public class FileService {
public FileService(Context context) {
DBManager.initializeInstance(new DBOpenHelper(context));
}
public Map<Integer, Integer> getData(String path){
SQLiteDatabase db = DBManager.getInstance().openDatabase();
Cursor cursor = db.rawQuery("select threadid, downlength from filedownlog where downpath=?", new String[]{path});
Map<Integer, Integer> data = new HashMap<Integer, Integer>();
while(cursor.moveToNext()){
data.put(cursor.getInt(0), cursor.getInt(1));
}
cursor.close();
DBManager.getInstance().closeDatabase();
return data;
}
public void save(String path, Map<Integer, Integer> map){//int threadid, int position
SQLiteDatabase db = DBManager.getInstance().openDatabase();
db.beginTransaction();
try{
for(Map.Entry<Integer, Integer> entry : map.entrySet()){
db.execSQL("insert into filedownlog(downpath, threadid, downlength) values(?,?,?)",
new Object[]{path, entry.getKey(), entry.getValue()});
}
db.setTransactionSuccessful();
}finally{
db.endTransaction();
}
DBManager.getInstance().closeDatabase();
}
public void update(String path, int threadId, int pos){
try {
SQLiteDatabase db = DBManager.getInstance().openDatabase();
db.execSQL("update filedownlog set downlength=? where downpath=? and threadid=?", new Object[]{pos, path, threadId});
DBManager.getInstance().closeDatabase();
}catch (Exception e) {
e.printStackTrace();
}
}
public void delete(String path){
try {
SQLiteDatabase db = DBManager.getInstance().openDatabase();
db.execSQL("delete from filedownlog where downpath=?", new Object[]{path});
DBManager.getInstance().closeDatabase();
}catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* Created by CW80JD2 on 2017/5/16.
*/
public class DBManager {
//解决多线程并发
private AtomicInteger mOpenCounter = new AtomicInteger();
private static DBManager instance;
private static SQLiteOpenHelper mDatabaseHelper;
private SQLiteDatabase mDatabase;
private DBManager(){
}
/**
* 初始化
* @param helper
*/
public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
if (instance == null) {
instance = new DBManager();
mDatabaseHelper = helper;
}
}
/**
* 获得当前实例对象
* @return
*/
public static synchronized DBManager getInstance() {
if (instance == null) {
throw new IllegalStateException(
DBManager.class.getSimpleName()
+ " is not initialized, call initializeInstance(..) method first.");
}
return instance;
}
/**
* 打开数据库对象
* @return
*/
public synchronized SQLiteDatabase openDatabase() {
if (mOpenCounter.incrementAndGet() == 1) {
// Opening new database
mDatabase = mDatabaseHelper.getWritableDatabase();
}
return mDatabase;
}
/**
* 多线程下关闭
*/
public synchronized void closeDatabase() {
if (mOpenCounter.decrementAndGet() == 0) {
// Closing database
mDatabase.close();
}
}
}
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class DBOpenHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "scanner.db";
private static final int VERSION = 1;
public DBOpenHelper(Context context) {
super(context, DB_NAME, null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE IF NOT EXISTS filedownlog (id INTEGER primary key autoincrement, downpath varchar(1024), threadid INTEGER, downlength INTEGER)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS filedownlog");
onCreate(db);
}
}