这两天用了下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