转载请注意:http://blog.youkuaiyun.com/wjzj000/article/details/73658491
本菜开源的一个自己写的Demo,希望能给Androider们有所帮助,水平有限,见谅见谅…
https://github.com/zhiaixinyang/PersonalCollect (拆解GitHub上的优秀框架于一体,全部拆离不含任何额外的库导入)
https://github.com/zhiaixinyang/MyFirstApp(Retrofit+RxJava+MVP)
写在前面
记录一篇实现断点续传的博客,原项目地址:https://github.com/103style/Download
开始
首先来说服务存在的意义是一旦开启基本上不出意外的话不会被系统所回收,因此适合做一些不需要与前台交互的功能。比如说后台下载,可能有朋友会说后台下载为什么不用一个线程呢?
这里我们来比较一下服务和线程:(相关内容来自博客:http://ticktick.blog.51cto.com/823160/1547032)
线程:
在Activity中被创建
一般在onCreate时创建,在onDestroy()中销毁,否则,Activity销毁后,Thread是会依然在后台运行着。如果此时一直后台运行的话,因为Activity已经被销毁,我们很难在重新去控制这个线程。Thread的生命周期即为整个Activity的生命周期。所以,在Activity中创建的Thread只适合完成一些依赖Activity本身有关的任务,比如定时更新一下Activity的控件状态等。
在Application中被创建
一般自定义Application类,重载onCreate方法,并在其中创建Thread,当然,也会在onTerminate()方法中销毁Thread,否则,如果Thread没有退出的话,即使整个Application退出了,线程依然会在后台运行着。这种情况下,Thread的生命周期即为整个Application的生命周期。
所以,在Application中创建的Thread,可以执行一些整个应用级别的任务,比如定时检查一下网络连接状态等等。
以上这两种情况下,Thread的生命周期都不应该超出整个应用程序的生命周期,也就是,整个APP退出之后,Thread都应该完全退出,这样才不会出现内存泄漏或者僵尸线程。那么,如果你希望整个APP都退出之后依然能运行该Thread,那么就应该把Thread放到Service中去创建和启动了。
在Service中被创建
这是保证最长生命周期的Thread的唯一方式,只要整个Service不退出,Thread就可以一直在后台执行,一般在Service的onCreate()中创建,在onDestroy()中销毁。所以,在Service中创建的Thread,适合长期执行一些独立于APP的后台任务,比较常见的就是:在Service中保持与服务器端的长连接。
- 服务:(来自官方的解释)
- Service 是一个可以在后台执行长时间运行操作而不提供用户界面的应用组件。服务可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。 此外,组件可以绑定到服务,以与之进行交互,甚至是执行进程间通信 (IPC)。 例如,服务可以处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序交互,而所有这一切均可在后台进行。
这里简单的梳理了进程和线程的一些区别是应用场景,接下来我们就进入代码阶段
整体思路
既然是断点续传,这里我们肯定需要用到数据库进行对停止时的数据进行存放。下载方面在Service中开启线程使用最基本的HttpURLConnection,既然有下载那么必然要更新下载进度,进度方面使用BroadcastReceiver进行交互更新。
代码
最开始我们在某个组件中开启我们这个下载服务:
Intent intent = new Intent(DownloadFileActivity.this, DownloadService.class);、
//通知Service开始下载
intent.setAction(DownloadService.ACTION_START);
intent.putExtra("fileinfo", fileInfo);
startService(intent);
//通知Serice暂停下载
intent.setAction(DownloadService.ACTION_PAUSE);
intent.putExtra("fileinfo", fileInfo);
startService(intent);
//fileInfo:是我们记录的下载文件的信息
url = "http://dldir1.qq.com/weixin/android/weixin6316android780.apk";
fileInfo = new FileInfo(0, url, "WeChat", 0, 0);
Serivce
在Service中的
onStartCommand()
方法中,我们通过intent的setAction()
参数来判断是开始下载还是续传。
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//获得Activity传来的参数
if (ACTION_START.equals(intent.getAction())) {
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileinfo");
//一个自定义的线程类
new InitThread(fileInfo).start();
} else if (ACTION_PAUSE.equals(intent.getAction())) {
if (mDownloadTask != null) {
mDownloadTask.isPause = true;
}
}
return super.onStartCommand(intent, flags, startId);
}
当我们判断是开始下载时,我们会调用一个后台线程类来开始下载内容(
InitThread
仅仅是用于获取下载文件的信息):
class InitThread extends Thread {
private FileInfo tFileInfo;
public InitThread(FileInfo tFileInfo) {
this.tFileInfo = tFileInfo;
}
@Override
public void run() {
HttpURLConnection conn = null;
RandomAccessFile raf = null;
//省略try-catch部分
//连接网络文件
URL url = new URL(tFileInfo.getUrl());
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(3000);
conn.setRequestMethod("GET");
int length = -1;
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
//获取文件长度
length = conn.getContentLength();
}
if (length < 0) {
return;
}
//一个常量Environment.getExternalStorageDirectory().getAbsolutePath() + "/downloads/";
File dir = new File(DOWNLOAD_PATH);
if (!dir.exists()) {
dir.mkdir();
}
//在本地创建文件
File file = new File(dir, tFileInfo.getFileName());
raf = new RandomAccessFile(file, "rwd");
//设置本地文件长度
raf.setLength(length);
tFileInfo.setLength(length);
//通过handler告知主线程开启真正的下载进程
mHandler.obtainMessage(MSG_INIT, tFileInfo).sendToTarget();
}
}
mHandler:
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INIT:
FileInfo fileinfo = (FileInfo) msg.obj;
//启动下载任务,传入我们在InitThread线程中获取到的文件信息
mDownloadTask = new DownloadTask(DownloadService.this, fileinfo);
mDownloadTask.download();
break;
}
}
};
接下来让我们进入DownloadTask类去看一看。
DownloadTask
在构造方法中进行初始化下载内容,以及操作数据库的相关类。
public DownloadTask(Context mContext, FileInfo mFileInfo) {
this.mContext = mContext;
this.mFileInfo = mFileInfo;
mThreadDAO = new ThreadDAOImpl(mContext);
}
public void download() {
//读取数据库的线程信息
List<ThreadInfo> threadInfos = mThreadDAO.getThread(mFileInfo.getUrl());
ThreadInfo info;
//如果没有从数据库中拿到信息,我们就直接开始初始化并下载
if (threadInfos.size() == 0) {
//初始化线程信息
info = new ThreadInfo(0, mFileInfo.getUrl(), 0, mFileInfo.getLength(), 0);
} else {
//如果数据库中有信息,拿到第一条数据
info = threadInfos.get(0);
}
//创建子线程进行下载
new DownloadThread(info).start();
}
#
class DownloadThread extends Thread {
private ThreadInfo threadInfo;
public DownloadThread(ThreadInfo threadInfo) {
this.threadInfo = threadInfo;
}
@Override
public void run() {
//如果传过来的线程信息是第一次创建(首次开启下载),直接向数据库插入线程信息
if (!mThreadDAO.isExists(threadInfo.getUrl(), threadInfo.getId())) {
mThreadDAO.insertThread(threadInfo);
}
HttpURLConnection connection;
RandomAccessFile raf;
InputStream is;
//省略try-catch
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(DownloadService.DOWNLOAD_PATH, mFileInfo.getFileName());
raf = new RandomAccessFile(file, "rwd");
raf.seek(start);
//声明一个Intent准备通过广播的形式完成更新进度
Intent intent = new Intent(DownloadService.ACTION_UPDATE);
mFinished += threadInfo.getFinish();
//开始下载
if (connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
//读取数据
is = connection.getInputStream();
byte[] buffer = new byte[1024 * 4];
int len = -1;
long time = System.currentTimeMillis();
while ((len = is.read(buffer)) != -1) {
//下载暂停时,保存进度
if (isPause) {
mThreadDAO.updateThread(mFileInfo.getUrl(), mFileInfo.getId(), mFinished);
return;
}
//写入文件
raf.write(buffer, 0, len);
//把下载进度发送广播给Activity
mFinished += len;
if (System.currentTimeMillis() - time > 1000) {
//减少UI负载
time = System.currentTimeMillis();
intent.putExtra("finished", (int)(mFinished * 100 / mFileInfo.getLength()));
mContext.sendBroadcast(intent);
}
}
intent.putExtra("finished",100);
mContext.sendBroadcast(intent);
//删除线程信息
mThreadDAO.deleteThread(mFileInfo.getUrl(), mFileInfo.getId());
is.close();
}
raf.close();
connection.disconnect();
}
这里的思路也是非常的清晰,通过InitThread获取到的文件信息,简单封装成一个FileInfo对象,然后通过这个FileInfo对象中封装的Url信息来从数据库中查看是否已经存在了下载信息。如果没有新建一个ThreadInfo(简单封了下载进度下载内容的类),如果存在取出这个ThreadInfo。
取出后就交付给下载线程进行下载任务。这里有一个需要注意的地方就是下载进度问题:
//设置下载位置
long start = threadInfo.getStart() + threadInfo.getFinish();
connection.setRequestProperty("Range", "bytes=" + start + "-" + threadInfo.getEnd());
//设置文件写入位置
File file = new File(DownloadService.DOWNLOAD_PATH, mFileInfo.getFileName());
raf = new RandomAccessFile(file, "rwd");
raf.seek(start);
从代码中我们可以看到,threadInfo类中封装了进度,通过它我们可以得到下载的进度,结合RandomAccessFile这个类我们可以完成断点式的文件输入。
我们的进度将通过广播的形式回传给我们的应用组件。
BroadcastReceiver
BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (DownloadService.ACTION_UPDATE.equals(intent.getAction())) {
int finished = intent.getIntExtra("finished", 0);
progressBar.setProgress(finished);
proText.setText(new StringBuffer().append(finished).append("%"));
}
}
};
到这里我们整体的流程就通了一遍,过程非常的简单,没有什么弯弯绕绕在里边。当然还有一些细节性的内容并没有提到:比如暂停时的实现、数据库相关内容…因为这篇博客的篇幅已经足够长了,所以以上没有提到的内容将在下篇博客中展开。
博客链接:http://blog.youkuaiyun.com/wjzj000/article/details/73691908
尾声
最后希望各位看官可以star我的GitHub,三叩九拜,满地打滚求star:
https://github.com/zhiaixinyang/PersonalCollect
https://github.com/zhiaixinyang/MyFirstApp