上一篇中我们主要介绍了如何实现数据库储存下载信息,如果你还没阅读过,建议先阅读上一篇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中接收和处理。

到这里整个流程就已经实现了,其实只有自己动手敲一遍,才能理解得深透,记得牢固。

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

下载源码

更多相关文章

  1. Android应用程序中Manifest.java文件的介绍
  2. Android获取本机IP地址(不是localhost)和MAC的方法
  3. Android 导入android源码有错,R.java文件不能自动生成解决方法
  4. Git,SVN使用方法杂记(更新中)
  5. 解决develop.android.com无法访问到最佳方法
  6. Android深入浅出系列课程---Lesson12 AFF110525_Android多线程系

随机推荐

  1. 从屏幕底部弹出PopupWindow
  2. Android自定义弹窗效果
  3. Android(安卓)Design Support Library之T
  4. Flutter Android(安卓)Studio打包安卓apk
  5. android 2.1 监听电话状态并自动接听来电
  6. Win10下编译Android(安卓)Ogre3d 1.12.6
  7. Animation Android(安卓)( 一)
  8. Android的事件处理之通过回调实现跟随手
  9. Windows 7安装Android(安卓)Studio
  10. Android(安卓)Geofence的学习(二)继续翻译