源码下载:http://vdisk.weibo.com/s/1dfmi
我们编写的是Andorid的HTTP多线程断点下载应用程序。因为之间我们学习的学习积累,直接使用单线程下载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
<?xml version="1.0"encoding="utf-8"?>
<manifestxmlns:android="http://schemas.android.com/apk/res/android"
package="com.changcheng.download"
android:versionCode="1"
android:versionName="1.0">
<applicationandroid:icon="@drawable/icon"android:label="@string/app_name">
<activityandroid:name=".MulThreadDownloader"
android:label="@string/app_name">
<intent-filter>
<actionandroid:name="android.intent.action.MAIN"/>
<categoryandroid:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
<uses-sdk android:minSdkVersion="7"/>
<!-- 在SDCard中创建与删除文件权限 -->
<uses-permissionandroid:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<!-- 往SDCard写入数据权限 -->
<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 访问internet权限-->
<uses-permissionandroid:name="android.permission.INTERNET"/>
</manifest>
3.strings.xml
<?xml version="1.0"encoding="utf-8"?>
<resources>
<stringname="hello">Hello World,DownloadActivity!</string>
<stringname="app_name">多线程断点下载</string>
<stringname="path">下载路径</string>
<stringname="downloadbutton">下载</string>
<stringname="sdcarderror">SDCard不存在或者写保护</string>
</resources>
4.main.xml
<?xml version="1.0"encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<!-- 下载路径 -->
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/path"
/>
<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="http://www.winrar.com.cn/download/wrar380sc.exe"
android:id="@+id/path"
/>
<!-- 下载按钮 -->
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/downloadbutton"
android:id="@+id/button"
/>
<!-- 进度条 -->
<ProgressBar
android:layout_width="fill_parent"
android:layout_height="20dip"
style="?android:attr/progressBarStyleHorizont
android:id="@+id/downloadbar"/>
<!-- 进度% -->
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:id="@+id/resultView"
/>
</LinearLayout>
5.MulThreadDownloader
package com.changcheng.download;
import java.io.File;
importcom.changcheng.net.download.DownloadProgressListener
importcom.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 classMulThreadDownloader extends Activity {
private EditText pathText;
private ProgressBar progressBar;
private TextView resultView;
private Handler handler = newHandler(){
@Override
public voidhandleMessage(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);
}
};
@Override
public void onCreate(BundlesavedInstanceState) {
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(newView.OnClickListener() {
@Override
public void onClick(View v){
String path = pathText.getText().toString();
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
//下载文件需要很长的时间,主线程是不能够长时间被阻塞,如果主线程被长时间阻塞, 那么Android被回收应用
download(path,Environment.getExternalStorageDirect
}else{
Toast.makeText(MulThreadDownloader.this,R.string.sdcarderror, 1).show();
}
}
});
}
//对于Android的UI控件,只能由主线程负责显示界面的更新,其他线程不能直接更新UI控件的显示
public voiddownload(final String path, finalFile saveDir){
new Thread(new Runnable(){
@Override
public void run() {
FileDownloader downer = newFileDownloader(MulThreadDownloader.this, path,saveDir, 3);
progressBar.setMax(downer.getFileSize());//设置进度条的最大刻度
try {
downer.download(newDownloadProgressListener
@Override
public voidonDownloadSize(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;
importjava.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
importcom.changcheng.download.service.FileService;
import android.content.Context;
import android.util.Log;
public class FileDownloader{
private Context context;
private FileService fileService;
private staticfinal 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<Integer,Integer> data = newConcurrentHashMap<Integer,Integer>();
private int block;
private String downloadUrl;//下载路径
public int getThreadSize(){
return threads.length;
}
public int getFileSize() {
return fileSize;
}
protected synchronizedvoid append(int size) {
downloadSize += size;
}
protected voidupdate(int threadId, int pos){
this.data.put(threadId, pos);
}
protected synchronizedvoid saveLogFile() {
this.fileService.update(this.downloadUrl,this.data);
}
public FileDownloader(Context context, StringdownloadUrl, File fileSaveDir,int threadNum){
try {
this.context = context;
this.downloadUrl = downloadUrl;
fileService = new FileService(context);
this.url = newURL(downloadUrl);
if(!fileSaveDir.exists())fileSaveDir.mkdirs();
this.threads = newDownloadThread[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, *
Map<Integer, Integer> 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< this.threads.length; i++) {
this.downloadSize +=this.data.get(i+1)-(this.block *i);
}
print("已经下载的长度"+this.downloadSize);
}
}else{
throw newRuntimeException("服务器响应错误 ");
}
} catch (Exception e) {
print(e.toString());
throw newRuntimeException("连接不到下载路径 ");
}
}
private String getFileName(HttpURLConnectionconn) {
String filename =this.url.toString().substring(this.url.toString().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 m =Pattern.compile(".*filename=(.*)").matcher(mine.toLowerCase());
if(m.find()) returnm.group(1);
}
}
filename = UUID.randomUUID()+ ".tmp";//默认取一个文件名
}
return filename;
}
public intdownload(DownloadProgressListener
try {
if(this.data.size() !=this.threads.length){
this.data.clear();
for (int i = 0; i< this.threads.length; i++) {
this.data.put(i+1, this.block* i);
}
}
for (int i = 0; i< this.threads.length; i++) {
int downLength =this.data.get(i+1) - (this.block* i);
if(downLength <this.block &&this.data.get(i+1)<this.fileSize){//该线程未完成下载时,继续下载
RandomAccessFile randOut = newRandomAccessFile(this.saveFile, "rw");
if(this.fileSize>0)randOut.setLength(this.fileSize);
randOut.seek(this.data.get(i+1));
this.threads[i] = newDownloadThread(this,this.url,randOut, this.block,this.data.get(i+1), i+1);
this.threads[i].setPriority(7);
this.threads[i].start();
}else{
this.threads[i] = null;
}
}
this.fileService.save(this.downloadUrl,this.data);
boolean notFinish =true;//下载未完成
while (notFinish) {// 循环判断是否下载完毕
Thread.sleep(900);
notFinish = false;//假定下载完成
for (int i = 0; i< this.threads.length; i++){
if (this.threads[i] !=null &&!this.threads[i].isFinish()) {
notFinish = true;//下载没有完成
if(this.threads[i].getDownLength()== -1){//如果下载失败,再重新下载
RandomAccessFile randOut = newRandomAccessFile(this.saveFile, "rw");
randOut.seek(this.data.get(i+1));
this.threads[i] = newDownloadThread(this,this.url,randOut, this.block,this.data.get(i+1), i+1);
this.threads[i].setPriority(7);
this.threads[i].start();
}
}
}
if(listener!=null)listener.onDownloadSize(this.downloadSize);
}
fileService.delete(this.downloadUrl);
} catch (Exception e) {
print(e.toString());
throw newException("下载失败");
}
return this.downloadSize;
}
public staticMap<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;
}
public staticvoid printResponseHeader(HttpURLConnectionhttp){
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());
}
}
private staticvoid print(String msg){
Log.i(TAG, msg);
}
}
7.DownloadProgressListener
package com.changcheng.net.download;
public interfaceDownloadProgressListener
public voidonDownloadSize(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;
importandroid.database.sqlite.SQLiteDatabase;
public class FileService {
private DBOpenHelper openHelper;
public FileService(Context context) {
openHelper = new DBOpenHelper(context);
}
public Map<Integer,Integer> getData(String path){
SQLiteDatabase db = openHelper.getReadableDatabase();
Cursor cursor = db.rawQuery("select threadid, position fromfiledown where downpath=?",newString[]{path});
Map<Integer, Integer> data =new HashMap<Integer,Integer>();
while(cursor.moveToNext()){
data.put(cursor.getInt(0), cursor.getInt(1));
}
cursor.close();
db.close();
return data;
}
public void save(String path,Map<Integer, Integer>map){//intthreadid,int position
SQLiteDatabase db = openHelper.getWritableDatabase();
db.beginTransaction();
try{
for(Map.Entry<Integer,Integer> 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();
}
public void update(Stringpath, Map<Integer, Integer> map){
SQLiteDatabase db = openHelper.getWritableDatabase();
db.beginTransaction();
try{
for(Map.Entry<Integer,Integer> entry : map.entrySet()){
db.execSQL("update filedown set position=? where downpath=? andthreadid=?",
new Object[]{entry.getValue(), path,entry.getKey()});
}
db.setTransactionSuccessful
}finally{
db.endTransaction();
}
db.close();
}
public void delete(Stringpath){
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 DownloadThreadextends Thread {
private staticfinal 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(FileDownloaderdownloader, 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));
}
@Override
public void run() {
if(downLength <block){//未下载完成
try {
HttpURLConnection http = (HttpURLConnection)downUrl.openConnection();
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, *
public boolean isFinish(){
return finish;
}
public long getDownLength(){
return downLength;
}
}
11.DBOpenHelper
package com.changcheng.download.service;
import android.content.Context;
importandroid.database.sqlite.SQLiteDatabase;
importandroid.database.sqlite.SQLiteOpenHelper;
public class DBOpenHelperextends SQLiteOpenHelper {
private staticfinal String DBNAME = "download.db";
private staticfinal int VERSION =2;
public DBOpenHelper(Context context) {
super(context, DBNAME,null, VERSION);
}
@Override
public voidonCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE IF NOT EXISTS filedown (id integerprimary key autoincrement, downpath varchar(100), threadid INTEGER,position INTEGER)");
}
@Override
public voidonUpgrade(SQLiteDatabase db, int oldVersion,int newVersion) {
db.execSQL("DROP TABLE IF EXISTS filedown");
onCreate(db);
}
}