前言
我们常常在开发过程中会遇到下载的功能实现,当我们下载中断时,又不希望下次从头开始继续下载,我们就需要用到断点续传了。
断点续传原理
断点续传是指当下载中断后,再次下载时可以从上次的下载进度继续下载。由此我们可以分析得出实现这个功能,我们需要实时保存下载进度,这样在下次继续下载的时候再把下载进度读取出来,继续下载。我们主要需要解决俩个问题:一、从上次的位置继续下载。二、从上次写入的文件继续写入。
第一点,通过Http的GET请求中的setRequestProperty(“Range”, “bytes=” + 开始位置+ “-” + “结束位置”)方法,可以设置下载的数据的开始位置和结束位置。这样我们就可以从上次的下载位置继续下载。
第二点,通过RandomAccessFile可以在本地文件中继续写入文件。
因此,实现断点续传我们可以按照以下步骤。
1.首先获取要下载的文件长度,用来设置RandomAccessFile(本地文件)的长度。
2.需要知道下载中断时,文件下载到哪里了,我们需要实时保存文件下载进度,这个功能我们可以用数据库来实现。
3.中断后再次下载时,读取进度,再从上次的下载进度继续下载,并在本地的文件继续写入。
4.实时的更新下载进度条,作为给用户的下载反馈。
多线程结合断点续传下载
多线程无非是将待下载的文件分成若干个部分进行下载并实现断点续传。
1.同样,我们首先要获取待下载的文件的长度,用来为每个线程分配下载长度。通过HttpURLConnection.getContentLength()获取待下载的文件的长度。如下:
filesize=connection.getContentLength();
2.通过前面获取的下载文件的长度,为每个线程计算下载长度,即为每个线程设置下载的起始位置跟结束位置。通过HttpUrlConnection.setRequestProperty(“Range”, “bytes=” + 开始位置+ “-” + “结束位置”)方法。
计算方法如下:
int block = (filesize % threadCount == 0) ? filesize / threadCount : filesize / threadCount + 1;
所以每个线程对应的起始位置跟结束位置分别为:i * block, (i + 1) * block(i从0开始)
3.通过RandomAccessFile可以在文件指定位置写入数据。如下:
mRandomAccessFile.seek(startPos)
4.为每个线程的下载的进度都保存数据,这样每次每次暂停后重新下载都重新读取下载进度,并且可以从上次位置重新下载。并且再本地文件中继续读入数据。
实现
首先来看布局,布局很简单,布局用ProgressBar来作为下载进度的反馈,每当下载进度发生变化,都会在这进行更新。
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.administrator.downloaddemo.MainActivity"> <Button android:id="@+id/start_download" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:text="开始下载"/> <Button android:id="@+id/stop_download" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_marginRight="20dp" android:text="停止下载"/> <ProgressBar android:id="@+id/pbSmall" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:layout_marginLeft="10dp" android:layout_marginTop="50dp" /></RelativeLayout>
接下来是用来实现界面更新的Handler,我们会传入这个hander来更新UI。当下载开始(DOWNLOAD_START)、下载进行(DOWNLOAD_KEEP )、下载结束(DOWNLOAD_COMPLETE)以及失败时(DOWNLOAD_FAIL)分别进行对应的操作。
private Handler mHandler=new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { switch (msg.what){ case Constant.DOWNLOAD_START: mProgressBar.setMax(msg.arg1); break; case Constant.DOWNLOAD_KEEP: mProgressBar.setProgress(msg.arg1); break; case Constant.DOWNLOAD_COMPLETE: Toast.makeText(MainActivity.this,"下载完成",Toast.LENGTH_SHORT).show(); String url= (String) msg.obj; DBManager.getInstance(MainActivity.this).delete(url); break; case Constant.DOWNLOAD_FAIL: Toast.makeText(MainActivity.this,"下载失败",Toast.LENGTH_SHORT).show(); String urlstr= (String) msg.obj; FileDownloader.getInstance().pauseDownload(urlstr); break; case Constant.DOWNLOAD_ClLEAN: // do something break; default: break; } return true; } });
具体的下载实现类是FileDownLoder,通过调用statDownload()方法并传入文件下载链接,文件大小,文件名,线程数量以及前面的handler如下便开始了下载:
FileDownloader.getInstance().init(context,handler,downloadurl,filesize,filename,threadcount).startDownload();
先看FileDownload的初始化:
#FileDownloader.java public synchronized FileDownloader init(Context context, Handler handler, String downloadurl, int filesize, String filename, int threadCount) { Log.d(TAG, "Run in init"); this.context = context; this.handler = handler; this.downloadurl = downloadurl; this.filesize = filesize; this.filename = filename; this.threadCount = threadCount; if (downloadStatemap == null) { downloadStatemap = new HashMap<>(); } initDatas(); return this; } private void initDatas() { RandomAccessFile accessFile = null; File file; int block = (filesize % threadCount == 0) ? filesize / threadCount : filesize / threadCount + 1; try { file = new File(filename); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } if (!DBManager.getInstance(context).isHasInfos(downloadurl)) { Log.d(TAG, "run download info"); for (int i = 0; i < threadCount; i++) { DownloadInfo info = new DownloadInfo(i, i * block, (i + 1) * block, 0, downloadurl); DBManager.getInstance(context).saveInfo(info); } } accessFile = new RandomAccessFile(file, "rw"); if (accessFile.length() == filesize) { return; } accessFile.setLength(filesize); } catch (Exception e) { e.printStackTrace(); } finally { try { if (accessFile != null) { accessFile.close(); } } catch (IOException e) { e.printStackTrace(); } } }
初始化了文件下载链接,文件大小,文件名,线程数量以及handler等信息,还有一个比较重要的是往数据库中写入下载信息,如果这是一个新建任务,就为它的每个下载线程初始化他的下载起始位置,结束位置。接下来我们看startDownload()这个方法:
#FileDownloader.java public synchronized void startDownload() { if (downloadStatemap.get(downloadurl) != null && downloadStatemap.get(downloadurl) == Constant.DOWNLOAD_STATE_START) { //已经正在下载中的话就不重新开启线程下载 Log.d(TAG, "download return"); return; } sendMessage(Constant.DOWNLOAD_START, filesize, -1, null); for (int i = 0; i < threadCount; i++) { ThreadPoolsUtil.getInstance().getCachedThreadPool().execute(new DownloadTask(context, handler, downloadurl, filesize, filename, i)); } }
每次开始下载都会向handler发送开始下载的message,主要是将文件大小传给ProgressBar。然后再根据线程数量开启下载任务。每个DownloadTask都是一个Runnable。我们接下去看看DownloadTask。
#DownloadTask.java public void run() { FileDownloader.getInstance().putDownloadState(mDownloadurl, Constant.DOWNLOAD_STATE_START); HttpURLConnection connection = null; BufferedInputStream inputStream = null; DownloadInfo info = new DownloadInfo(); Log.d(TAG, "is has info: " + DBManager.getInstance(mContext).isHasInfos(mDownloadurl)); if (DBManager.getInstance(mContext).isHasInfos(mDownloadurl)) { //判断是否存在未完成的该任务 info = DBManager.getInstance(mContext).getInfo(mDownloadurl, threadId); } try { URL url = new URL(mDownloadurl); int compeltesize = info.getCompeleteSize(); int startPos = info.getStartPos(); //本地数据库中的保存的开始位置跟结束位置 int endPos = info.getEndPos(); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setConnectTimeout(10000); connection.setReadTimeout(10000); connection.setRequestProperty("Connection", "Keep-Alive"); connection.setRequestProperty("Range", "bytes=" + startPos+compeltesize + "-" + endPos); inputStream = new BufferedInputStream(connection.getInputStream()); mRandomAccessFile = new RandomAccessFile(mFilename, "rw"); mRandomAccessFile.seek(startPos+compeltesize); //上次的最后的写入位置 byte[] buffer = new byte[8 * 1024]; int length = 0; while ((length = inputStream.read(buffer)) > 0) { if (FileDownloader.getInstance().getDownloadState(mDownloadurl) == Constant.DOWNLOAD_STATE_PAUSE) { //下载任务被暂停 return; }// Log.d(TAG, "write file length: " + length); mRandomAccessFile.write(buffer, 0, length); compeltesize += length;// Log.d(TAG,"save completesize is: "+compeltesize); DBManager.getInstance(mContext).updataInfos(threadId, compeltesize, mDownloadurl); //保存数据库中的下载进度 sendMessage(Constant.DOWNLOAD_KEEP, calculateCompeltesize(), -1, null); //更新进度条 } Log.d(TAG, "calculateCompeltesize: " + calculateCompeltesize() + " filesize: " + size + "threadid: " + threadId); if (calculateCompeltesize() >= size) { //判断下载是否完成 sendMessage(Constant.DOWNLOAD_COMPLETE, -1, -1, mDownloadurl); } } catch (Exception e) { sendMessage(Constant.DOWNLOAD_FAIL, -1, -1, mDownloadurl); e.printStackTrace(); } finally { try { if (inputStream != null) { inputStream.close(); } if (connection != null) { connection.disconnect(); } if (mRandomAccessFile != null) { mRandomAccessFile.close(); } sendMessage(Constant.DOWNLOAD_ClLEAN, -1, -1, mDownloadurl); } catch (IOException e) { e.printStackTrace(); } } }
下载过程中会从数据库读取下载位置的信息,根据这个信息来写入数据到本地文件。并通过handler来更新进度条。并且通过FileDownloader.getInstance().getDownloadState(mDownloadurl)实时检测下载状态是否被暂停了。暂停通过调用以下函数:
#FileDownloader.java public synchronized void pauseDownload(String downloadurl) { downloadStatemap.put(downloadurl, Constant.DOWNLOAD_STATE_PAUSE); } public void pauseAll() { if (downloadStatemap == null) { return; } for (String key:downloadStatemap.keySet()){ downloadStatemap.put(key,Constant.DOWNLOAD_STATE_PAUSE); } }
在FileDownLoder里面有着保存着每个下载URL对应的下载状态的Hashmap。可以通过设置这些url的下载状态来跟新任务的下载状态。