这篇文章主要介绍了 Android 断点续传原理以及实现的相关资料, 这里对断点续传原理进行了详细介绍,需要的朋友可以参考下
Android 是一种基于 Linux 的自由及开放源代码的操作系统,主要使用于移动设备,如智能手机和平板电脑,由 Google 公司和开放手机联盟领导及开发。尚未有统一中文名称,中国大陆地区较多人使用 "安卓" 或 "安致"。
Android 断点续传原理以及实现
0. 前言
在 Android 开发中,断点续传听起来挺容易,在下载一个文件时点击暂停任务暂停,点击开始会继续下载文件。但是真正实现起来知识点还是蛮多的,因此今天有时间实现了一下,并进行记录。
1. 断点续传原理
在本地下载过程中要使用数据库实时存储到底存储到文件的哪个位置了,这样点击开始继续传递时,才能通过 HTTP 的 GET 请求中的 setRequestProperty() 方法可以告诉服务器,数据从哪里开始,到哪里结束。同时在本地的文件写入时,RandomAccessFile 的 seek() 方法也支持在文件中的任意位置进行写入操作。同时通过广播将子线程的进度告诉 Activity 的 ProcessBar。
2. Activity 的按钮响应
当点击开始按钮时,将 url 写在了 FileInfo 类的对象 info 中并通过 Intent 从 Activity 传递到了 Service 中。这里使用 setAction() 来区分是开始按钮还是暂停按钮。
- public class FileInfo implements Serializable{
- private String url; //URL
- private int length; //长度或结束位置
- private int start; //开始位置
- private int now;//当前进度
- //构造方法,set/get略
- }
- //开始按钮逻辑,停止逻辑大致相同
- strat.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- Intent intent = new Intent(MainActivity.this,DownLoadService.class);
- intent.setAction(DownLoadService.ACTION_START);
- intent.putExtra("fileUrl",info);
- startService(intent);
- }
- });
3. 在 Service 中的子线程中获取文件大小
在 Service 中的 onStartCommand() 中,将 FileInfo 对象从 Intent 中取出,如果是开始命令,则开启一个线程,根据该 url 去获得要下载文件的大小,将该大小写入对象并通过 Handler 传回 Service,同时在本地创建一个相同大小的本地文件。暂停命令最后会讲到。
- public void run() {
- HttpURLConnection urlConnection = null;
- RandomAccessFile randomFile = null;
- try {
- URL url = new URL(fileInfo.getUrl());
- urlConnection = (HttpURLConnection) url.openConnection();
- urlConnection.setConnectTimeout(3000);
- urlConnection.setRequestMethod("GET");
- int length = -1;
- if (urlConnection.getResponseCode() == HttpStatus.SC_OK) {
- //获得文件长度
- length = urlConnection.getContentLength();
- }
- if (length <= 0) {
- return;
- }
- //创建相同大小的本地文件
- File dir = new File(DOWNLOAD_PATH);
- if (!dir.exists()) {
- dir.mkdir();
- }
- File file = new File(dir, FILE_NAME);
- randomFile = new RandomAccessFile(file, "rwd");
- randomFile.setLength(length);
- //长度给fileInfo对象
- fileInfo.setLength(length);
- //通过Handler将对象传递给Service
- mHandle.obtainMessage(0, fileInfo).sendToTarget();
- } catch (Exception e) {
- e.printStackTrace();
- } finally { //流的回收逻辑略
- }
- }
- }
4. 数据库操作封装
在 Service 的 handleMessage() 方法中拿到有 length 属性的 FileInfo 对象,并使用自定义的 DownLoadUtil 类进行具体的文件下载逻辑。这里传入上下文,因为数据库处理操作需要用到。
- downLoadUtil = new DownLoadUtil(DownLoadService.this,info);
- downLoadUtil.download();
这里有一个数据库操作的接口 ThreadDAO,内部有增删改查等逻辑,用于记录下载任务的信息。自定义一个 ThreadDAOImpl 类将这里的逻辑实现,内部数据库创建关于继承 SQLiteOpenHelper 的自定义类的逻辑就不贴了,比较简单,该类会在 ThreadDAOImpl 类的构造方法中创建实例。完成底层数据库操作的封装。
- public interface ThreadDAO {
- //插入一条数据
- public void insert(FileInfo info);
- //根据URL删除一条数据
- public void delete(String url);
- //根据URL更新一条进度
- public void update(String url,int finished);
- //根据URL找到一条数据
- public List<FileInfo> get(String url);
- //是否存在
- public boolean isExits(String url);
- }
5. 具体的文件下载逻辑
- public class DownLoadUtil {
- //构造方法略
- public void download(){
- List<FileInfo> lists = threadDAO.get(fileInfo.getUrl());
- FileInfo info = null;
- if(lists.size() == 0){
- //第一次下载,创建子线程下载
- new MyThread(fileInfo).start();
- }else{
- //中间开始的
- info = lists.get(0);
- new MyThread(info).start();
- }
- }
- class MyThread extends Thread{
- private FileInfo info = null;
- public MyThread(FileInfo threadInfo) {
- this.info = threadInfo;
- }
- @Override
- public void run() {
- //向数据库添加线程信息
- if(!threadDAO.isExits(info.getUrl())){
- threadDAO.insert(info);
- }
- HttpURLConnection urlConnection = null;
- RandomAccessFile randomFile =null;
- InputStream inputStream = null;
- try {
- URL url = new URL(info.getUrl());
- urlConnection = (HttpURLConnection) url.openConnection();
- urlConnection.setConnectTimeout(3000);
- urlConnection.setRequestMethod("GET");
- //设置下载位置
- int start = info.getStart() + info.getNow();
- urlConnection.setRequestProperty("Range","bytes=" + start + "-" + info.getLength());
- //设置文件写入位置
- File file = new File(DOWNLOAD_PATH,FILE_NAME);
- randomFile = new RandomAccessFile(file, "rwd");
- randomFile.seek(start);
- //向Activity发广播
- Intent intent = new Intent(ACTION_UPDATE);
- finished += info.getNow();
- if (urlConnection.getResponseCode() == HttpStatus.SC_PARTIAL_CONTENT) {
- //获得文件流
- inputStream = urlConnection.getInputStream();
- byte[] buffer = new byte[512];
- int len = -1;
- long time = System.currentTimeMillis();
- while ((len = inputStream.read(buffer))!= -1){
- //写入文件
- randomFile.write(buffer,0,len);
- //把进度发送给Activity
- finished += len;
- //看时间间隔,时间间隔大于500ms再发
- if(System.currentTimeMillis() - time >500){
- time = System.currentTimeMillis();
- intent.putExtra("now",finished *100 /fileInfo.getLength());
- context.sendBroadcast(intent);
- }
- //判断是否是暂停状态
- if(isPause){
- threadDAO.update(info.getUrl(),finished);
- return; //结束循环
- }
- }
- //删除线程信息
- threadDAO.delete(info.getUrl());
- }
- }catch (Exception e){
- e.printStackTrace();
- }finally {//回收工作略
- }
- }
- }
- }
上面也讲到使用自定义的 DownLoadUtil 类进行具体的文件下载逻辑,这也是最关键的部分了,在该类的构造方法中进行 ThreadDAOImpl 实例的创建。并在 download() 中通过数据库查询的操作,判断是否是第一次开始下载任务,如果是,则开启一个子线程 MyThread 进行下载任务,否则将进度信息从数据库中取出,并将该信息传递给 MyThread。
在 MyThread 中,通过 info.getStart() + info.getNow() 设置开始下载的位置,如果是第一次下载两个数将都是 0,如果是暂停后再下载,则 info.getNow() 会取出非 0 值,该值来自数据库存储。使用 setRequestProperty 告知服务器从哪里开始传递数据,传递到哪里结束,本地使用 RandomAccessFile 的 seek() 方法进行数据的本地存储。使用广播将进度的百分比传递给 Activity,Activity 再改变 ProcessBar 进行 UI 调整。
这里很关键的一点是在用户点击暂停后会在 Service 中调用 downLoadUtil.isPause = true,因此上面 while 循环会结束,停止下载并通过数据库的 update() 保存进度值。从而在续传时取出该值,重新对服务器发起文件起始点的下载任务请求,同时也在本地文件的相应位置继续写入操作。
6. 效果如下所示
来源: http://www.phperz.com/article/17/0313/314551.html