Android多文件断点续传(三)——实现文件断点续传
16lz
2021-01-23
上一篇中我们主要介绍了如何实现数据库储存下载信息,如果你还没阅读过,建议先阅读上一篇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应用程序中Manifest.java文件的介绍
- Android获取本机IP地址(不是localhost)和MAC的方法
- Android 导入android源码有错,R.java文件不能自动生成解决方法
- Git,SVN使用方法杂记(更新中)
- 解决develop.android.com无法访问到最佳方法
- Android深入浅出系列课程---Lesson12 AFF110525_Android多线程系