Android 实现简易下载管理器 (暂停、断点续传、多线程下载)
来源:互联网 发布:淘宝客qq群现在好做吗 编辑:程序博客网 时间:2024/05/18 00:32
什么都先别说,先看预览图!
预览图中是限制了同时最大下载数为 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<String,DLBean> 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<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; } @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 实现简易下载管理器 (暂停、断点续传、多线程下载)
- android下多线程下载,断点续传,及暂停按钮
- Android中实现可暂停的断点续传的下载
- android 多线程断点续传下载
- Android多线程.断点续传下载
- android 多线程断点续传下载
- Android 多线程下载断点续传
- Android多线程断点续传下载
- Android多线程断点续传下载
- android多线程断点续传下载
- android 多线程下载断点续传
- android多线程断点续传下载
- Android多线程断点续传下载
- Android多线程下载断点续传
- Android多线程断点续传下载
- Android多线程断点续传下载
- Android多线程断点续传下载
- android断点续传多线程下载
- 配置Gradle构建
- 矩形覆盖
- CSS文本超出
- Universal-Image-Loader完全解析
- Using convolutional neural nets to detect facial keypoints tutorial
- Android 实现简易下载管理器 (暂停、断点续传、多线程下载)
- IOS 解决集成zbar链接错误
- windows下安装nginx
- com.android.build.transform.api.TransformException
- hduoj1198(并查集)
- ios 指定某个页面是横屏还是竖屏
- Android Studio Mac版快捷键
- iOS沙盒目录结构解析
- BLOB和TEXT的区别