Android中实现单线程下载文件是比较容易的,可是要使得自己的应用支持多线程断点下载就要考虑到很多细节了,今天我们一起来探讨一下多线程断点下载时怎么实现的。
首先先画一张图说明一下Android中下载文件的大致流程:
上面的图介绍的是比较清楚的,我们要下载一个文件,首先需要在Activity中选择需要下载的目标,然后把下载的任务交个Service中(这里为什么要交给Service,相信很多人都知道,我们在Activity中执行下载也是可以的,可是Activity是很容易让用户销毁的,如果我们退出了Activity,可是下载线程还在执行,那么就会导致退出的Activity无法被回收,也就导致了内存泄露,所以我们会把这种比较耗时的请求交给Service后台任务就执行,因为Service不是那么容易被销毁的),然后在Service中创建下载线程读取网络文件,并把文件存放在本地文件中,并且可以在下载的过程中通过发送广播的方式通知Activity当前下载的进度。
上面的图介绍了单线程下载的流程,理解了上面的图之后,多线程断点下载也就很容易理解了。改为多线程下载只需要我们修改读取网络文件那部分,将网络文件分成好几个段,人后创建多个线程分别读取各段的数据,然后在本地进行组装就实现了多线成下载。例如网络文件有100字节,我们分为三个线程进行下载:
我们首先计算出每个线程所需要下载的字节起始终止位置,就可以从网络上进行读取,从而就是实现了多线程下载。
可是我们要怎样使得我们的下载程序支持断点下载呢?
其实原理也很简单,我们在第一次下载时,先创建下载线程进行下载,当用户点击暂停下载时,我们只需要保存下载线程的信息(如:每个线程的下载起始位置、下载的进度信息等),当用户再次点击继续下载时,我们只需要读取上次的下载信息继续接着上次的下载位置下载就可以了。
下面结合代码简单了解一下多线程断点下载:
首先我们看一下Activity,Activity的布局就不说了,我们在Activity中需要注册一个广播监听,用于获取下载进度并更新界面。
更新UI的广播接收器
BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (DownloadService2.ACTION_UPDATE.equals(intent.getAction())) { long finished = intent.getLongExtra("finished", 0); int id = intent.getIntExtra("id", 0); //更新相对应的进度条 listAdapter.updateProgress(id, finished); //progressBar.setProgress(finished); } else if (DownloadService2.ACTION_FINISHED.equals(intent.getAction())) { FileInfo fileinfo = (FileInfo) intent.getSerializableExtra("fileinfo"); //更新进度为100 listAdapter.updateProgress(fileinfo.getId(), 100); Toast.makeText( MainActivity2.this, fileinfo.getFileName() + "下载完成", Toast.LENGTH_SHORT).show(); } } };
注册广播:
private void initRegister() { //注册广播接收器 IntentFilter filter = new IntentFilter(); filter.addAction(DownloadService2.ACTION_UPDATE); filter.addAction(DownloadService2.ACTION_FINISHED); registerReceiver(mReceiver, filter); }
我们点击开始下载按钮,会启动下载文件的Service,Service或调用onStartCommand()方法:
@Override public int onStartCommand(Intent intent, int flags, int startId) { //获得Activity传来的参数 if (ACTION_START.equals(intent.getAction())) { FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileinfo"); Log.e(TAG, "onStartCommand: ACTION_START-" + fileInfo.toString()); new InitThread(fileInfo).start(); } else if (ACTION_PAUSE.equals(intent.getAction())) { FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileinfo"); Log.e(TAG, "onStartCommand:ACTION_PAUSE- " + fileInfo.toString()); //从集合在取出下载任务 DownloadTask2 task2 = tasks.get(fileInfo.getId()); if (task2 != null) { //停止下载任务 task2.isPause = true; } } return super.onStartCommand(intent, flags, startId); }
ACTION_START //指开始下载
ACTION_PAUSE //指暂停下载
上面的FileInfo是保存下载进度的实体类,当开始下载时,先创建一个线程准备初始化的数据,比如获取网络文件大小、在本地创建相应保存网络数据的文件等:
/** * 初始化 子线程 */ class InitThread extends Thread { private FileInfo tFileInfo; public InitThread(FileInfo tFileInfo) { this.tFileInfo = tFileInfo; } @Override public void run() { HttpURLConnection conn ; RandomAccessFile raf ; try { //连接网络文件 URL url = new URL(tFileInfo.getUrl()); conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(3000); conn.setRequestMethod("GET"); int length = -1; Log.e("getResponseCode==", conn.getResponseCode() + ""); if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { //获取文件长度 length = conn.getContentLength(); Log.e("length==", length + ""); } if (length < 0) { return; } //创建下载文件的目录 File dir = new File(DOWNLOAD_PATH); if (!dir.exists()) { if (!dir.mkdir()){ return; } } //在本地创建文件 File file = new File(dir, tFileInfo.getFileName()); raf = new RandomAccessFile(file, "rwd"); //设置本地文件长度 raf.setLength(length); tFileInfo.setLength(length); Log.e("tFileInfo.getLength==", tFileInfo.getLength() + ""); //发送消息准备下载 mHandler.obtainMessage(MSG_INIT, tFileInfo).sendToTarget(); raf.close(); conn.disconnect(); } catch (Exception e) { e.printStackTrace(); } } }
当初始化完成后会通过Handler向主线程发送一条消息,表示已经初始化完成,可以下载,Service中的Handler如下:
Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_INIT: FileInfo fileinfo = (FileInfo) msg.obj; Log.e("mHandler--fileinfo:", fileinfo.toString()); //启动下载任务 DownloadTask2 downloadTask2 = new DownloadTask2(DownloadService2.this, fileinfo, runThreadCount); downloadTask2.download(); //将下载任务添加到集合中 tasks.put(fileinfo.getId(), downloadTask2); break; } } };
在Handler中开始启动多线程开始下载任务,多线程下载主要封装在DownloadTask2中,下面我们看一下这个类的实现:
DownloadTask2的下载方法:
public void download() { //读取数据库的线程信息 List<ThreadInfo> threadInfos = mThreadDAO2.getThread(mFileInfo.getUrl()); Log.e("threadsize==", threadInfos.size() + ""); //如果为空,表示是第一次下载 if (threadInfos.size() == 0) { //获得每个线程下载的长度 long length = mFileInfo.getLength() / mThreadCount; for (int i = 0; i < mThreadCount; i++) { ThreadInfo threadInfo = new ThreadInfo(i, mFileInfo.getUrl(), length * i, (i + 1) * length - 1, 0); if (i + 1 == mThreadCount) { threadInfo.setEnd(mFileInfo.getLength()); } //添加到线程信息集合中 threadInfos.add(threadInfo); //向数据库插入线程信息 mThreadDAO2.insertThread(threadInfo); } } mThradList = new ArrayList<>(); //启动多个线程进行下载 for (ThreadInfo thread : threadInfos) { DownloadThread2 downloadThread = new DownloadThread2(thread); // downloadThread.start(); //在线程池中执行下载线程 DownloadTask2.sExecutorService.execute(downloadThread); //添加线程到集合中 mThradList.add(downloadThread); } }
创建多线程下载之前,首先获取下载的URL,并从数据库中个根据URL获取响应下载信息,如果获取为空,表明是第一次下载,如果获取的不为空,则创建线程接着获取的信息继续下载。
下面是下载线程的实现:
class DownloadThread2 extends Thread { private ThreadInfo threadInfo; public boolean isFinished = false; public DownloadThread2(ThreadInfo threadInfo) { this.threadInfo = threadInfo; } @Override public void run() { //向数据库插入线程信息 // Log.e("isExists==", mThreadDAO2.isExists(threadInfo.getUrl(), threadInfo.getId()) + ""); // if (!mThreadDAO2.isExists(threadInfo.getUrl(), threadInfo.getId())) { // mThreadDAO2.insertThread(threadInfo); // } HttpURLConnection connection; RandomAccessFile raf; InputStream is; try { URL url = new URL(threadInfo.getUrl()); connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(3000); connection.setRequestMethod("GET"); //设置下载位置 long start = threadInfo.getStart() + threadInfo.getFinish(); connection.setRequestProperty("Range", "bytes=" + start + "-" + threadInfo.getEnd()); //设置文件写入位置 File file = new File(DownloadService2.DOWNLOAD_PATH, mFileInfo.getFileName()); raf = new RandomAccessFile(file, "rwd"); raf.seek(start); Intent intent = new Intent(DownloadService2.ACTION_UPDATE); mFinished += threadInfo.getFinish(); Log.e("threadInfo.getFinish==", threadInfo.getFinish() + ""); // Log.e("getResponseCode ===", connection.getResponseCode() + ""); //开始下载 if (connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) { Log.e("getContentLength==", connection.getContentLength() + ""); //读取数据 is = connection.getInputStream(); byte[] buffer = new byte[1024 * 4]; int len = -1; long time = System.currentTimeMillis(); while ((len = is.read(buffer)) != -1) { if (isPause) { Log.e("mfinished==pause===", mFinished + ""); //下载暂停时,保存进度到数据库 mThreadDAO2.updateThread(mFileInfo.getUrl(), mFileInfo.getId(), threadInfo.getFinish()); return; } //写入文件 raf.write(buffer, 0, len); //累加整个文件下载进度 mFinished += len; //累加每个线程完成的进度 threadInfo.setFinish(threadInfo.getFinish() + len); //每隔1秒刷新UI if (System.currentTimeMillis() - time > 1000) {//减少UI负载 time = System.currentTimeMillis(); //把下载进度发送广播给Activity intent.putExtra("id", mFileInfo.getId()); intent.putExtra("finished", mFinished * 100 / mFileInfo.getLength()); mContext.sendBroadcast(intent); Log.e(" mFinished==update==", mFinished * 100 / mFileInfo.getLength() + ""); } } //标识线程执行完毕 isFinished = true; //检查下载任务是否完成 checkAllThreadFinished(); // //删除线程信息 // mThreadDAO2.deleteThread(mFileInfo.getUrl(), mFileInfo.getId()); is.close(); } raf.close(); connection.disconnect(); } catch (Exception e) { e.printStackTrace(); } } }
在下载线程中一直循环读取网络数据,每次循环检查下载是否完成,如果下载完成删除数据库的下载信息:
/** * 判断所有线程是否都执行完毕 */ private synchronized void checkAllThreadFinished() { boolean allFinished = true; //编辑线程集合 判断是否执行完毕 for (DownloadThread2 thread : mThradList) { if (!thread.isFinished) { allFinished = false; break; } } if (allFinished) { //删除线程信息 mThreadDAO2.deleteThread(mFileInfo.getUrl()); //发送广播给Activity下载结束 Intent intent = new Intent(DownloadService2.ACTION_FINISHED); intent.putExtra("fileinfo", mFileInfo); mContext.sendBroadcast(intent); } }
在下载线程中有这样一个判断:
if (isPause) { Log.e("mfinished==pause===", mFinished + ""); //下载暂停时,保存进度到数据库 mThreadDAO2.updateThread(mFileInfo.getUrl(), mFileInfo.getId(), threadInfo.getFinish()); return; }
isPause是下载是否暂停的标记,当用户点击暂停时,会把isPause赋值为true,下载线程在下载的过程中发现isPause被赋值为true后,就保存当前下载的具体信息,然后结束下载线程。
上面用到了数据库的操作,用于保存下载进度信息,这里就不具体介绍了,下面给出源码,大家可以看看
http://download.csdn.net/download/lxk_1993/9511182