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

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

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

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

服务基础

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

public class OCDownloadService extends Service{... ...@Nullable@Overridepublic IBinder onBind(Intent intent) {//当服务被Bind的时候我们就返回一个带有服务对象的类给Bind服务的Activityreturn 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<String,DLBean> allTaskList;private OCThreadExecutor threadExecutor;private boolean keepAlive = false;private int runningThread = 0;@Overridepublic 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);}}/*** 下载的请求就是从这里传进来的,我们在这里进行下载任务的前期处理*/@Overridepublic 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<DLBean> 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;}@Overridepublic 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;@Overrideprotected 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");       //更新整个列表的 ActionintentFilter.addAction("update_singel");    //更新单独条目的 Action//广播接收器 与 本地广播 的创建和注册updateHandler = new UpdateHandler();broadcastManager = LocalBroadcastManager.getInstance(this);broadcastManager.registerReceiver(updateHandler,intentFilter);//创建服务连接serviceConnection = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {//当服务连接上的时候downloadService = ((OCDownloadService.GetServiceClass)service).getService();downloadAdapter.updateAllItem(downloadService.getTaskList());}@Overridepublic 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*/@Overridepublic void onRecycleViewClick(DLBean bean) {if (downloadService != null){downloadService.clickTask(bean.getUrl(),false);}}/*** RecyclerView 的长按事件* @param bean  点击条目中的 下载信息Bean*/@Overridepublic void onRecycleViewLongClick(DLBean bean) {if (downloadService != null){downloadService.clickTask(bean.getUrl(),true);}}/*** 本地广播接收器  负责更新UI*/class UpdateHandler extends BroadcastReceiver{... ...}@Overrideprotected void onDestroy() {super.onDestroy();//解绑接收器broadcastManager.unregisterReceiver(updateHandler);//解绑服务unbindService(serviceConnection);}    }

广播更新UI

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

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

    /*** 本地广播接收器  负责更新UI*/class UpdateHandler extends BroadcastReceiver{@Overridepublic 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 实现简易下载管理器 (暂停、断点续传、多线程下载)相关推荐

  1. 如何实现 HTTP 断点续传多线程下载

    1. HTTP断点续传多线程下载 一个比较常见的场景,就是断点续传/下载,在网络情况不好的时候,可以在断开连接以后,仅继续获取部分内容. 例如在网上下载软件,已经下载了 95% 了,此时网络断了,如果 ...

  2. 断点续传---多线程下载进阶(一)

    打算总结7篇笔记.来学习下断点续传---多线程下载进阶 AndroidManifest.xml <?xml version="1.0" encoding="utf- ...

  3. 安卓day29网络编程 HttpClient AsyncHttpClient 断点续传多线程下载器 HttpUtils

    一.排坑 HttpClient.Header飘红 Android 6.0 已经移除了httpClient module下的build.gradle中加入: android{useLibrary 'or ...

  4. curl命令断点续传多线程下载文件

    新版本发布 curl命令7.66.0版本支持多线程下载 参考:https://daniel.haxx.se/blog/2019/09/11/curl-7-66-0-the-parallel-http- ...

  5. php文件断点续传,php文件下载限速,文件断点续传,多线程下载文件原理解析

    文件下载限速 首先,我们写一段使用php输出文件给浏览器下载的代码<?php /** * Created by PhpStorm. * User: tioncico * Date: 19-2-4 ...

  6. Android使用开源项目Xutils实现多线程下载文件

    #1.下载utils项目 https://github.com/wyouflf/xUtils #2布局文件里实现UI <LinearLayout xmlns:android="http ...

  7. android 多线程下载,断点续传,线程池

    android 多线程下载,断点续传,线程池 你可以在这里看到这个demo的源码: https://github.com/onlynight/MultiThreadDownloader 效果图 这张效 ...

  8. android多线程下载原理,安卓多线程断点续传下载功能(靠谱第三方组件,原理demo)...

    一,原生的DownloadManager 从Android 2.3(API level 9)开始,Android以Service的方式提供了全局的DownloadManager来系统级地优化处理长时间 ...

  9. 【Android】多线程下载加断点续传

    http://blog.csdn.net/smbroe/article/details/42270573 文件下载在App应用中也用到很多,一般版本更新时多要用的文件下载来进行处理,以前也有看过很多大 ...

最新文章

  1. java web 自定义异常_Java web, service 层应该通过异常(自定义Exception)来中断业务吗?...
  2. centos6 mysql 启动_【17-02-18】 【求助】centos6 mysql重启后无法正常启动
  3. [转载] python与c/c++相比有哪些优势
  4. EF – 8.多对多关联
  5. 【Java】@ResponseBody 返回JsonObject
  6. 有佳文档管理v2.03
  7. 矢量地图自定义切片样式
  8. 病毒分析之中华吸血鬼
  9. 写给喜欢单片机的初学者
  10. 正斜杠,又称左斜杠,符号是/;反斜杠,也称右斜杠,符号是\。
  11. PostgreSQL下载与安装(Windows版)
  12. 求推荐三本左右 高级的java开发者的相关书籍。类似 大型网站技术架构:核心原理与案例分析+李智慧 之类的书籍。...
  13. [轻笔记]Juliav0.6配置jupyter
  14. 彻底掌握 Commonjs 和 Es Module
  15. linux遇到nul字符,服务器崩溃,系统日志中出现ascii NUL字符(^ @ ^ @ ^ @ ...)
  16. 关于笔记本电池显示未充电的
  17. HDOJ HDU 1850 Being a Good Boy in Spring Festival
  18. 第 105 场双周赛
  19. Codeforces Round #797 (Div. 3)无F
  20. Spark算子综合案例

热门文章

  1. C语言基础:输入两个分数,输出它们的和以及差。(以分数形式)
  2. 手机吃鸡语音服务器异常错误,绝地求生游戏报错解决方法汇总
  3. 五路DI五路DO,网络继电器,Modbus TCP远程IO模块 WJ95
  4. 一加手机升级鸿蒙,配骁龙801四核/3GB内存 一加手机再升级
  5. 微信朋友圈python广告评论_【Python】我的微信朋友圈分析
  6. Java程序设计实验3 | 面向对象(上)
  7. ddos肉鸡都是哪来的?
  8. C#动态库封装(CH341\CP2112)
  9. 低版本浏览器兼容处理
  10. Revit (3) - 二开 -创建柱子