您的位置:首页 > 移动开发 > Android开发

android程序---->android多线程下载(一)

2016-04-10 11:03 453 查看

http://www.cnblogs.com/huhx/p/continueDownload.html

  多线程下载是加快下载速度的一种方式,通过开启多个线程去执行一个任务,可以使任务的执行速度变快。多线程的任务下载时常都会使用得到断点续传下载,就是我们在一次下载未结束时退出下载,第二次下载时会接着第一次下载的进度继续下载。对于android中的下载,我想分多个部分去讲解分析。今天,我们就首先开始android中下载断点续传代码的实现。源码下载:java多线程断点续传(一) 。关于多线程下载单个文件的实现,请参见博客(后续补上):android程序---->android多线程下载(二)

目录导航

android中断点续传的思路
android断点续传基本的UI
android断点续传的工具类
下载暂停取消的具体流程
友情链接


android中断点续传的思路

一、 断点续传的实现步骤:
第一步: 我们要获得下载资源的的长度,用http请求中HttpURLConnection的getContentLength()方法
第二步:在本地创建一个文件,设计其长度。File file = new File()
第三步:从数据库中获得上次下载的进度,当暂停下载时,存储下载的状态,用到数据库的知识
第四步:从上次下载的位置下载数据,同时保存进度到数据库:RandomAccessFile的seek方法与HttpURLConnection的setRequestProperty方法
第五步:将下载进度回传到Activity,可以通过Intent将数据广播到Activity中
第六步:下载完成后删除下载信息,在数据库中删除相应的信息

二、 断点续传实现的流程图:


android断点续传基本的UI编写

明白了上述的实现流程,现在我们开始一个android项目,开始断点续传代码的编写,项目结构如下:

运行的截图如下:

一、 编写基本的UI,三个TextView,分别显示文件名、下载进度和下载速度,一个ProgressBar。二个Button,分别用于开始下载、暂停下载和取消下载。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.example.linux.continuedownload.MainActivity">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

<TextView
android:layout_marginLeft="80dp"
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

<TextView
android:layout_marginLeft="80dp"
android:id="@+id/speed"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<ProgressBar
android:visibility="invisible"
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<Button
android:id="@+id/start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始下载" />

<Button
android:layout_marginLeft="20dp"
android:id="@+id/stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="暂停下载" />

<Button
android:layout_marginLeft="20dp"
android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="取消下载" />
</LinearLayout>

</LinearLayout>


二、 在MainActivity中初始化一些组件,绑定按钮的事件:
在onCreate方法中初始化一些组件:

// 初始化组件
textView = (TextView) findViewById(R.id.textView);
progressView = (TextView) findViewById(R.id.progress);
speedView = (TextView) findViewById(R.id.speed);
progressBar = (ProgressBar) findViewById(R.id.progressBar);
progressBar.setMax(100);
startButton = (Button) findViewById(R.id.start);
stopButton = (Button) findViewById(R.id.stop);
cancelButton = (Button) findViewById(R.id.cancel);
// 创建一个文件信息对象
final FileInfo fileInfo = new FileInfo(0, fileUrl, "huhx.apk", 0, 0);


在onCreate方法中绑定开始下载按钮事件:
点击start按钮,设置进度条可见,并且设置start的Action,启动服务。

startButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
textView.setText(fileInfo.getFileName());
progressBar.setVisibility(View.VISIBLE);
// 通过Intent传递参数给service
Intent intent = new Intent(MainActivity.this, DownloadService.class);
intent.setAction(DownloadService.ACTION_START);
intent.putExtra("fileInfo", fileInfo);
startService(intent);
}
});


在onCreate方法中绑定暂停下载按钮事件:
点击stop按钮,设置stop的Action,启动服务。

stopButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 通过Intent传递参数给service
Intent intent = new Intent(MainActivity.this, DownloadService.class);
intent.setAction(DownloadService.ACTION_STOP);
intent.putExtra("fileInfo", fileInfo);
startService(intent);
}
});


在onCreate方法中绑定取消下载按钮事件:
点击cancel按钮,设置cancel的Action,启动服务,之后更新UI。

cancelButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 通过Intent传递参数给service
Intent intent = new Intent(MainActivity.this, DownloadService.class);
intent.setAction(DownloadService.ACTION_CANCEL);
intent.putExtra("fileInfo", fileInfo);
startService(intent);

// 更新textView和progressBar的显示UI
textView.setText("");
progressBar.setVisibility(View.INVISIBLE);
progressView.setText("");
speedView.setText("");
}
});


注册广播,用于Service向Activity传递一些下载进度信息:

// 静态注册广播
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(DownloadService.ACTION_UPDATE);
registerReceiver(broadcastReceiver, intentFilter);

/**
* 更新UI
*/
BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (DownloadService.ACTION_UPDATE.equals(intent.getAction())) {
int finished = intent.getIntExtra("finished", 0);
int speed = intent.getIntExtra("speed", 0);

Log.i("Main", finished + "");
progressBar.setProgress(finished);
progressView.setText(finished + "%");
speedView.setText(speed + "KB/s");
}
}
};


三、 在AndroidManifest.xm文件中声明权限,定义服务

<service android:name="com.huhx.services.DownloadService" android:exported="true" />

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />



android断点续传的工具类

二、 我们定义一些实体类,用于断点续传过程的信息的良好封装:
下载文件信息: 省略了get和set方法,以及toString和构造方法

public class FileInfo implements Serializable{
// 文件Id,用于标识文件
private int fileId;
// 文件的下载地址
private String url;
// 文件的名称
private String fileName;
// 文件的长度,也就是大小
private int length;
// 文件已经的下载量
private int finished;

}


下载资源的线程信息:省略同上

public class ThreadInfo {
// 线程ID
private int threadId;
// 下载资源的地址
private String url;
//下载资源的开始处
private int start;
//下载资源的结束处
private int end;
//资源已经的下载量
private int finished;
}


三、 我们开始数据库方面的编写,它用于存储更新线程的下载的进度信息
首先我们要创建一个数据库的工具类:

package com.huhx.util;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

/**
* Created by huhx on 2016/4/9.
*/
public class SqliteDBHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "download.db";
private static final int version = 1;

private static final String CREATE_THREADINFO = "create table thread_info(_id integer primary key autoincrement, " +
"thread_id integer, url text, start integer, end integer, finished integer)";
private static final String DROP_THREADINFO = "drop table if exists thread_info";

public SqliteDBHelper(Context context) {
super(context, DB_NAME, null, version);
}

@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_THREADINFO);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(DROP_THREADINFO);
db.execSQL(CREATE_THREADINFO);
}
}


定义一个Dao接口,用于数据库对线程信息的CRUD操作:

/**
* Created by Linux on 2016/4/9.
*/
public interface ThreadDao {
// 插入线程信息
public void insertThread(ThreadInfo threadInfo);

// 删除线程信息
public void deleteThread(String url, int threadId);

// 删除所有关于这个url的线程
public void deleteThread(String url);

// 更新线程信息
public void updateThread(String url, int threadId, int finished);

// 查询线程信息
public List<ThreadInfo> queryThread(String url);

// 线程信息是否存在
public boolean isThreadInfoExist(String url, int threadId);
}


具体实现上述Dao的Impl类:

package com.huhx.util;

import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import com.huhx.model.ThreadInfo;

import java.util.ArrayList;
import java.util.List;

/**
* Created by huhx on 2016/4/9.
*/
public class ThreadDaoImpl implements ThreadDao {

private SqliteDBHelper sqliteDBHelper;

public ThreadDaoImpl(Context context) {
sqliteDBHelper = new SqliteDBHelper(context);
}

@Override
public void insertThread(ThreadInfo threadInfo) {
SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();
Object[] objects = new Object[]{
threadInfo.getThreadId(), threadInfo.getUrl(), threadInfo.getStart(), threadInfo.getEnd(), threadInfo.getFinished()
};
database.execSQL("insert into thread_info(thread_id, url, start, end, finished) values(?,?,?,?,?)", objects);
database.close();

}

@Override
public void deleteThread(String url, int threadId) {
SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();
Object[] objects = new Object[]{
url, threadId
};
database.execSQL("delete from thread_info where url = ? and thread_id = ?", objects);
database.close();
}

@Override
public void deleteThread(String url) {
SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();
Object[] objects = new Object[]{
url
};
database.execSQL("delete from thread_info where url = ?", objects);
database.close();
}

@Override
public void updateThread(String url, int threadId, int finished) {
SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();
Object[] objects = new Object[]{
finished, url, threadId
};
database.execSQL("update thread_info set finished = ? where url = ? and thread_id = ?", objects);
database.close();
}

@Override
public List<ThreadInfo> queryThread(String url) {
SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();
List<ThreadInfo> threadInfos = new ArrayList<>();
Cursor cursor = database.rawQuery("select * from thread_info where url = ?", new String[]{url});
while (cursor.moveToNext()) {
ThreadInfo threadInfo = new ThreadInfo();
threadInfo.setThreadId(cursor.getInt(cursor.getColumnIndex("thread_id")));
threadInfo.setUrl(cursor.getString(cursor.getColumnIndex("url")));
threadInfo.setStart(cursor.getInt(cursor.getColumnIndex("start")));
threadInfo.setEnd(cursor.getInt(cursor.getColumnIndex("end")));
threadInfo.setFinished(cursor.getInt(cursor.getColumnIndex("finished")));

threadInfos.add(threadInfo);
}
cursor.close();
database.close();
return threadInfos;
}

@Override
public boolean isThreadInfoExist(String url, int threadId) {
SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();
Cursor cursor = database.rawQuery("select * from thread_info where url = ? and thread_id = ?", new String[]{url, threadId+""});

boolean isExist = cursor.moveToNext();
cursor.close();
database.close();
return isExist;
}
}



下载暂停取消的具体流程

四、 最后我们开始最重要的Service以及核心的下载代码的编写,我们按照上述的开始、暂停、取消的顺序,来讲解断点续传的实现过程。
我们在DownloadService中onStartCommand方法中接收的Intent,关于Service的使用请参见:android基础---->service的生命周期

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 获得Activity传过来的参数
if (ACTION_START.equals(intent.getAction())) {
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
// 启动初始化线程
new InitThread(fileInfo).start();
} else if (ACTION_STOP.equals(intent.getAction())) {
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
if (downloadTask != null) {
downloadTask.isPause = true;
}
} else if (ACTION_CANCEL.equals(intent.getAction())) {
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
if (downloadTask != null) {
downloadTask.isPause = true;
}
// 删除本地文件
File file = new File(DOWNLOAD_PATH, fileInfo.getFileName());
if (file.exists()) {
file.delete();
}
handler.obtainMessage(DOWNLOAD_CANCEL, fileInfo).sendToTarget();
}
return super.onStartCommand(intent, flags, startId);
}


五、 文件的开始下载流程:
开始下载时,启动一个初始化线程,并把文件信息传递给线程,该线程通过Http请求得到文件的长度,在本地创建下载文件的载体,设置大小并发送下载的消息给Handler:

/**
* 初始化子线程
*/
class InitThread extends Thread {
private FileInfo fileInfo = null;

public InitThread(FileInfo fileInfo) {
this.fileInfo = fileInfo;
}

@Override
public void run() {
// 连接网络文件
HttpURLConnection connection = null;
RandomAccessFile randomAccessFile = null;
try {
URL url = new URL(fileInfo.getUrl());
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(3000);
connection.setRequestMethod("GET");

connection.connect();

int length = -1;
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
// 获取文件的长度
length = connection.getContentLength();
}
if (length <= 0) {
return;
}
// 在本地创建文件
File dir = new File(DOWNLOAD_PATH);
if (dir.exists()) {
dir.mkdir();
}
File file = new File(dir, fileInfo.getFileName());
// 设置文件长度
randomAccessFile = new RandomAccessFile(file, "rwd");
randomAccessFile.setLength(length);

fileInfo.setLength(length);
handler.obtainMessage(DOWNLOAD_MESSAGE, fileInfo).sendToTarget();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
randomAccessFile.close();
connection.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}


handler接收消息,并加以处理:注意这里有两种消息,我们暂时只考虑DOWNLOAD_MESSAGE消息,它启动下载任务

private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DOWNLOAD_MESSAGE:
FileInfo fileInfo = (FileInfo) msg.obj;
// 启动下载任务
downloadTask = new DownloadTask(DownloadService.this, fileInfo);
downloadTask.download();
break;
case DOWNLOAD_CANCEL:
FileInfo fileCancelInfo = (FileInfo) msg.obj;
downloadTask = new DownloadTask(DownloadService.this);
downloadTask.cancelDownload(fileCancelInfo);
break;
}
}
};


在download方法中,首先判断是否有线程下载过文件,如果没有就创建一个。有的话,从数据库直接得到。而且开启了下载的任务线程

public void download() {
// 读取数据库的线程信息
List<ThreadInfo> threadInfos = threadDao.queryThread(fileInfo.getUrl());
ThreadInfo threadInfo = null;
if (threadInfos.size() == 0) {
threadInfo = new ThreadInfo(0, fileInfo.getUrl(), 0, fileInfo.getLength(), 0);
} else {
threadInfo = threadInfos.get(0);
}
new DownloadThread(threadInfo).start();
}


在下载的线程中,通过Http请求数据并通过字节流的方式存储在本地的文件中。间隔500毫秒,就发送一次更新UI的广播。如果收到了暂停的信号,就暂停下载。在下载完成之后,删除数据库中的线程信息

class DownloadThread extends Thread {
private ThreadInfo threadInfo = null;

public DownloadThread(ThreadInfo threadInfo) {
this.threadInfo = threadInfo;
}

@Override
public void run() {
// 向数据库插入线程信息
if (!threadDao.isThreadInfoExist(threadInfo.getUrl(), threadInfo.getThreadId())) {
threadDao.insertThread(threadInfo);
}
HttpURLConnection connection = null;
RandomAccessFile randomAccessFile = null;
InputStream inputStream = null;
try {
URL url = new URL(threadInfo.getUrl());
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(5000);
connection.setRequestMethod("GET");

int start = threadInfo.getStart() + threadInfo.getFinished();
connection.setRequestProperty("Range", "bytes=" + start + "-" + threadInfo.getEnd());

File file = new File(DownloadService.DOWNLOAD_PATH, fileInfo.getFileName());
randomAccessFile = new RandomAccessFile(file, "rwd");
randomAccessFile.seek(start);
Intent intent = new Intent(DownloadService.ACTION_UPDATE);

// 开始下载
finished += threadInfo.getFinished();
if (connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
inputStream = connection.getInputStream();
byte[] buffer = new byte[4 * 1024];
int len = -1;
long time = System.currentTimeMillis();
long time1;
while ((len = inputStream.read(buffer)) != -1) {
randomAccessFile.write(buffer, 0, len);
finished += len;
if ((time1 = System.currentTimeMillis() - time) > 500) {
time = System.currentTimeMillis();
intent.putExtra("finished", finished * 100 / fileInfo.getLength());
intent.putExtra("speed", (int) (len / time1));
context.sendBroadcast(intent);
}
if (isPause) {
threadDao.updateThread(threadInfo.getUrl(), threadInfo.getThreadId(), finished);
return;
}
}
// 删除线程信息,再次发送广播避免上面的广播延迟
intent.putExtra("finished", finished * 100 / fileInfo.getLength());
context.sendBroadcast(intent);
threadDao.deleteThread(threadInfo.getUrl(), threadInfo.getThreadId());
Log.i("Main", "finished: " + finished + ", and file length: " + fileInfo.getLength());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
connection.disconnect();
randomAccessFile.close();
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}


六、 文件的暂停下载流程:如果下载任务在启动,那么设置isPause为true,在上述的讲解中我们知道,此时字节流停止的传输。

if (downloadTask != null) {
downloadTask.isPause = true;
}


七、 文件的取消下载流程:
暂停下载的流程,然后删除本地文件,最后发送取消下载的消息:

FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
if (downloadTask != null) { downloadTask.isPause = true; }
// 删除本地文件
File file = new File(DOWNLOAD_PATH, fileInfo.getFileName());
if (file.exists()) {
file.delete();
}
handler.obtainMessage(DOWNLOAD_CANCEL, fileInfo).sendToTarget();


handler处理取消下载的消息:调用DownloadTask的cancelDownload方法,并把文件信息传入

case DOWNLOAD_CANCEL:
FileInfo fileCancelInfo = (FileInfo) msg.obj;
downloadTask = new DownloadTask(DownloadService.this);
downloadTask.cancelDownload(fileCancelInfo);
break;


在cancelDownload方法中删除数据库中的线程信息:

// 取消下载任务
public void cancelDownload(FileInfo fileInfo) {
threadDao.deleteThread(fileInfo.getUrl());
}


最后在MainActivity中更新UI:

// 更新textView和progressBar的显示UI
textView.setText("");
progressBar.setVisibility(View.INVISIBLE);
progressView.setText("");
speedView.setText("");



友情链接

handler的原理 android高级---->Handler的原理
service的生命周期 android基础---->service的生命周期
android中广播的使用 android基础---->Broadcast的使用
android中数据库的使用 android基础---->SQLite数据库的使用
测试的断点续传源码下载

关于android中多线程的下载,请参见我的博客(后续补上): android程序---->android多线程下载(二)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: