这两天用了下android5.1下载功能,简直鸡肋。无奈自己研究了半天的断点续传,总结一下。本文参考了慕课网XRay_Chen大神的视频,在此表示衷心的感谢!顺便说明,作者本身还只是个菜鸟,因此我希望能写一篇能让菜鸟看得懂的博客
基础知识
断点续传指从文件上次中断的地方开始传送数据,而并非是从文件开头传送。
因此我们可以从上面一句话得出这样一个结论,如果想要实现断点续传,必须保存断点。于是,我们可以定义这样一个普通类,用来保存文件的网络地址url,断点的位置finished(初始为0),以及结束位置end,简单代码如下
public class ThreadInfo {
private int id;
private String url;
private long end;
private long finished;
public ThreadInfo() {
}
public ThreadInfo(int id, String url, long end, long finished) {
this.id = id;
this.url = url;
this.end = end;
this.finished = finished;
}
//set get方法
其次我们还需要定义一个普通的文件类,保存基本的文件信息,如文件大小,URL,名字等。
//注意,此类实线了Serializable接口,表明可也被序列化,主要是为了在Intent中传送此类
public class FileInfo implements Serializable {
private String name;
private String url;
private long length;
public FileInfo() {
}
public FileInfo(String name, String url, long length) {
this.name = name;
this.url = url;
this.length = length;
}
//set get方法
同样我们需要知道一些简单的http协议的内容
Header
作用
示例
Accept-Ranges
可以请求网页实体的一个或者多个子范围字段
Accept-Ranges: bytes
翻译成大白话来讲就是,你可以通过设置Range来下载文件的一部分内容
注意:其返回的状态码不是200(HttpURLConnection.HTTP_OK),而是206(HttpURLConnection.HTTP_PARTIAL),因此在安卓中我们这样使用这个Header,代码实例
connection = (HttpURLConnection) new URL("Address").openConnection();
connection.setConnectTimeout(5000);
//使用GET
connection.setRequestMethod("GET");
connection.setRequestProperty("Range", "bytes="+start+"-"+end");
//start与end表示你要下载的范围
//判断返回的状态码
if (connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
doSomething();
}
除了安卓的基本使用外,基础知识大概也就这么多.
页面布局
大致就是如下的样子,相当的简单
一个TextView 一个ProgressBar,两个Button
实现过程
定义普通的常量
public class Variable {
public static final String ACTION_START = "action_start";
public static final String ACTION_PAUSE = "action_pause";
public static final String ACTION_UPDATE = "action_update";
public static final String DB_NAME = "download";
public static final String TABLE_NAME = "thread_info";
public static final String URL = "http://openbox.mobilem.360.cn/index/d/sid/2147683";
public static final String PATH = Environment.getExternalStorageDirectory().toString();
}
Service部分
我们的Activity实现的功能是向Service发送下载与暂停请求,Intent需要传递下载文件的对象,此时文件长度临时设置为0,当从网络获取到文件长度是再设置
fileInfo = new FileInfo("csu.apk", Variable.URL, 0);
然后通过按钮设置响应事件,向Service发送下载与暂停的请求
start.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this, DownloadService.class);
//发送Variable.ACTION_START即开始下载请求
intent.setAction(Variable.ACTION_START);
intent.putExtra("file_info", fileInfo);
startService(intent);
}
});
pause.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, DownloadService.class);
//发送Variable.ACTION_PAUSE,即暂停
intent.setAction(Variable.ACTION_PAUSE);
intent.putExtra("file_info", fileInfo);
startService(intent);
}
});
从Service的生命周期可以看出,第一次启动服务时,运行 onCreate()->onStartCommand(),再次启动时只会调用onStartCommand(),因此我们可以重写onStartCommand()
//如何解决开始与暂停,自定义DownloadTask类,之后有具体实现
private DownloadTask downloadTask;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) return super.onStartCommand(null, flags, startId);
//比较Intent.getAction的与Variable.ACTION_START是否一致,请求是否相同
fileInfo = (FileInfo) intent.getSerializableExtra("file_info");
if (intent.getAction().equals(Variable.ACTION_START)) {
//开始下载准备
//下载文件的基本信息
download(fileInfo);//下载文件的基本信息
//下载基本信息之后在下载下载文件内容
} else if (intent.getAction().equals(Variable.ACTION_PAUSE)) {
//暂停下载
if (downloadTask != null) downloadTask.pause();
}
return super.onStartCommand(intent, flags, startId);
}
下载基本的文件信息,与网络有关,开线程下载
private void download(FileInfo fileInfo) {
new InitThread(fileInfo, this).start();
}
private class InitThread extends Thread {
FileInfo fileInfo;
Context context;
public InitThread(FileInfo fileInfo, Context context) {
this.fileInfo = fileInfo;
this.context = context;
}
@Override
public void run() {
HttpURLConnection connection;
//RandomAccessFile利用文件指针,可以从给定的位置开始读写数据
RandomAccessFile raf;
try {
connection = (HttpURLConnection) new URL(fileInfo.getUrl()).openConnection();
connection.setConnectTimeout(3000);
//获取文件文件长度
long length = -1;
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK)
length = connection.getContentLength();
if (length <= 0) return;
//在SD创建一个与带下载文件相同大小的文件
File file = new File(Variable.PATH, fileInfo.getName());
raf = new RandomAccessFile(file, "rwd");
raf.setLength(length);
//设置文件长度
fileInfo.setLength(length);
//向handler发送开始下载的请求,下载开始
handler.obtainMessage(MSG_INT, fileInfo).sendToTarget();
} catch (IOException e) {
e.printStackTrace();
}
super.run();
}
}
Service中定义handler开始下载
private Handler handler = new Handler() {
@Override
//处理发送消息
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == MSG_INT) {
FileInfo fileInfo = (FileInfo) msg.obj;
//开始下载fileInfo
downloadTask = new DownloadTask(DownloadService.this, fileInfo);
downloadTask.download();
}
}
};
恩,现在Service已经实现.
DownloadTask部分
主要实现文件的下载与暂停的控制,以及定时的发给主线程完成进度的消息.
下载/暂停部分
这一部分涉及到断点类ThreadInfo的基本操作,由于中断,所以需要我们及时的把当前的断点保存起来,这里选择使用数据库,当然你也可以选择其他方式,在此不在赘述数据库的实现,只是提供一个基本的接口
public interface ThreadDB {
public void insert(ThreadInfo info);
public void update(int id, String url, long finished);
public void delete(int id, String url);
public List<ThreadInfo> getThreadInfo(String url);
public boolean isExists(String url, int id);
}
下载/暂停的基本实现
public class DownloadTask {
private Context context;
private FileInfo fileInfo;
//数据库的操作
private ThreadDB impl;
//已完成
private long finished = 0;
//是否暂停
public boolean isPause = false;
private ThreadInfo thread_info;
public DownloadTask(Context context, FileInfo fileInfo) {
this.context = context;
this.fileInfo = fileInfo;
impl = new ThreadDBImpl(context);
}
public void download() {
isPause = false;
List<ThreadInfo> thread_infos = impl.getThreadInfo(fileInfo.getUrl());
//从数据库获取断点线程信息
if (thread_infos.size() == 0)
thread_info = new ThreadInfo(0, fileInfo.getUrl(), fileInfo.getLength(), 0);
else
thread_info = thread_infos.get(0);
//下载,仍旧开线程
new DownloadThread(thread_info).start();
}
//暂停方法
public void pause() {
isPause = true;
}
//下载线程
}
下载线程私有类实现
private class DownloadThread extends Thread {
ThreadInfo thread_info;
Intent intent;
public DownloadThread(ThreadInfo thread_info) {
//从数据库活取的最新断点信息
this.thread_info = thread_info;
//发送广播的intent
intent = new Intent(Variable.ACTION_UPDATE);
}
@Override
public void run() {
if (!impl.isExists(thread_info.getUrl(), thread_info.getId()))
impl.insert(thread_info);
HttpURLConnection connection = null;
RandomAccessFile raf = null;
InputStream input = null;
try {
connection = (HttpURLConnection) new URL(thread_info.getUrl()).openConnection();
connection.setConnectTimeout(5000);
connection.setRequestMethod("GET");
//获取start与end
long start = thread_info.getFinished();
long end = thread_info.getEnd();
//http获取部分内容
connection.setRequestProperty("Range", "bytes=" + start + "-" + end);
File file = new File(Variable.PATH, fileInfo.getName());
raf = new RandomAccessFile(file, "rwd");
raf.seek(start);
finished = thread_info.getFinished();
if (connection.getResponseCode() == HttpStatus.SC_PARTIAL_CONTENT) {
input = connection.getInputStream();
byte[] buffer = new byte[1024 * 4];
int len = -1;
long time = System.currentTimeMillis();
while ((len = input.read(buffer)) != -1) {
//写入数据
raf.write(buffer, 0, len);
finished += len;
//当前进度
long progress = finished * 100 / fileInfo.getLength();
//定时发送广播
...
...
//判断是否停止下载
if (isPause) {
//停止下载,保存当前最新断点信息
impl.update(thread_info.getId(), thread_info.getUrl(), finished);
return;
}
}
//下载完成后,删除数据库中的信息
impl.delete(thread_info.getId(), thread_info.getUrl());
}
} catch (IOException ignored) {
} finally {
try {
if (connection != null) connection.disconnect();
if (raf != null) raf.close();
if (input != null) input.close();
} catch (Exception ignored) {
}
}
super.run();
}
}
广播部分
发送广播
//每隔500毫秒发送进度,避免发送频繁导致UI线程崩溃
if (System.currentTimeMillis() - time > 500 || progress == 100) {
time = System.currentTimeMillis();
intent.putExtra("value", progress);
context.sendBroadcast(intent);
}
Activity部分
Activity部分除了控制UI还有接受广播,更新下载进度
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//省略UI代码
//定义IntentFilter,注册广播
IntentFilter filter = new IntentFilter();
filter.addAction(Variable.ACTION_UPDATE);
registerReceiver(receiver, filter);
}
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Variable.ACTION_UPDATE)) {
//当接收到广播,更新进度
long finished = intent.getLongExtra("value", 0);
progressBar.setProgress((int) finished);
}
}
};
@Override
protected void onDestroy() {
super.onDestroy();
//
unregisterReceiver(receiver);
}
总结
终于写完了,由于篇幅过长,在此,总结一下
大致过程
当然,楼主也提供了响应的源代码 github
2015年5月24日 于418