Android 实现简易下载管理器 (暂停、断点续传、多线程下载)

什么都先别说,先看预览图!

Android 实现简易下载管理器 (暂停、断点续传、多线程下载)_第1张图片

预览图中是限制了同时最大下载数为 2 的.

其实下载管理器的实现是挺简单的,我们需要弄清楚几点就行了

1.所有任务的Bean应该存在哪里,用什么存?
2.如何判断任务是否已存在?
3.如何判断任务是新的任务或是从等待中恢复的任务?
4.应该如何把下载列表传递给Adapter?
5.如何将下载的进度传递出去?
6.如何有效率地刷新显示的列表? (ListView 或 RecycleView)

服务基础

首先我们需要明确一点,下载我们应该使用服务来进行,这样我们才能进行后台下载。
所以我们就开始创建我们的Service:

public class OCDownloadService extends Service{

    ... ...

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        //当服务被Bind的时候我们就返回一个带有服务对象的类给Bind服务的Activity
        return new GetServiceClass();
    }

    /**
     * 传递服务对象的类
     */
    public class GetServiceClass extends Binder{

        public OCDownloadService getService(){
            return OCDownloadService.this;
        }

    }
    ... ...
}

然后我们在AndroidManifest.xml里面注册一下:

<service android:name=".OCDownloader.OCDownloadService"/>

下载请求的检查与处理

然后我们就开始进入正题 !
首先第一点,我们使用HashMap来当作储存下载任务信息的总表,这样的好处是我们可以在查找任务的时候通过 Key 来查询,而不需要通过遍历 List 的方法来获取任务信息。而且我们传递的时候可以直接使用它的一份Copy就行了,不需要把自己传出去。

下面我们来看代码:

(关于Service的生命周期啥的我就不再重复说了。我这里使用的是本地广播来传输下载信息的更新。剩下的在代码注释中有详细的解释)

public class OCDownloadService extends Service{

    static final int MAX_DOWNLOADING_TASK = 2; //最大同时下载数
    private LocalBroadcastManager broadcastManager;
    private HashMap allTaskList;
    private OCThreadExecutor threadExecutor;

    private boolean keepAlive = false;
    private int runningThread = 0;

    @Override
    public void onCreate() {
        super.onCreate();

        //创建任务线程池
        if (threadExecutor == null){
            threadExecutor = new OCThreadExecutor(MAX_DOWNLOADING_TASK,"downloading");
        }

        //创建总表对象
        if (allTaskList == null){
            allTaskList = new HashMap<>();
        }

        //创建本地广播器
        if (broadcastManager == null){
            broadcastManager = LocalBroadcastManager.getInstance(this);
        }
    }

    /**
     * 下载的请求就是从这里传进来的,我们在这里进行下载任务的前期处理
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        //检测传过来的请求是否完整。我们只需要 下载网址、文件名、下载路径 即可。
        if (intent != null && intent.getAction() != null && intent.getAction().equals("NewTask")){
            String url = intent.getExtras().getString("url");
            String title = intent.getExtras().getString("title");
            String path = intent.getExtras().getString("path");

            //检测得到的数据是否有效
            if (TextUtils.isEmpty(url) || TextUtils.isEmpty(title) || TextUtils.isEmpty(path)){
                Toast.makeText(OCDownloadService.this,"Invail data",Toast.LENGTH_SHORT).show();
                return super.onStartCommand(intent, flags, startId);
            }else {

                //如果有效则执行检查步骤
                checkTask(new DLBean(title,url,path));
            }
        }
        return super.onStartCommand(intent, flags, startId);
    }

    /**
     * 检查新的下载任务
     * @param requestBean   下载对象的信息Bean
     */
    private synchronized void checkTask(@Nullable DLBean requestBean){
        if (requestBean != null){

            //先检查是否存在同名的文件
            if (new File(requestBean.getPath()+"/"+requestBean.getTitle()).exists()){
                Toast.makeText(OCDownloadService.this,"File is already downloaded",Toast.LENGTH_SHORT).show();
            }else {

                //再检查是否在总表中
                if (allTaskList.containsKey(requestBean.getUrl())){
                    DLBean bean = allTaskList.get(requestBean.getUrl());
                    //检测当前的状态
                    //如果是 暂停 或 失败 状态的则当作新任务开始下载
                    switch (bean.getStatus()){
                        case DOWNLOADING:
                            Toast.makeText(OCDownloadService.this,"Task is downloading",Toast.LENGTH_SHORT).show();
                            return;
                        case WAITTING:
                            Toast.makeText(OCDownloadService.this,"Task is in the queue",Toast.LENGTH_SHORT).show();
                            return;
                        case PAUSED:
                        case FAILED:
                            requestBean.setStatus(OCDownloadStatus.WAITTING);
                            startTask(requestBean);
                            break;
                    }
                }else {
                    //如果不存在,则添加到总表
                    requestBean.setStatus(OCDownloadStatus.WAITTING);
                    allTaskList.put(requestBean.getUrl(),requestBean);
                    startTask(requestBean);
                }

            }

        }

    }

    /**
     * 将任务添加到下载队列中
     * @param requestBean   下载对象的信息Bean
     */
    private void startTask(DLBean requestBean){
        if (runningThread < MAX_DOWNLOADING_TASK){
            //如果当前还有空闲的位置则直接下载 , 否则就是在等待中
            requestBean.setStatus(OCDownloadStatus.DOWNLOADING);
            runningThread += 1;
            threadExecutor.submit(new FutureTask<>(new DownloadThread(requestBean)),requestBean.getUrl());
        }
        updateList();
    }

    /**
     * 得到一份总表的 ArrayList 的拷贝
     * @return  总表的拷贝
     */
    public ArrayList getTaskList(){
        return new ArrayList<>(allTaskList.values());
    }

    /**
     * 更新整个下载列表
     */
    private void updateList(){
        //我们等下再说这里
        ... ...
    }

    /**
     * 更新当前项目的进度
     * @param totalSize 下载文件的总大小
     * @param downloadedSize    当前下载的进度
     */
    private void updateItem(DLBean bean , long totalSize, long downloadedSize){
        //我们等下再说这里
        ... ...
    }

    /**
     * 执行的下载任务的Task
     */
    private class DownloadThread implements Callable<String>{
        //我们等下再说这里
        ... ...
    }

}

在大家看了一遍之后我再解释一遍流程:

1.收到新的任务请求
2.判断任务的信息是否完整
3.检查任务是否存在于总表,并检查状态
4.如果任务不存在总表中 或 任务之前是暂停、失败状态则当作新任务,否则提示任务已存在
5.如果当前已经是最大下载数,则任务标记为等待,不执行;否则开始下载

下载线程的实现

下面我们来看是如何下载的,这就会讲到断点续传的问题了,首先这个断点续传的功能得服务器支持才可以。然后我们在下载的时候生成一个临时文件,在下载完成之前我们将这个任务的所有数据存入这个文件中,直到下载完成,我们才将名字更改回正式的。网上有人将数据存入数据库中,我觉得这种方式虽然避免了临时文件的产生,但是这效率就…………

    /**
     * 执行的下载任务方法
     */
    private class DownloadThread implements Callable<String>{

        private DLBean bean;
        private File downloadFile;
        private String fileSize = null;

        public DownloadThread(DLBean bean) {
            this.bean = bean;
        }

        @Override
        public String call() throws Exception {

            //先检查是否有之前的临时文件
            downloadFile = new File(bean.getPath()+"/"+bean.getTitle()+".octmp");
            if (downloadFile.exists()){
                fileSize = "bytes=" + downloadFile.length() + "-";
            }

            //创建 OkHttp 对象相关
            OkHttpClient client = new OkHttpClient();

            //如果有临时文件,则在下载的头中添加下载区域
            Request request;
            if ( !TextUtils.isEmpty(fileSize) ){
                request = new Request.Builder().url(bean.getUrl()).header("Range",fileSize).build();
            }else {
                request = new Request.Builder().url(bean.getUrl()).build();
            }
            Call call = client.newCall(request);
            try {
                bytes2File(call);
            } catch (IOException e) {
                Log.e("OCException",""+e);
                if (e.getMessage().contains("interrupted")){
                    Log.e("OCException","Download task: "+bean.getUrl()+" Canceled");
                    downloadPaused();
                }else {
                    downloadFailed();
                }
                return null;
            }
            downloadCompleted();
            return null;
        }

        /**
         * 当产生下载进度时
         * @param downloadedSize    当前下载的数据大小
         */
        public void onDownload(long downloadedSize) {
            bean.setDownloadedSize(downloadedSize);
            Log.d("下载进度", "名字:"+bean.getTitle()+"  总长:"+bean.getTotalSize()+"  已下载:"+bean.getDownloadedSize() );
            updateItem(bean, bean.getTotalSize(), downloadedSize);
        }

        /**
         * 下载完成后的操作
         */
        private void downloadCompleted(){
            //当前下载数减一
            runningThread -= 1;
            //将临时文件名更改回正式文件名
            downloadFile.renameTo(new File(bean.getPath()+"/"+bean.getTitle()));
            //从总表中移除这项下载信息
            allTaskList.remove(bean.getUrl());
            //更新列表
            updateList();
            if (allTaskList.size() > 0){
                //执行剩余的等待任务
                checkTask(startNextTask());
            }
            threadExecutor.removeTag(bean.getUrl());
        }

        /**
         * 下载失败后的操作
         */
        private void downloadFailed(){
            runningThread -= 1;
            bean.setStatus(OCDownloadStatus.FAILED);
            if (allTaskList.size() > 0){
                //执行剩余的等待任务
                checkTask(startNextTask());
            }
            updateList();
            threadExecutor.removeTag(bean.getUrl());
        }

        /**
         * 下载暂停后的操作
         */
        private void downloadPaused(){
            runningThread -= 1;
            bean.setStatus(OCDownloadStatus.PAUSED);
            if (allTaskList.size() > 0){
                //执行剩余的等待任务
                checkTask(startNextTask());
            }
            updateList();
            threadExecutor.removeTag(bean.getUrl());
        }

        /**
         * 查找一个等待中的任务
         * @return  查找到的任务信息Bean , 没有则返回 Null
         */
        private DLBean startNextTask(){
            for (DLBean dlBean : allTaskList.values()) {
                if (dlBean.getStatus() == OCDownloadStatus.WAITTING) {
                    //在找到等待中的任务之后,我们先把它的状态设置成 暂停 ,再进行创建
                    dlBean.setStatus(OCDownloadStatus.PAUSED);
                    return dlBean;
                }
            }
            return null;
        }

        /**
         * 将下载的数据存到本地文件
         * @param call  OkHttp的Call对象
         * @throws IOException  下载的异常
         */
        private void bytes2File(Call call) throws IOException{

            //设置输出流. 
            OutputStream outPutStream;

            //检测是否支持断点续传
            Response response = call.execute();
            ResponseBody responseBody = response.body();
            String responeRange = response.headers().get("Content-Range");
            if (responeRange == null || !responeRange.contains(Long.toString(downloadFile.length()))){

                //最后的标记为 true 表示下载的数据可以从上一次的位置写入,否则会清空文件数据.
                outPutStream = new FileOutputStream(downloadFile,false);
            }else {
                outPutStream = new FileOutputStream(downloadFile,true);
            }

            InputStream inputStream = responseBody.byteStream();

            //如果有下载过的历史文件,则把下载总大小设为 总数据大小+文件大小 . 否则就是总数据大小
            if ( TextUtils.isEmpty(fileSize) ){
                bean.setTotalSize(responseBody.contentLength());
            }else {
                bean.setTotalSize(responseBody.contentLength() + downloadFile.length());
            }

            int length;
            //设置缓存大小
            byte[] buffer = new byte[1024];

            //开始写入文件
            while ((length = inputStream.read(buffer)) != -1){
                outPutStream.write(buffer,0,length);
                onDownload(downloadFile.length());
            }

            //清空缓冲区
            outPutStream.flush();
            outPutStream.close();
            inputStream.close();
        }

    }

代码实现的步骤:

1.检测是否存在本地文件并由此设置请求头内的请求长度范围
2.访问网址并获取到返回的头,检测是否支持断点续传,由此设置是否重新开始写入数据
3.获取输入流,开始写入数据
4.如果抛出了异常,并且异常不为中断,则为下载失败,否则不作响应
5.下载失败、下载完成,都会自动寻找仍在队列中的等待任务进行下载

广播更新消息

在Service这里面我们什么都不用管,就是把数据广播出去就行了

    /**
     * 更新整个下载列表
     */
    private void updateList(){
        broadcastManager.sendBroadcast(new Intent("update_all"));
    }

    /**
     * 更新当前项目的进度
     * @param totalSize 下载文件的总大小
     * @param downloadedSize    当前下载的进度
     */
    private void updateItem(DLBean bean , long totalSize, long downloadedSize){
        int progressBarLength = (int) (((float)  downloadedSize / totalSize) * 100);
        Intent intent = new Intent("update_singel");
        intent.putExtra("progressBarLength",progressBarLength);
        intent.putExtra("downloadedSize",String.format("%.2f", downloadedSize/(1024.0*1024.0)));
        intent.putExtra("totalSize",String.format("%.2f", totalSize/(1024.0*1024.0)));
        intent.putExtra("item",bean);
        broadcastManager.sendBroadcast(intent);
    }

下载管理Activity 实现

Service做好了之后,我们接下来就是要做查看任务的Activity了!
这个Activity用于展示下载任务、暂停继续终止任务。

我们先看整个Activity的基础部分,我们之后再说接收器部分的实现。RecyclerView的Adapter点击事件回调 和 服务连接这类的我就不再赘述了。这些都不是我们关心的重点,需要注意的就是服务和广播要注意解除绑定和解除注册。

public class OCDownloadManagerActivity extends AppCompatActivity implements OCDownloadAdapter.OnRecycleViewClickCallBack{

    RecyclerView downloadList;
    OCDownloadAdapter downloadAdapter;
    OCDownloadService downloadService;
    LocalBroadcastManager broadcastManager;
    UpdateHandler updateHandler;
    ServiceConnection serviceConnection;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_download_manager);

        //RecycleView 的 Adapter 创建与点击事件的绑定
        downloadAdapter = new OCDownloadAdapter();
        downloadAdapter.setRecycleViewClickCallBack(this);

        //RecyclerView 的创建与相关操作
        downloadList = (RecyclerView)findViewById(R.id.download_list);
        downloadList.setLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false));
        downloadList.setHasFixedSize(true);
        downloadList.setAdapter(downloadAdapter);

        //广播过滤器的创建
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("update_all");       //更新整个列表的 Action
        intentFilter.addAction("update_singel");    //更新单独条目的 Action

        //广播接收器 与 本地广播 的创建和注册
        updateHandler = new UpdateHandler();
        broadcastManager = LocalBroadcastManager.getInstance(this);
        broadcastManager.registerReceiver(updateHandler,intentFilter);

        //创建服务连接
        serviceConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                //当服务连接上的时候
                downloadService = ((OCDownloadService.GetServiceClass)service).getService();
                downloadAdapter.updateAllItem(downloadService.getTaskList());
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {
                //当服务断开连接的时候
                if (broadcastManager != null && updateHandler != null){
                    broadcastManager.unregisterReceiver(updateHandler);
                }
            }
        };

        //连接服务并进行绑定
        startService(new Intent(this,OCDownloadService.class));
        bindService(new Intent(this,OCDownloadService.class),serviceConnection,BIND_AUTO_CREATE);    

    }

    /**
     * RecyclerView 的单击事件
     * @param bean  点击条目中的 下载信息Bean
     */
    @Override
    public void onRecycleViewClick(DLBean bean) {
        if (downloadService != null){
            downloadService.clickTask(bean.getUrl(),false);
        }
    }

    /**
     * RecyclerView 的长按事件
     * @param bean  点击条目中的 下载信息Bean
     */
    @Override
    public void onRecycleViewLongClick(DLBean bean) {
        if (downloadService != null){
            downloadService.clickTask(bean.getUrl(),true);
        }
    }

    /**
     * 本地广播接收器  负责更新UI
     */
    class UpdateHandler extends BroadcastReceiver{
        ... ...
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        //解绑接收器
        broadcastManager.unregisterReceiver(updateHandler);

        //解绑服务
        unbindService(serviceConnection);
    }    

}

广播更新UI

接下来我们来实现广播接收器部分,也就是列表的刷新。

为什么要分开单独更新与整体更新呢?因为在下载的过程中的进度更新是非常非常频繁的,如果我们以这么高的频率来刷新UI,无疑会产生很大的负担。如果列表中只有几项的时候也许还行,但如果有1000+条的时候就很不容乐观了 (1年前刚开始接触这个东西的时候,是QQ中的一个好友@eprendre 告诉了我这个思路的。 如果各位dalao还有更好的方法麻烦在评论区留下您的见解)

    /**
     * 本地广播接收器  负责更新UI
     */
    class UpdateHandler extends BroadcastReceiver{

        @Override
        public void onReceive(Context context, Intent intent) {
            switch (intent.getAction()){
                case "update_all":
                    //更新所有项目

                    downloadAdapter.updateAllItem(downloadService.getTaskList());
                    break;
                case "update_singel":
                    //仅仅更新当前项

                    DLBean bean = intent.getExtras().getParcelable("item");
                    String downloadedSize = intent.getExtras().getString("downloadedSize");
                    String totalSize = intent.getExtras().getString("totalSize");
                    int progressLength = intent.getExtras().getInt("progressBarLength");
                    //如果获取到的 Bean 有效
                    if (bean != null){
                        View itemView = downloadList.getChildAt(downloadAdapter.getItemPosition(bean));
                        //如果得到的View有效
                        if (itemView != null){
                            TextView textProgress = (TextView)itemView.findViewById(R.id.textView_download_length);
                            ProgressBar progressBar = (ProgressBar)itemView.findViewById(R.id.progressBar_download);

                            //更新文字进度
                            textProgress.setText(downloadedSize+"MB / "+totalSize+"MB");

                            //更新进度条进度
                            progressBar.setProgress(progressLength);
                            TextView status = (TextView)itemView.findViewById(R.id.textView_download_status);

                            //更新任务状态
                            switch (bean.getStatus()){
                                case DOWNLOADING:
                                    status.setText("Downloading");
                                    break;
                                case WAITTING:
                                    status.setText("Waitting");
                                    break;
                                case FAILED:
                                    status.setText("Failed");
                                    break;
                                case PAUSED:
                                    status.setText("Paused");
                                    break;
                            }
                        }
                    }
                    break;
            }
        }

    }

这里说一点就是 OKHttp 的下载进度监听,我之前曾按照

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0904/3416.html

这里说的方法做过一次,能成功监听到下载。但是我现在用OKHTTP3,这个方法好像并不奏效……估计是版本的问题吧。

你可能感兴趣的:(Android)