版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a1533588867/article/details/53131325
上一篇中我们主要介绍了如何实现数据库储存下载信息,如果你还没阅读过,建议先阅读上一篇Android多文件断点续传(二)——实现数据库储存下载信息。数据库我们已经准备好,现在就可以开始来实现DownloadService进行断点续传了。
一.DownloadService
/**
* Created by kun on 2016/11/10.
* 下载服务
*/
public class DownloadService extends Service{
public static final String ACTION_START = "ACTION_START";
public static final String ACTION_PAUSE = "ACTION_PAUSE";
/**
* 下载任务集合
*/
private List<DownloadTask> downloadTasks = new ArrayList<>();
public static ExecutorService executorService = Executors.newCachedThreadPool();
@Override
public void onCreate() {
super.onCreate();
EventBus.getDefault().register(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if(intent.getAction().equals(ACTION_START)){
FileBean fileBean = (FileBean) intent.getSerializableExtra("FileBean");
for(DownloadTask downloadTask:downloadTasks){
if(downloadTask.getFileBean().getId() ==fileBean.getId()){
//如果下载任务中以后该文件的下载任务 则直接返回
return super.onStartCommand(intent, flags, startId);
}
}
executorService.execute(new InitThread(fileBean));
}else if(intent.getAction().equals(ACTION_PAUSE)){
FileBean fileBean = (FileBean) intent.getSerializableExtra("FileBean");
DownloadTask pauseTask = null;
for(DownloadTask downloadTask:downloadTasks){
if(downloadTask.getFileBean().getId() ==fileBean.getId()){
downloadTask.pauseDownload();
pauseTask = downloadTask;
break;
}
}
//将下载任务移除
downloadTasks.remove(pauseTask);
}
return super.onStartCommand(intent, flags, startId);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void getEventMessage(EventMessage eventMessage) {
switch (eventMessage.getType()){
case 1://下载线程初始化完毕
FileBean fileBean = (FileBean) eventMessage.getObject();
//开始下载
DownloadTask downloadTask = new DownloadTask(this,fileBean,3);
downloadTasks.add(downloadTask);
break;
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
EventBus.getDefault().unregister(this);
}
}
在AndroidManifest中注册
<service android:name=".services.DownloadService"/>
在DownloadService中的onStartCommand方法中我们获取到列表中开始和暂停按钮传递过来的数据,我们先来看开始下载的逻辑。
if(intent.getAction().equals(ACTION_START)){
FileBean fileBean = (FileBean) intent.getSerializableExtra("FileBean");
for(DownloadTask downloadTask:downloadTasks){
if(downloadTask.getFileBean().getId() ==fileBean.getId()){
//如果下载任务中以后该文件的下载任务 则直接返回
return super.onStartCommand(intent, flags, startId);
}
}
executorService.execute(new InitThread(fileBean));
}
为了防止多次点击开始按钮造成多次创建下载任务,这里对当前的下载文件进行了判断,已经开始下载的了会保存在下载任务列表中downloadTasks,这个后面会说到,如果第一次下载则用FileBean创建一个初始线程InitThread,并将该线程交给线程池executorService管理。
public static ExecutorService executorService = Executors.newCachedThreadPool();
在这里我们采用的线程池是java提供的四中线程池中的缓存线程池,特点是如果现有线程没有可用的,则创建一个新线程并添加到池中,如果有线程可用,则复用现有的线程。如果60 秒钟未被使用的线程则会被回收。因此,长时间保持空闲的线程池不会使用任何内存资源。具体的知识大家可以查阅相关资料。
接着我们看一下InitThread具体做了什么。
二.InitThread
/**
* Created by 坤 on 2016/11/10.
* 初始化线程
*/
public class InitThread extends Thread{
private FileBean fileBean;
public InitThread(FileBean fileBean) {
this.fileBean = fileBean;
}
@Override
public void run() {
HttpURLConnection connection =null;
RandomAccessFile randomAccessFile = null;
try {
URL url = new URL(fileBean.getUrl());
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(10000);
connection.setRequestMethod("GET");
int fileLength = -1;
if(connection.getResponseCode() == HttpURLConnection.HTTP_OK){
fileLength = connection.getContentLength();
}
if(fileLength<=0) return;
File dir = new File(Config.downLoadPath);
if(!dir.exists()){
dir.mkdir();
}
File file = new File(dir,fileBean.getFileName());
randomAccessFile = new RandomAccessFile(file,"rwd");
randomAccessFile.setLength(fileLength);
fileBean.setLength(fileLength);
EventMessage eventMessage = new EventMessage(1,fileBean);
EventBus.getDefault().post(eventMessage);
}catch (Exception e){
e.printStackTrace();
}
}
}
在InitThread的run方法中主要是获取文件的长度,通过FileBean的url得到HttpURLConnection,在通过 HttpURLConnection的getContentLength()获取到文件的长度。
这里我们用到了一个关键的类——RandomAccessFile,这个类可以帮助我们在文件的任何位置读取、写入或者修改数据,构造方法中需要传入一个File,以及一段字符,这里File传入了我们所要保存下载的文件,而“rwd”则代表了reading、writing、deleting,表示可以对文件进行读写和修改的操作。这个在后面的分段下载会再次用到。
获取到文件的长度后我们通过EventBus将数据发送出去。这里用到了EventMessage,我们在其构造方法中传入了1和FileBean,我们看一下里面的具体代码。
**
* Created by kun on 2016/11/10.
*/
public class EventMessage {
/**
* 1 获取下载文件的长度
* 2 下载完成
* 3 下载进度刷新
*/
private int type;
private Object object;
public EventMessage(int type, Object object) {
this.type = type;
this.object = object;
}
... ... //get set
}
代码很简单,封装了一个Integer和一个Object,其中type主要用于区分事件类型,而object主要用于传递数据。
接着我们在DownloadService中的getEventMessage()方法获取EventBus传递过来的数据。
@Subscribe(threadMode = ThreadMode.MAIN)
public void getEventMessage(EventMessage eventMessage) {
switch (eventMessage.getType()){
case 1://下载线程初始化完毕
FileBean fileBean = (FileBean) eventMessage.getObject();
//开始下载
DownloadTask downloadTask = new DownloadTask(this,fileBean,3);
downloadTasks.add(downloadTask);
break;
}
}
这里可以看到通过type判断事件类型,然后强制转换得到FileBean,接着创建了DownloadTask ,其中第三个参数主要设置该文件用多少个线程去下载,接着将下载任务添加到下载任务列表中,这样在点击开始下载的时候通过判断downloadTasks是否已存在DownloadTask,从而避免重复创建下载任务了。
三.DownloadTask
/**
* Created by kun on 2016/11/11.
* 下载任务
*/
public class DownloadTask implements DownloadCallBack {
private FileBean fileBean;
private ThreadDao dao;
/**
* 总下载完成进度
*/
private int finishedProgress = 0;
/**
* 下载线程信息集合
*/
private List<ThreadBean> threads;
/**
* 下载线程集合
*/
private List<DownloadThread> downloadThreads = new ArrayList<>();
public DownloadTask(Context context,FileBean fileBean, int downloadThreadCount) {
this.fileBean = fileBean;
dao = new ThreadDaoImpl(context);
//初始化下载线程
initDownThreads(downloadThreadCount);
}
private void initDownThreads(int downloadThreadCount) {
//查询数据库中的下载线程信息
threads = dao.getThreads(fileBean.getUrl());
if(threads.size()==0){//如果列表没有数据 则为第一次下载
//根据下载的线程总数平分各自下载的文件长度
int length = fileBean.getLength()/downloadThreadCount;
for(int i = 0; i<downloadThreadCount; i++){
ThreadBean thread = new ThreadBean(i,fileBean.getUrl(),i * length,
(i + 1) * length -1,0);
if(i == downloadThreadCount-1){
thread.setEnd(fileBean.getLength());
}
//将下载线程保存到数据库
dao.insertThread(thread);
threads.add(thread);
}
}
//创建下载线程开始下载
for(ThreadBean thread : threads){
finishedProgress+= thread.getFinished();
DownloadThread downloadThread = new DownloadThread(fileBean, thread, this);
DownloadService.executorService.execute(downloadThread);
downloadThreads.add(downloadThread);
}
}
/**
* 暂停下载
*/
public void pauseDownload(){
for(DownloadThread downloadThread : downloadThreads){
if (downloadThread!=null) {
downloadThread.setPause(true);
}
}
}
@Override
public void pauseCallBack(ThreadBean threadBean) {
dao.updateThread(threadBean.getUrl(),threadBean.getId(),threadBean.getFinished());
}
private long curTime = 0;
@Override
public void progressCallBack(int length) {
finishedProgress += length;
//每500毫秒发送刷新进度事件
if(System.currentTimeMillis() - curTime >500 || finishedProgress==fileBean.getLength()){
fileBean.setFinished(finishedProgress);
EventMessage message = new EventMessage(3,fileBean);
EventBus.getDefault().post(message);
curTime = System.currentTimeMillis();
}
}
@Override
public synchronized void threadDownLoadFinished(ThreadBean threadBean) {
for(ThreadBean bean:threads){
if(bean.getId() == threadBean.getId()){
//从列表中将已下载完成的线程信息移除
threads.remove(bean);
break;
}
}
if(threads.size()==0){//如果列表size为0 则所有线程已下载完成
//删除数据库中的信息
dao.deleteThread(fileBean.getUrl());
//发送下载完成事件
EventMessage message = new EventMessage(2,fileBean);
EventBus.getDefault().post(message);
}
}
public FileBean getFileBean() {
return fileBean;
}
}
在构造方法中我们可以看到调用了initDownThreads()方法
private void initDownThreads(int downloadThreadCount) {
//查询数据库中的下载线程信息
threads = dao.getThreads(fileBean.getUrl());
if(threads.size()==0){//如果列表没有数据 则为第一次下载
//根据下载的线程总数平分各自下载的文件长度
int length = fileBean.getLength()/downloadThreadCount;
for(int i = 0; i<downloadThreadCount; i++){
ThreadBean thread = new ThreadBean(i,fileBean.getUrl(),i * length,
(i + 1) * length -1,0);
if(i == downloadThreadCount-1){//最后一条线程的终止位置为文件长度
thread.setEnd(fileBean.getLength());
}
//将下载线程保存到数据库
dao.insertThread(thread);
threads.add(thread);
}
}
//创建下载线程开始下载
for(ThreadBean thread : threads){
finishedProgress+= thread.getFinished();
DownloadThread downloadThread = new DownloadThread(fileBean, thread, this);
DownloadService.executorService.execute(downloadThread);
downloadThreads.add(downloadThread);
}
}
首先通过文件下载的Url从数据库获取下载线程信息,如果获取到的线程信息列表Size为0,则该文件是第一次下载,那么就根据downloadThreadCount平分文件长度,然后创建downloadThreadCount 个 ThreadBean,每个ThreadBean中保存这下载的起始位置和终止位置。接着将ThreadBean保存到数据库中并且添加到线程信息列表中。
接着创建下载线程开始下载,这里定义了一个变量finishedProgress用于记录当前总下载长度,由于有可能之前下载到一半暂停了,数据库中保存着下载信息,因此在开始下载前需要加上之前已下载完成的长度。
可以看到通过下载线程信息ThreadBean创建对应的下载线程DownloadThread,然后将下载线程交给线程池管理。并且将下载线程放到列表downloadThreads中,方便后面对线程进行暂停操作。
在创建DownloadThread传入的第三个参数是一个接口——DownloadCallBack,用于监听下载进度。DownloadTask已经实现了该接口,于是直接传this.
四.DownloadCallBack
/**
* Created by kun on 2016/11/11.
* 下载进度回调
*/
public interface DownloadCallBack {
/**
* 暂停回调
* @param threadBean
*/
void pauseCallBack(ThreadBean threadBean);
/**
* 下载进度
* @param length
*/
void progressCallBack(int length);
/**
* 线程下载完毕
* @param threadBean
*/
void threadDownLoadFinished(ThreadBean threadBean);
}
我们简单看看DownloadCallBack中的代码,主要有三个方法,分别为暂停回调,进度实时回调,以及下载完成回调。
五.DownloadThread
/**
* Created by kun on 2016/11/11.
* 下载线程
*/
public class DownloadThread extends Thread {
private FileBean fileBean;
private ThreadBean threadBean;
private DownloadCallBack callback;
private Boolean isPause = false;
public DownloadThread(FileBean fileBean,ThreadBean threadBean, DownloadCallBack callback) {
this.fileBean = fileBean;
this.threadBean = threadBean;
this.callback = callback;
}
public void setPause(Boolean pause) {
isPause = pause;
}
@Override
public void run() {
HttpURLConnection connection = null;
RandomAccessFile raf = null;
InputStream inputStream = null;
try {
URL url = new URL(threadBean.getUrl());
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(10000);
connection.setRequestMethod("GET");
//设置下载起始位置
int start = threadBean.getStart() + threadBean.getFinished();
connection.setRequestProperty("Range","bytes="+start+"-"+threadBean.getEnd());
//设置写入位置
File file = new File(Config.downLoadPath,fileBean.getFileName());
raf = new RandomAccessFile(file,"rwd");
raf.seek(start);
//开始下载
if(connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL){
inputStream = connection.getInputStream();
byte[] bytes = new byte[1024];
int len = -1;
while ((len = inputStream.read(bytes))!=-1){
raf.write(bytes,0,len);
//将加载的进度回调出去
callback.progressCallBack(len);
//保存进度
threadBean.setFinished(threadBean.getFinished()+len);
//在下载暂停的时候将下载进度保存到数据库
if(isPause){
callback.pauseCallBack(threadBean);
return;
}
}
//下载完成
callback.threadDownLoadFinished(threadBean);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
inputStream.close();
raf.close();
connection.disconnect();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
我们可以看到DownloadThread中的代码其实并不复杂,关键主要是设置的下载位置以及文件的写入位置
//设置下载起始位置
int start = threadBean.getStart() + threadBean.getFinished();
connection.setRequestProperty("Range","bytes="+start+"-"+threadBean.getEnd());
//设置写入位置
File file = new File(Config.downLoadPath,fileBean.getFileName());
raf = new RandomAccessFile(file,"rwd");
raf.seek(start);
起始位置很好理解,就是线程所分配到的起始位置再加上此线程之前已下载完成长度。这里需要用到HttpURLConnection中的setRequestProperty方法,这个方法可以帮助我们任意指定位置去获取下载数据,而不是从头到尾去获取。
需要注意的是调用setRequestProperty()方法后,ResponseCode就不再是HTTP_OK(200)了,而是HTTP_PARTIAL(206)。
接着写入位置还是利用RandomAccessFile的seek()方法帮助我们设置指定位置去写入数据到文件中。
设置完成后就可以进行写入操作了
if(connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL){
inputStream = connection.getInputStream();
byte[] bytes = new byte[1024];
int len = -1;
while ((len = inputStream.read(bytes))!=-1){
raf.write(bytes,0,len);
//将下载的进度回调出去
callback.progressCallBack(len);
//保存进度
threadBean.setFinished(threadBean.getFinished()+len);
//在下载暂停的时候将下载进度保存到数据库
if(isPause){
callback.pauseCallBack(threadBean);
return;
}
}
//下载完成
callback.threadDownLoadFinished(threadBean);
}
在下载工程中实时回调progressCallBack方法以及更新线程信息ThreadBean中的finished数据。
这里通过isPause的值来判断是否执行了暂停操作,如果执行了暂停操作,则将调用pauseCallBack方法,并将最新的线程信息传递过去。
当方法执行完毕,这回调threadDownLoadFinished方法,将最新的线程信息传递过去。
这里下载线程的逻辑就处理完毕了,我们需要回过头去看一下DownloadTask如何处理这些回调方法。
暂停回调
可以看到这里更新了一下数据库中下载线程的信息
@Override
public void pauseCallBack(ThreadBean threadBean) {
dao.updateThread(threadBean.getUrl(),threadBean.getId(),threadBean.getFinished());
}
下载进度回调
private long curTime = 0;
@Override
public void progressCallBack(int length) {
finishedProgress += length;
//每500毫秒发送刷新进度事件
if(System.currentTimeMillis() - curTime >500 || finishedProgress==fileBean.getLength()){
fileBean.setFinished(finishedProgress);
EventMessage message = new EventMessage(3,fileBean);
EventBus.getDefault().post(message);
curTime = System.currentTimeMillis();
}
}
方法中下载长度inishedProgress加上了线程下载的长度 ,然后每隔500毫秒或者在下载完成的时候更新FileBean的已下载的长度,最后通过EventBus将FileBean发送出去。然后在MianActivity中对事件进行接收,接收到进度刷新事件后就调用adaper的updateProgress刷新页面。
@Subscribe(threadMode = ThreadMode.MAIN)
public void getEventMessage(EventMessage eventMessage) {
switch (eventMessage.getType()) {
case 2://下载完成
FileBean fileBean1 = (FileBean) eventMessage.getObject();
Toast.makeText(this,fileBean1.getFileName()+"已下载完成",Toast.LENGTH_SHORT).show();
break;
case 3://下载进度刷新
FileBean fileBean2 = (FileBean) eventMessage.getObject();
adaper.updateProgress(fileBean2);
break;
}
}
下载完成回调
@Override
public synchronized void threadDownLoadFinished(ThreadBean threadBean) {
for(ThreadBean bean:threads){
if(bean.getId() == threadBean.getId()){
//从列表中将已下载完成的线程信息移除
threads.remove(bean);
break;
}
}
if(threads.size()==0){//如果列表size为0 则所有线程已下载完成
//删除数据库中的信息
dao.deleteThread(fileBean.getUrl());
//发送下载完成事件
EventMessage message = new EventMessage(2,fileBean);
EventBus.getDefault().post(message);
}
}
之前我们在创建下载线程的时候将对应的线程信息加入到threads列表中,现在通过下载完成回调回来的线程对应的线程信息获取到threads中对应的线程信息,然后将其从threads中移除。最后判断threads中的内容是否都移除完毕,如果都移完毕,则删除数据库中的信息,然后再通过EventBus发送下载完成的事件出去。最后在MainActiviy中接收和处理。
到这里整个流程就已经实现了,其实只有自己动手敲一遍,才能理解得深透,记得牢固。
后续增加对网络状态变化的处理,有兴趣可以接着阅读:
Android多文件断点续传(四)——处理网络状态变化
————————————————————————————————————