[Android]网络资源下载时断点续传的实现
2012-04-01 15:58
435 查看
断点续传用到的知识点:
1.使用RandomAccessFile设定文件大小并于指定位置开始读数据[randomAccessFile.seek(position)]。
2.请求资源链接时指定所请求数据的返回范围。
httpURLConnection.setRequestProperty("Range", "bytes=" + start + "-" + (contentLength - 1));
效果图如下[CSDN]:
(相当抱歉,这个动画的时间太长了)
以下代码中的NetworkTool为通过个人编程经验封装好的网络工具类,强力推荐,当然也欢迎拍砖。
使用NetworkTool访问一个网络链接并获取数据的小示例为:
代码中的DontPressWithParentButton可用于ListView中,当点击该Button时不会触发ListView中的OnItemClickListener。实现方法为重写Button的setPressed(boolean)方法:
代码如下:
所要添加的权限:
附:HTTP1.1 Range与Content-Range范例说明
假设你要开发一个多线程下载工具,你会自然的想到把文件分割成多个部分,比如4个部分,然后创建4个线程,每个线程负责下载一个部分,如果文件大小为403个byte,那么你的分割方式可以为:0-99 (前100个字节),100-199(第二个100字节),200-299(第三个100字节),300-402(最后103个字节)。
分割完成,每个线程都明白自己的任务,比如线程3的任务是负责下载200-299这部分文件,现在的问题是:线程3发送一个什么样的请求报文,才能够保证只请求文件的200-299字节,而不会干扰其他线程的任务。这时,我们可以使用HTTP1.1的Range头。Range头域可以请求实体的一个或者多个子范围,Range的值为0表示第一个字节,也就是Range计算字节数是从0开始的:
表示头500个字节:Range: bytes=0-499
表示第二个500字节:Range: bytes=500-999
表示最后500个字节:Range: bytes=-500
表示500字节以后的范围:Range: bytes=500-
第一个和最后一个字节:Range: bytes=0-0,-1
同时指定几个范围:Range: bytes=500-600,601-999
所以,线程3发送的请求报文必须有这一行:
Range: bytes=200-299
服务器接收到线程3的请求报文,发现这是一个带有Range头的GET请求,如果一切正常,服务器的响应报文会有下面这行:
HTTP/1.1 206 OK
表示处理请求成功,响应报文还有这一行:
Content-Range: bytes 200-299/403
斜杠后面的403表示文件的大小,通常Content-Range的用法为:
. The first 500 bytes:
Content-Range: bytes 0-499/1234
. The second 500 bytes:
Content-Range: bytes 500-999/1234
. All except for the first 500 bytes:
Content-Range: bytes 500-1233/1234
. The last 500 bytes:
Content-Range: bytes 734-1233/1234
1.使用RandomAccessFile设定文件大小并于指定位置开始读数据[randomAccessFile.seek(position)]。
2.请求资源链接时指定所请求数据的返回范围。
httpURLConnection.setRequestProperty("Range", "bytes=" + start + "-" + (contentLength - 1));
效果图如下[CSDN]:
(相当抱歉,这个动画的时间太长了)
以下代码中的NetworkTool为通过个人编程经验封装好的网络工具类,强力推荐,当然也欢迎拍砖。
使用NetworkTool访问一个网络链接并获取数据的小示例为:
HttpURLConnection httpConn = NetworkTool.openUrl(context, url); int respondCode = NetworkTool.connect(httpConn); if (respondCode == HttpURLConnection.HTTP_OK) { byte[] data = NetworkTool.fetchData_doClose(httpConn); String content = new String(data); data = null; // parse content } else { // handles something } NetworkTool.disconnect(httpConn);
代码中的DontPressWithParentButton可用于ListView中,当点击该Button时不会触发ListView中的OnItemClickListener。实现方法为重写Button的setPressed(boolean)方法:
@Override public void setPressed(boolean pressed) { if (pressed && ((View) getParent()).isPressed()) { return; } super.setPressed(pressed); }
代码如下:
package lab.sodino.downloadbreak; import java.io.File; import java.text.DecimalFormat; import lab.sodino.downloadbreak.bean.BeanDownload; import lab.sodino.downloadbreak.util.LogOut; import lab.sodino.downloadbreak.util.NetworkTool; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.View; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; public class ActDownload extends Activity { /** 下载存放地:"/sdcard/sodino/"。 */ public static final String RES_LOAD_FOLDER = File.separator + "sdcard" + File.separator + "sodino" + File.separator; /** 刷新进度。 */ public static final int REFRESH = 1; public static final int CODE = 10; private BeanDownload bean; private TextView txtName; private TextView txtProgress; private TextView txtSize; private ProgressBar progressBar; private Button btnAction; private Handler handler; private BtnListener btnListener; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.l_download); initBeanDownload(); initViews$Handler(); } private void initBeanDownload() { bean = new BeanDownload(); bean.name = "微信.apk"; // 请找个可以无需跳转直接下载的地址 bean.url = "http://XXOO.com/weixin20android16.apk"; bean.state = BeanDownload.STATE_INTERRUPTED; bean.size = bean.loadedSize = 0l; bean.enable = true; } private void initViews$Handler() { txtName = (TextView) findViewById(R.id.txtName); txtName.setText(bean.name); txtProgress = (TextView) findViewById(R.id.txtProgress); txtProgress.setText(getProgressTxt(bean)); txtSize = (TextView) findViewById(R.id.txtSize); txtSize.setText(formatSizeTxt(bean.size)); progressBar = (ProgressBar) findViewById(R.id.progressBar); progressBar.setProgress(getProgressInt(bean, progressBar.getMax())); btnListener = new BtnListener(); btnAction = (Button) findViewById(R.id.btnAction); btnAction.setOnClickListener(btnListener); btnAction.setText(getTxt(bean)); btnAction.setEnabled(isEnable(bean)); // handler handler = new Handler() { public void handleMessage(Message msg) { txtProgress.setText(getProgressTxt(bean)); txtSize.setText(formatSizeTxt(bean.size)); progressBar.setProgress(getProgressInt(bean, progressBar.getMax())); btnAction.setText(getTxt(bean)); btnAction.setEnabled(isEnable(bean)); } }; } private void pauseDownload() { bean.enable = false; handler.sendEmptyMessage(REFRESH); } private void doDownload() { handler.sendEmptyMessage(REFRESH); new DownloadThread().start(); } private void reloadDownload() { bean.size = bean.loadedSize = 0; bean.enable = true; doDownload(); } private void installDownload() { Intent intent = new Intent(Intent.ACTION_VIEW); String filePath = RES_LOAD_FOLDER + bean.name; intent.setDataAndType(Uri.parse("file://" + filePath), "application/vnd.android.package-archive"); // 如果仅是简单的startActivity(intent),会造成onCreate()再执行一次。 ActDownload.this.startActivityForResult(intent, CODE); } class BtnListener implements Button.OnClickListener { public void onClick(View v) { LogOut.out(this, "state:" + bean.state); switch (bean.state) { case BeanDownload.STATE_LOADING: // 点击了"暂停" pauseDownload(); break; case BeanDownload.STATE_INTERRUPTED: // 点击了"继续" doDownload(); break; case BeanDownload.STATE_DOWNLOAD_FAIL: // 点击了"重载" reloadDownload(); break; case BeanDownload.STATE_COMPLETED: // 点击了"安装" installDownload(); break; } } } class DownloadThread extends Thread { public void run() { bean.state = BeanDownload.STATE_LOADING; bean.enable = true; NetworkTool.download2File(ActDownload.this, bean, handler); LogOut.out(this, "size:" + bean.size + " loaded:" + bean.loadedSize + " enable:" + bean.enable); // 测试“重载”请释放下面代码的注释然后等待下载正常结束 // bean.loadedSize = 0; if (bean.size > 0 && bean.loadedSize == bean.size) { String localPath = RES_LOAD_FOLDER + bean.name; File tmpFile = new File(localPath + ".tmp"); tmpFile.renameTo(new File(localPath)); bean.enable = false; bean.state = BeanDownload.STATE_COMPLETED; } else { if (bean.enable == false) { bean.state = BeanDownload.STATE_INTERRUPTED; } else { bean.state = BeanDownload.STATE_DOWNLOAD_FAIL; } } LogOut.out(this, "state=" + bean.state); handler.sendEmptyMessage(REFRESH); } } public static String getProgressTxt(BeanDownload bean) { String resStr = "0%"; if (bean.size != 0) { double result = bean.loadedSize * 1.0 / bean.size; DecimalFormat decFormat = new DecimalFormat("#.#%"); resStr = decFormat.format(result); } return resStr; } private String formatSizeTxt(long size) { String sizeTxt = "未知"; if (size > 0) { size = size >> 10; sizeTxt = String.valueOf(size) + "k"; } return sizeTxt; } public static int getProgressInt(BeanDownload bean, int max) { int result = (bean.size > 0) ? (int) (bean.loadedSize * max * 1.0 / bean.size) : 0; return result; } private String getTxt(BeanDownload bean) { String txt = "安装"; switch (bean.state) { case BeanDownload.STATE_COMPLETED: txt = "安装"; break; case BeanDownload.STATE_LOADING: txt = "暂停"; break; case BeanDownload.STATE_INTERRUPTED: txt = "继续"; break; case BeanDownload.STATE_DOWNLOAD_FAIL: txt = "重载"; break; } return txt; } private boolean isEnable(BeanDownload bean) { boolean enable = true; if (bean.enable == false && bean.state == BeanDownload.STATE_LOADING) { enable = false; } return enable; } }
package lab.sodino.downloadbreak.bean; /** * @author Sodino E-mail:sodinoopen@hotmail.com * @version Time:2011-6-8 下午11:33:10 */ public class BeanDownload { /** 正在下载数据。Button应显示“暂停”。 */ public static final int STATE_LOADING = 0; /** 数据全部下载完成。Button应显示“安装”。 */ public static final int STATE_COMPLETED = 1; /** 数据下载过程中被暂停。Button应显示“继续”。 */ public static final int STATE_INTERRUPTED = 2; /** 下载安装包失败。Button应显示“失败”。 */ public static final int STATE_DOWNLOAD_FAIL = 3; public String name; public long size; public long loadedSize; public String url; public int state; public boolean enable; }
package lab.sodino.downloadbreak.ui; import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.widget.Button; /** * @author Sodino E-mail:sodinoopen@hotmail.com * @version Time:2011-6-5 下午08:37:27 */ public class DontPressWithParentButton extends Button { public DontPressWithParentButton(Context context, AttributeSet attrs) { super(context, attrs); } @Override public void setPressed(boolean pressed) { if (pressed && ((View) getParent()).isPressed()) { return; } super.setPressed(pressed); } }
package lab.sodino.downloadbreak.util; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.URL; import lab.sodino.downloadbreak.ActDownload; import lab.sodino.downloadbreak.bean.BeanDownload; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Handler; /** * 管理联网操作,包括管理url参数、下载APK包、获取任务字符串。<br/> * * @author Sodino E-mail:sodinoopen@hotmail.com * @version Time:2011-4-6 下午03:42:50 */ public class NetworkTool { /** * 开启一个HTTP链接。 */ public static HttpURLConnection openUrl(Context context, String urlStr) { LogOut.out("Network", "urlStr[" + urlStr + "]"); URL urlURL = null; HttpURLConnection httpConn = null; try { urlURL = new URL(urlStr); // 需要android.permission.ACCESS_NETWORK_STATE // 在没有网络的情况下,返回值为null。 NetworkInfo networkInfo = ((ConnectivityManager) context .getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo(); // 如果是使用的运营商网络 if (networkInfo != null) { if (networkInfo.getType() == ConnectivityManager.TYPE_MOBILE) { // 获取默认代理主机ip String host = android.net.Proxy.getDefaultHost(); // 获取端口 int port = android.net.Proxy.getDefaultPort(); if (host != null && port != -1) { // 封装代理連接主机IP与端口号。 InetSocketAddress inetAddress = new InetSocketAddress(host, port); // 根据URL链接获取代理类型,本链接适用于TYPE.HTTP java.net.Proxy.Type proxyType = java.net.Proxy.Type.valueOf(urlURL .getProtocol().toUpperCase()); java.net.Proxy javaProxy = new java.net.Proxy(proxyType, inetAddress); httpConn = (HttpURLConnection) urlURL.openConnection(javaProxy); } else { httpConn = (HttpURLConnection) urlURL.openConnection(); } } else { httpConn = (HttpURLConnection) urlURL.openConnection(); } httpConn.setDoInput(true); } else { // LogOut.out(this, "No Avaiable Network"); } } catch (NullPointerException npe) { npe.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return httpConn; } /** 启动链接并将RespondCode值返回。 */ public static int connect(HttpURLConnection httpConn) { int code = -1; if (httpConn != null) { try { httpConn.connect(); code = httpConn.getResponseCode(); } catch (IOException e) { e.printStackTrace(); } } LogOut.out("NetworkTool", "respond_code=" + code); return code; } /** * 将指定的HTTP链接内容存储到指定的的文件中。<br/> * 返回值仅当参考。<br/> * * @param httpConn * @param filePath * 指定存储的文件路径。 */ public static boolean download2File(HttpURLConnection httpConn, String filePath) { boolean result = true; File file = new File(filePath); FileOutputStream fos = null; byte[] data = new byte[1024]; int readLength = -1; InputStream is = null; try { fos = new FileOutputStream(file); is = httpConn.getInputStream(); while ((readLength = is.read(data)) != -1) { fos.write(data, 0, readLength); fos.flush(); } fos.flush(); } catch (IOException ie) { result = false; ie.printStackTrace(); } finally { try { if (is != null) { is.close(); } if (fos != null) { fos.close(); } } catch (IOException ie) { ie.printStackTrace(); } } return result; } /** * 将bean资源下载。<br/> * 支持断点续传。 * */ public static void download2File(Context context, BeanDownload bean, Handler handler) { String filePath = ActDownload.RES_LOAD_FOLDER + bean.name + ".tmp"; HttpURLConnection httpConn = null; File file = new File(filePath); RandomAccessFile randomFile = null; FileOutputStream fos = null; int dataBlockLength = 2048; byte[] data = new byte[dataBlockLength]; int readLength = -1; InputStream is = null; try { if (bean.size <= 0) { bean.loadedSize = 0; if (file.getParentFile().exists() == false) { file.getParentFile().mkdirs(); } if (file.exists() == false) { file.createNewFile(); } // 采用普通的下载方式 fos = new FileOutputStream(file); httpConn = openUrl(context, bean.url); int respondCode = connect(httpConn); LogOut.out("NetworkTool", "respondCode=" + respondCode); if (respondCode == HttpURLConnection.HTTP_OK) { bean.size = httpConn.getContentLength(); is = httpConn.getInputStream(); while ((readLength = is.read(data)) != -1 && bean.enable) { fos.write(data, 0, readLength); bean.loadedSize += readLength; handler.sendEmptyMessage(ActDownload.REFRESH); } } } else { // 采用断点续传方式 randomFile = new RandomAccessFile(file, "rw"); randomFile.setLength(bean.size); httpConn = openUrl(context, bean.url); httpConn.setRequestProperty("Range", "bytes=" + bean.loadedSize + "-" + (bean.size - 1)); int respondCode = connect(httpConn); if (respondCode == HttpURLConnection.HTTP_PARTIAL) { is = httpConn.getInputStream(); while ((readLength = is.read(data)) != -1 && bean.enable) { randomFile.seek(bean.loadedSize); randomFile.write(data, 0, readLength); bean.loadedSize += readLength; handler.sendEmptyMessage(ActDownload.REFRESH); } } } } catch (Exception e) { e.printStackTrace(); } finally { try { if (is != null) { is.close(); } if (httpConn != null) { disconnect(httpConn); } if (fos != null) { fos.close(); } if (randomFile != null) { randomFile.close(); } } catch (Exception e) { e.printStackTrace(); } } } /** 读取HttpURLConnection的数据并关闭相关流。 */ public static byte[] fetchData_doClose(HttpURLConnection httpConn) { byte[] data = null; ByteArrayOutputStream baos = null; InputStream is = null; int read = -1; try { baos = new ByteArrayOutputStream(); is = httpConn.getInputStream(); while ((read = is.read()) != -1) { baos.write(read); } data = baos.toByteArray(); } catch (IOException ie) { ie.printStackTrace(); } finally { try { if (is != null) { is.close(); } if (baos != null) { baos.close(); } if (httpConn != null) { httpConn.disconnect(); } } catch (IOException ie) { ie.printStackTrace(); } } return data; } public static void disconnect(HttpURLConnection httpConn) { if (httpConn != null) { httpConn.disconnect(); } } }
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="20dip" android:layout_marginBottom="0dip"> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" android:layout_weight="1" android:layout_marginLeft="10dip" android:layout_marginRight="10dip"> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="10dip" android:layout_marginBottom="10dip" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:id="@+id/txtName" android:singleLine="true" android:ellipsize="middle" android:textColor="#ffffffff" android:textSize="15sp" ></TextView> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/txtProgress" android:textColor="#ffffffff" android:textSize="10sp" android:layout_marginRight="10dip" ></TextView> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/txtSize" android:textColor="#ffffffff" android:textSize="10sp" ></TextView> </LinearLayout> <ProgressBar android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/progressBar" style="?android:attr/progressBarStyleHorizontal" mce_style="?android:attr/progressBarStyleHorizontal" ></ProgressBar> </LinearLayout> <lab.sodino.downloadbreak.ui.DontPressWithParentButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:minWidth="60dip" android:minHeight="30dip" android:id="@+id/btnAction" android:textColor="#ff000000" android:textStyle="bold" android:layout_marginRight="5dip" android:layout_marginLeft="5dip" android:layout_marginTop="10dip" android:layout_marginBottom="10dip" android:focusable="false" android:focusableInTouchMode="false" ></lab.sodino.downloadbreak.ui.DontPressWithParentButton> </LinearLayout>
所要添加的权限:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
附:HTTP1.1 Range与Content-Range范例说明
假设你要开发一个多线程下载工具,你会自然的想到把文件分割成多个部分,比如4个部分,然后创建4个线程,每个线程负责下载一个部分,如果文件大小为403个byte,那么你的分割方式可以为:0-99 (前100个字节),100-199(第二个100字节),200-299(第三个100字节),300-402(最后103个字节)。
分割完成,每个线程都明白自己的任务,比如线程3的任务是负责下载200-299这部分文件,现在的问题是:线程3发送一个什么样的请求报文,才能够保证只请求文件的200-299字节,而不会干扰其他线程的任务。这时,我们可以使用HTTP1.1的Range头。Range头域可以请求实体的一个或者多个子范围,Range的值为0表示第一个字节,也就是Range计算字节数是从0开始的:
表示头500个字节:Range: bytes=0-499
表示第二个500字节:Range: bytes=500-999
表示最后500个字节:Range: bytes=-500
表示500字节以后的范围:Range: bytes=500-
第一个和最后一个字节:Range: bytes=0-0,-1
同时指定几个范围:Range: bytes=500-600,601-999
所以,线程3发送的请求报文必须有这一行:
Range: bytes=200-299
服务器接收到线程3的请求报文,发现这是一个带有Range头的GET请求,如果一切正常,服务器的响应报文会有下面这行:
HTTP/1.1 206 OK
表示处理请求成功,响应报文还有这一行:
Content-Range: bytes 200-299/403
斜杠后面的403表示文件的大小,通常Content-Range的用法为:
. The first 500 bytes:
Content-Range: bytes 0-499/1234
. The second 500 bytes:
Content-Range: bytes 500-999/1234
. All except for the first 500 bytes:
Content-Range: bytes 500-1233/1234
. The last 500 bytes:
Content-Range: bytes 734-1233/1234
相关文章推荐
- [Android]网络资源下载时断点续传的实现
- [Android]网络资源下载时断点续传的实现
- [Android]网络资源下载时断点续传的实现
- [Android]网络资源下载时断点续传的实现
- Android中实现下载URL地址的网络资源的实例分享
- Android网络资源下载时断点续传
- Android——实现网络下载资源
- Android实现断点续传下载文件,网络编程
- Android实现网络多线程断点续传下载
- Android平台下通过HTTP协议实现断点续传下载
- Android学习历程4-Android实现网络多线程断点续传下载
- Android实现网络多线程断点续传下载
- Android文件下载(实现断点续传)
- Unity实现 下载网络资源到本地 WWW加载
- Android使用OKHttp3实现下载(断点续传、显示进度)
- 用JAVA代码实现下载网络上的资源
- Android 实现网络多线程APK文件下载
- Android实现网络多线程断点续传下载
- Android文件下载(实现断点续传)
- Android实现网络多线程断点下载介绍