Android多文件断点续传(三)——实现文件断点续传

版权声明:本文为博主原创文章,未经博主允许不得转载。 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多文件断点续传(四)——处理网络状态变化

————————————————————————————————————

下载源码

猜你喜欢

转载自blog.csdn.net/a1533588867/article/details/53131325