android中提供多线程下载思路
2010年12月13日
单线程下载HTTP文件对我们来说是一件非常简单的事。那么,多线程断点下载的难点在哪里?1.多线程下载,2.支持断点。
多线程下载:
如何才能从文件的指定位置处开始下载文件?(比如从50MB开始)这一点我们可以通过HTTP请求信息头来设置,还记得HTTP请求信息头的“Range”属性吗?
断点:
首要问题(多线程下载)已经被我们解决了,支持断点下载想必大家也已经想到了。就是将下载的进度保存到文件中,但在Android中却不能这么做。在Android平台中,我们需要向文件中写出下载的文件数据,还需要向另一个文件中写出下载进度,这样会出错。这样会导致有一个文件的内容没有被写出。所以我们就不能以文件的方式来保存下载进度,但可以通过数据库的方式保存下载进度。这两大问题我们已经有了解决思路,那么就开始动手编写吧!(以下源码仅供学习)
1.创建Android工程
Project name:MulThreadDownloader
BuildTarget:Android2.1
Application name:多线程断点下载
Package name:com.changcheng.download
Create Activity:MulThreadDownloader
Min SDK Version:7
2.AndroidManifest.xml
3.strings.xml
Hello World, DownloadActivity!
多线程断点下载
下载路径
下载
SDCard不存在或者写保护
4.main.xml
5.MulThreadDownloader
package com.changcheng.download;
import java.io.File;
import com.changcheng.net.download.DownloadProgressListener;
import com.changcheng.net.download.FileDownloader;
import com.changcheng.download.R;
import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
public class MulThreadDownloader extends Activity {
private EditText pathText;
private ProgressBar progressBar;
private TextView resultView;
private Handler handler = new Handler(){
[email protected]
public void handleMessage(Message msg) {
if(!Thread.currentThread().isInterrupted()){
switch (msg.what) {
case 1:
// 获取当前文件下载的进度
int size = msg.getData().getInt("size");
progressBar.setProgress(size);
int result = (int)(((float)size/(float)progressBar.getMax()) * 100);
resultView.setText(result+ "%");
if(progressBar.getMax() == size){
Toast.makeText(MulThreadDownloader.this, "文件下载完成", 1).show();
}
break;
case -1:
String error = msg.getData().getString("error");
Toast.makeText(MulThreadDownloader.this, error, 1).show();
break;
}
}
super.handleMessage(msg);
}
};
[email protected]
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
pathText = (EditText)this.findViewById(R.id.path);
progressBar = (ProgressBar)this.findViewById(R.id.downloadbar);
resultView = (TextView)this.findViewById(R.id.resultView);
Button button = (Button)this.findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
[email protected]
public void onClick(View v) {
String path = pathText.getText().toString();
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
//下载文件需要很长的时间,主线程是不能够长时间被阻塞,如果主线程被长时间阻塞, 那么Android被回收应用
download(path, Environment.getExternalStorageDirectory());
}else{
Toast.makeText(MulThreadDownloader.this, R.string.sdcarderror, 1).show();
}
}
});
}
/**
* 下载文件
* @param path 下载路径
* @param saveDir 文件保存目录
*/
//对于Android的UI控件,只能由主线程负责显示界面的更新,其他线程不能直接更新UI控件的显示
public void download(final String path, final File saveDir){
new Thread(new Runnable() {
[email protected]
public void run() {
FileDownloader downer = new FileDownloader(MulThreadDownloader.this, path, saveDir, 3);
progressBar.setMax(downer.getFileSize());//设置进度条的最大刻度
try {
downer.download(new DownloadProgressListener(){
[email protected]
public void onDownloadSize(int size) {
Message msg = new Message();
msg.what = 1;
msg.getData().putInt("size", size);
handler.sendMessage(msg);//发送消息
}});
} catch (Exception e) {
Message msg = new Message();
msg.what = -1;
msg.getData().putString("error", "下载失败");
handler.sendMessage(msg);
}
}
}).start();
}
}
6.FileDownload
package com.changcheng.net.download;
import java.io.File;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
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 com.changcheng.download.service.FileService;
import android.content.Context;
import android.util.Log;
/**
* 文件下载器
* @author [email protected]
*
*/
public class FileDownloader {
private Context context;
private FileService fileService;
private static final String TAG = "FileDownloader";
/* 已下载文件大小 */
private int downloadSize = 0;
/* 原始文件大小 */
private int fileSize = 0;
/* 线程数 */
private DownloadThread[] threads;
/* 下载路径 */
private URL url;
/* 本地保存文件 */
private File saveFile;
/* 下载记录文件 */
private File logFile;
/* 缓存各线程最后下载的位置*/
private Map data = new ConcurrentHashMap();
/* 每条线程下载的大小 */
private int block;
private String downloadUrl;//下载路径
/**
* 获取线程数
*/
public int getThreadSize() {
return threads.length;
}
/**
* 获取文件大小
* @return
*/
public int getFileSize() {
return fileSize;
}
/**
* 累计已下载大小
* @param size
*/
protected synchronized void append(int size) {
downloadSize += size;
}
/**
* 更新指定线程最后下载的位置
* @param threadId 线程id
* @param pos 最后下载的位置
*/
protected void update(int threadId, int pos) {
this.data.put(threadId, pos);
}
/**
* 保存记录文件
*/
protected synchronized void saveLogFile() {
this.fileService.update(this.downloadUrl, this.data);
}
/**
* 构建文件下载器
* @param downloadUrl 下载路径
* @param fileSaveDir 文件保存目录
* @param threadNum 下载线程数
*/
public FileDownloader(Context context, String downloadUrl, File fileSaveDir, int threadNum) {
try {
this.context = context;
this.downloadUrl = downloadUrl;
fileService = new FileService(context);
this.url = new URL(downloadUrl);
if(!fileSaveDir.exists()) fileSaveDir.mkdirs();
this.threads = new DownloadThread[threadNum];
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(6*1000);
conn.setRequestMethod("GET");
conn.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, */*");
conn.setRequestProperty("Accept-Language", "zh-CN");
conn.setRequestProperty("Referer", downloadUrl);
conn.setRequestProperty("Charset", "UTF-8");
conn.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)");
conn.setRequestProperty("Connection", "Keep-Alive");
conn.connect();
printResponseHeader(conn);
if (conn.getResponseCode()==200) {
this.fileSize = conn.getContentLength();//根据响应获取文件大小
if (this.fileSize logdata = fileService.getData(downloadUrl);
if(logdata.size()>0){
data.putAll(logdata);
}
this.block = this.fileSize / this.threads.length + 1;
if(this.data.size()==this.threads.length){
for (int i = 0; i 0) randOut.setLength(this.fileSize);
randOut.seek(this.data.get(i+1));
this.threads = new DownloadThread(this, this.url, randOut, this.block, this.data.get(i+1), i+1);
this.threads.setPriority(7);
this.threads.start();
}else{
this.threads = null;
}
}
this.fileService.save(this.downloadUrl, this.data);
boolean notFinish = true;//下载未完成
while (notFinish) {// 循环判断是否下载完毕
Thread.sleep(900);
notFinish = false;//假定下载完成
for (int i = 0; i getHttpResponseHeader(HttpURLConnection http) {
Map header = new LinkedHashMap();
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 header = getHttpResponseHeader(http);
for(Map.Entry entry : header.entrySet()){
String key = entry.getKey()!=null ? entry.getKey()+ ":" : "";
print(key+ entry.getValue());
}
}
private static void print(String msg){
Log.i(TAG, msg);
}
}
7.DownloadProgressListener
package com.changcheng.net.download;
public interface DownloadProgressListener {
public void onDownloadSize(int size);
}
8.FileService
package com.changcheng.download.service;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
/**
* 业务bean
*
*/
public class FileService {
private DBOpenHelper openHelper;
public FileService(Context context) {
openHelper = new DBOpenHelper(context);
}
/**
* 获取线程最后下载位置
* @param path
* @return
*/
public Map getData(String path){
SQLiteDatabase db = openHelper.getReadableDatabase();
Cursor cursor = db.rawQuery("select threadid, position from filedown where downpath=?", new String[]{path});
Map data = new HashMap();
while(cursor.moveToNext()){
data.put(cursor.getInt(0), cursor.getInt(1));
}
cursor.close();
db.close();
return data;
}
/**
* 保存下载线程初始位置
* @param path
* @param map
*/
public void save(String path, Map map){//int threadid, int position
SQLiteDatabase db = openHelper.getWritableDatabase();
db.beginTransaction();
try{
for(Map.Entry entry : map.entrySet()){
db.execSQL("insert into filedown(downpath, threadid, position) values(?,?,?)",
new Object[]{path, entry.getKey(), entry.getValue()});
}
db.setTransactionSuccessful();
}finally{
db.endTransaction();
}
db.close();
}
/**
* 实时更新线程的最后下载位置
* @param path
* @param map
*/
public void update(String path, Map map){
SQLiteDatabase db = openHelper.getWritableDatabase();
db.beginTransaction();
try{
for(Map.Entry entry : map.entrySet()){
db.execSQL("update filedown set position=? where downpath=? and threadid=?",
new Object[]{entry.getValue(), path, entry.getKey()});
}
db.setTransactionSuccessful();
}finally{
db.endTransaction();
}
db.close();
}
/**
* 当文件下载完成后,清掉该文件对应的下载记录
* @param path
*/
public void delete(String path){
SQLiteDatabase db = openHelper.getWritableDatabase();
db.execSQL("delete from filedown where downpath=?", new Object[]{path});
db.close();
}
}
9.DownloadThread
package com.changcheng.net.download;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import android.util.Log;
public class DownloadThread extends Thread {
private static final String TAG = "DownloadThread";
private RandomAccessFile saveFile;
private URL downUrl;
private int block;
/* 下载开始位置 */
private int threadId = -1;
private int startPos;
private int downLength;
private boolean finish = false;
private FileDownloader downloader;
public DownloadThread(FileDownloader downloader, URL downUrl, RandomAccessFile saveFile, int block, int startPos, int threadId) {
this.downUrl = downUrl;
this.saveFile = saveFile;
this.block = block;
this.startPos = startPos;
this.downloader = downloader;
this.threadId = threadId;
this.downLength = startPos - (block * (threadId - 1));
}
[email protected]
public void run() {
if(downLength 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");
InputStream inStream = http.getInputStream();
int max = block>1024 ? 1024 : (block>10 ? 10 : 1);
byte[] buffer = new byte[max];
int offset = 0;
print("线程 " + this.threadId + "从位置"+ this.startPos+ "开始下载 ");
while (downLength < block && (offset = inStream.read(buffer, 0, max)) != -1) {
saveFile.write(buffer, 0, offset);
downLength += offset;
downloader.update(this.threadId, block * (threadId - 1) + downLength);
downloader.saveLogFile();
downloader.append(offset);
int spare = block-downLength;//求剩下的字节数
if(spare < max) max = (int) spare;
}
saveFile.close();
inStream.close();
print("线程 " + this.threadId + "完成下载 ");
this.finish = true;
this.interrupt();
} catch (Exception e) {
this.downLength = -1;
print("线程"+ this.threadId+ ":"+ e);
}
}
}
private static void print(String msg){
Log.i(TAG, msg);
}
/**
* 下载是否完成
* @return
*/
public boolean isFinish() {
return finish;
}
/**
* 已经下载的内容大小
* @return 如果返回值为-1,代表下载失败
*/
public long getDownLength() {
return downLength;
}
}
11.DBOpenHelper
package com.changcheng.download.service;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class DBOpenHelper extends SQLiteOpenHelper {
private static final String DBNAME = "download.db";
private static final int VERSION = 2;
public DBOpenHelper(Context context) {
super(context, DBNAME, null, VERSION);
}
[email protected]
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE IF NOT EXISTS filedown (id integer primary key autoincrement, downpath varchar(100), threadid INTEGER, position INTEGER)");
}
[email protected]
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS filedown");
onCreate(db);
}
}