android学习笔记----多线程断点续传下载原理设计
目录
用java实现多线程下载:
用android实现多线程下载(HttpURLConnection):
用android实现多线程下载(OkHttp):
android实现(HttpURLConnection)的Demo源码:https://github.com/liuchenyang0515/MultithreadBreakpointDowload
android实现(OkHttp)的Demo源码(推荐):https://github.com/liuchenyang0515/MultithreadBreakpointDowload1
下载原理:
用java实现多线程下载:
先把tomcat服务器开起来,然后在webapps/ROOT/目录下放abc.exe供下载测试
先来段java实现的代码:
import java.io.BufferedReader;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.RandomAccessFile;import java.net.HttpURLConnection;import java.net.MalformedURLException;import java.net.ProtocolException;import java.net.URL;public class MultiDownload { // 定一下载路径 private static final String path = "http://192.168.164.1:8080/abc.exe"; private static final int threadCount = 3; // 假设开3个线程 private static int runningThread; // 代表当前正在运行的线程 public static void main(String[] args) { RandomAccessFile rafAccessFile = null; // 获取服务器文件的大小 try { HttpURLConnection conn = connectNetSettings(); int code = conn.getResponseCode(); if (code == 200) { // 获取服务器文件的大小 int length = conn.getContentLength(); // 把线程的数量赋值给正在运行的线程 runningThread = threadCount; rafAccessFile = new RandomAccessFile(getFileName(path), "rw"); // 创建一个和服务器大小一样的的文件,提前申请好空间 rafAccessFile.setLength(length); rafAccessFile.close(); int blockSize = length / threadCount; // 计算每个线程下载的开始位置和结束位置 for (int i = 0; i < threadCount; ++i) { int startIndex = i * blockSize; // 每个线程下载的开始位置 int endIndex; // 每个线程下载的结束位置 if (i == threadCount - 1) { // 如果是最后一个线程 endIndex = length - 1; } else { endIndex = (i + 1) * blockSize - 1; } System.out.println("线程id:" + i + "理论下载的位置" + startIndex + "=========" + endIndex); // 四 开启线程去服务器下载文件 DownLoadThread downLoadThread = new DownLoadThread(startIndex, endIndex, i); downLoadThread.start(); } } } catch (Exception e) { e.printStackTrace(); } finally { try { rafAccessFile.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } private static HttpURLConnection connectNetSettings() throws MalformedURLException, IOException, ProtocolException { URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); return conn; } // 定义线程去服务器下载文件 private static class DownLoadThread extends Thread { // 通过构造方法把每个线程下载的开始和结束位置传进来 private int startIndex; private int endIndex; private int threadId; public DownLoadThread(int startIndex, int endIndex, int threadId) { this.startIndex = startIndex; this.endIndex = endIndex; this.threadId = threadId; } public void close(T t) { try { if (t != null) { t.close(); } } catch (Exception e) { e.printStackTrace(); } } @Override public void run() { InputStream in = null; RandomAccessFile raf = null; BufferedReader br = null; RandomAccessFile raff = null; RandomAccessFile breakpoint = null; try { HttpURLConnection conn = connectNetSettings(); File file = new File(threadId + ".txt"); if (file.exists() && file.length() > 0) { br = new BufferedReader(new InputStreamReader(new FileInputStream(file))); String lastPosition = br.readLine(); // 读出来的内容就是上次下载保存的位置 int last = Integer.parseInt(lastPosition); // 要改变一下startIndex位置 startIndex = last; System.out.println("线程id:" + threadId + "真实下载的位置" + startIndex + "=========" + endIndex); br.close(); } // 设置一个请求头Range,作用是告诉服务器每个线程下载的开始和结束位置 // 固定写法 conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex); int code = conn.getResponseCode(); // 206代表部分资源请求成功,200表示请求全部资源成功 if (code == 206) { // 创建随机读写文件对象 raf = new RandomAccessFile(getFileName(path), "rw"); // 每个线程要从自己的位置开始写 raf.seek(startIndex); // 存的是abc.exe in = conn.getInputStream(); // 把数据写到文件中 int len = -1; byte[] buffer = new byte[1024]; int total = 0; // 代表当前线程下载的大小 // 下面这句不要写在while里面,避免重复关联文件导致文件无法删除 raff = new RandomAccessFile(threadId + ".txt", "rwd");// 关联文件时,文件指针初始为0的位置 while ((len = in.read(buffer)) != -1) { raf.write(buffer, 0, len); total += len; // 实现断点续传,就是把当前线程下载的位置存起来 // 下次再下载的时候,就是按照上次下载的位置继续下载就行 int currentThreadPosition = startIndex + total; // 用FileOuputStream可能因为突然停止导致不能立即写到硬盘 raff.writeBytes(String.valueOf(currentThreadPosition)); raff.seek(0); // 记录断点的txt文件需要每次从头开始写而不是续写,默认从文件指针处继续写 } raff.close(); raf.close(); in.close(); System.out.println("线程id:" + threadId + "下载完成"); synchronized (DownLoadThread.class) { breakpoint = new RandomAccessFile("time.txt", "rwd"); breakpoint.seek(0); // 准备从time.txt开头读取未下载完成的线程个数 String s = null; if ((s = breakpoint.readLine()) != null) {// 读取剩余的需要下载的线程个数 runningThread = Integer.valueOf(s); } --runningThread; breakpoint.seek(0); // 尝试读取后文件指针变化,再设置为0,从0处开始写入 breakpoint.write(String.valueOf(runningThread).getBytes()); breakpoint.close(); if (runningThread == 0) { for (int i = 0; i < threadCount; ++i) { File deleteFile = new File(i + ".txt"); System.out.println(deleteFile.toString()); deleteFile.delete(); } new File("time.txt").delete(); } } } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { close(breakpoint); close(raff); close(raf); close(in); close(br); } } } public static String getFileName(String path) { int index = path.lastIndexOf("/") + 1; return path.substring(index); }}
假如断点特殊情况,断的很巧妙,一个线程下载完了别的线程还没下载完,下次再开始下载的时候,runningThread又被初始化为3个,其他2个线程下载完后runningThread=1不为0,这样就导致删除不了txt文件。
方法:同样将还没下载完成的线程个数写到文件中
想要达到上面效果,必须这么处理:
synchronized (DownLoadThread.class) { breakpoint = new RandomAccessFile("time.txt", "rwd"); breakpoint.seek(0); // 准备从time.txt开头读取未下载完成的线程个数 String s = null; if ((s = breakpoint.readLine()) != null) { // 读取剩余的需要下载的线程个数 runningThread = Integer.valueOf(s); } --runningThread; breakpoint.seek(0); // 尝试读取后文件指针变化,再设置为0,从0处开始写入 breakpoint.write(String.valueOf(runningThread).getBytes()); breakpoint.close(); if (runningThread == 0) { for (int i = 0; i < threadCount; ++i) { File deleteFile = new File(i + ".txt"); System.out.println(deleteFile.toString()); deleteFile.delete(); } new File("time.txt").delete(); }}
笔记批注:
流处理我尝试关闭了2次,第一次是因为想尽早关闭,减少占用资源消耗,第二次是在finally{...},是想尽量确保所有的流能关闭。
有几个线程就把资源大小除以几,除不尽的就让最后一个线程多下载一点,这就是为什么我们经常用迅雷下载的时候明明到了99%却最后下载的越来越慢,因为别的线程都下载完了,还在等待最后一个线程下载。
setRequestProperty是HttpURLConnection继承的URLConnection中的方法。
public void setRequestProperty(String key, String value)
设置一般请求属性。 如果具有密钥的属性已存在,则使用新值覆盖其值。
注意:HTTP需要所有请求属性,它们可以合法地使用相同键的多个实例来使用逗号分隔的列表语法,这样可以将多个属性附加到单个属性中。
参数
key
- 请求已知的关键字(例如,“ Accept
”)。
value
- value
的值。
异常
IllegalStateException
- 如果已经连接
NullPointerException
- 如果键是 null
另请参见:
getRequestProperty(java.lang.String)
用android实现多线程下载(HttpURLConnection):
android的demo目录如下:
因为是模拟器,所以这里使用了SD卡,并没有判断SD卡是否存在
如果需要做的更加完善,需要
判断SD卡是否存在
下载前要判断手机网络类型,是在wifi情况下载还是蜂窝移动数据下载
下载前需要扫描手机是否有病毒等等......
这里没有实现那么多,主要为了实现多线程现在和断点续传的功能。
MainActivity.java
package com.example.multi_threadbreakpointdowload;import android.Manifest;import android.content.pm.PackageManager;import android.os.Bundle;import android.os.Environment;import android.support.annotation.NonNull;import android.support.v4.app.ActivityCompat;import android.support.v4.content.ContextCompat;import android.support.v7.app.AppCompatActivity;import android.view.LayoutInflater;import android.view.View;import android.widget.EditText;import android.widget.LinearLayout;import android.widget.ProgressBar;import android.widget.Toast;import java.io.File;import java.io.IOException;import java.io.RandomAccessFile;import java.net.HttpURLConnection;import java.util.ArrayList;import java.util.List;import static com.example.multi_threadbreakpointdowload.ConnectionUtils.close;public class MainActivity extends AppCompatActivity { private LinearLayout ll_pb_layout; private EditText et_threadCount; private EditText et_path; private String path; private int runningThread; private int threadCount; private List pbLists; // 用来存进度条的引用 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); et_path = (EditText) findViewById(R.id.et_path); et_threadCount = (EditText) findViewById(R.id.et_threadCount); ll_pb_layout = (LinearLayout) findViewById(R.id.ll_pb); // 添加一个集合,用来存进度条的引用 pbLists = new ArrayList(); } // 点击按钮实现下载的逻辑 public void onclick(View v) throws IOException { if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); } else { switch (v.getId()) { case R.id.btn_01: runDownLoad(); break; case R.id.btn_02: clearReady(); runDownLoad(); break; } } } private void clearReady() { for (int i = 0; i < threadCount; ++i) { File deleteFile = new File(Environment.getExternalStorageDirectory().getPath() + "/" + i + ".txt"); deleteFile.delete(); } new File(Environment.getExternalStorageDirectory().getPath() + "/time.txt").delete(); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case 1: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { runDownLoad(); } else { Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show(); } break; } } private void runDownLoad() { // 获取下载的路径 path = et_path.getText().toString().trim(); // 获取线程的数量 threadCount = Integer.parseInt(et_threadCount.getText().toString().trim()); // 先移除上次进度条再添加 ll_pb_layout.removeAllViews(); pbLists.clear(); for (int i = 0; i < threadCount; ++i) { // 把定义的item布局转换成一个View对象 // item布局的父布局是ll_pb_layout对象对应的布局,然后false就是这个view按照子布局item的形式来 ProgressBar pbView = (ProgressBar) LayoutInflater.from(MainActivity.this).inflate(R.layout.item, ll_pb_layout, false); // 把pbView添加到集合中 pbLists.add(pbView); // 动态添加进度条 ll_pb_layout.addView(pbView); } new Thread() { @Override public void run() { RandomAccessFile rafAccessFile = null; // 获取服务器文件的大小 try { HttpURLConnection conn = ConnectionUtils.connectNetSettings(path); int code = conn.getResponseCode(); if (code == 200) { // 获取服务器文件的大小 int length = conn.getContentLength(); // 把线程的数量赋值给正在运行的线程 runningThread = threadCount; rafAccessFile = new RandomAccessFile(ConnectionUtils.getFileName(path), "rw"); // 创建一个和服务器大小一样的的文件,提前申请好空间 rafAccessFile.setLength(length); rafAccessFile.close(); int blockSize = length / threadCount; // 计算每个线程下载的开始位置和结束位置 for (int i = 0; i < threadCount; ++i) { int startIndex = i * blockSize; // 每个线程下载的开始位置 int endIndex; // 每个线程下载的结束位置 if (i == threadCount - 1) { // 如果是最后一个线程 endIndex = length - 1; } else { endIndex = (i + 1) * blockSize - 1; } System.out.println("线程id:" + i + "理论下载的位置" + startIndex + "=========" + endIndex); // 四 开启线程去服务器下载文件 DownLoadThread downLoadThread = new DownLoadThread(startIndex, endIndex, i, path, pbLists, runningThread, threadCount); downLoadThread.start(); } } } catch (Exception e) { e.printStackTrace(); } finally { close(rafAccessFile); } } }.start(); }}
DownLoadThread.java
package com.example.multi_threadbreakpointdowload;import android.os.Environment;import android.widget.ProgressBar;import java.io.BufferedReader;import java.io.File;import java.io.FileInputStream;import java.io.InputStream;import java.io.InputStreamReader;import java.io.RandomAccessFile;import java.net.HttpURLConnection;import java.util.List;import static com.example.multi_threadbreakpointdowload.ConnectionUtils.close;// 定义线程去服务器下载文件public class DownLoadThread extends Thread { // 通过构造方法把每个线程下载的开始和结束位置传进来 private int startIndex; private int endIndex; private int threadId; private String path; private int PbMaxSize; // 代表当前线程下载的最大值 private int pblastPositon; // 如果中断过,获取上次下载的位置 private List pbLists; // 用来存进度条的引用 private int runningThread; private int threadCount; public DownLoadThread(int startIndex, int endIndex, int threadId, String path, List pbLists, int runningThread, int threadCount) { this.startIndex = startIndex; this.endIndex = endIndex; this.threadId = threadId; this.path = path; this.pbLists = pbLists; this.runningThread = runningThread; this.threadCount = threadCount; } @Override public void run() { InputStream in = null; RandomAccessFile raf = null; BufferedReader br = null; RandomAccessFile raff = null; RandomAccessFile breakpoint = null; try { // 计算当前进度条的最大值 PbMaxSize = endIndex - startIndex; HttpURLConnection conn = ConnectionUtils.connectNetSettings(path); File file = new File(Environment.getExternalStorageDirectory().getPath() + "/" + threadId + ".txt"); if (file.exists() && file.length() > 0) { br = new BufferedReader(new InputStreamReader(new FileInputStream(file))); String lastPosition = br.readLine(); // 读出来的内容就是上次下载保存的位置 int last = Integer.parseInt(lastPosition); // 给我们定义的进度条位置赋值 pblastPositon = last - startIndex; // 要改变一下startIndex位置 startIndex = last; System.out.println("线程id:" + threadId + "真实下载的位置" + startIndex + "=========" + endIndex); br.close(); } // 设置一个请求头Range,作用是告诉服务器每个线程下载的开始和结束位置 // 固定写法 conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex); int code = conn.getResponseCode(); // 206代表部分资源请求成功,200表示请求全部资源成功 if (code == 206) { // 创建随机读写文件对象 raf = new RandomAccessFile(ConnectionUtils.getFileName(path), "rw"); // 每个线程要从自己的位置开始写 raf.seek(startIndex); // 存的是abc.exe in = conn.getInputStream(); // 把数据写到文件中 int len = -1; byte[] buffer = new byte[1024 * 1024]; int total = 0; // 代表当前线程下载的大小 // 下面这句不要写在while里面,避免重复关联文件导致文件无法删除 raff = new RandomAccessFile(Environment.getExternalStorageDirectory().getPath() + "/" + threadId + ".txt", "rwd"); while ((len = in.read(buffer)) != -1) { raf.write(buffer, 0, len); total += len; // 实现断点续传,就是把当前线程下载的位置存起来 // 下次再下载的时候,就是按照上次下载的位置继续下载就行 int currentThreadPosition = startIndex + total; // 用FileOuputStream可能因为突然停止导致不能立即写到硬盘 raff.writeBytes(String.valueOf(currentThreadPosition)); raff.seek(0);// 避免每次写数据不断往后添加 // 设置当前进度条的最大值和当前进度 pbLists.get(threadId).setMax(PbMaxSize); // 设置进度条的最大值 pbLists.get(threadId).setProgress(pblastPositon + total); // 设置当前进度条的当前进度 } raff.close(); raf.close(); in.close(); System.out.println("线程id:" + threadId + "下载完成"); synchronized (DownLoadThread.class) { breakpoint = new RandomAccessFile(Environment.getExternalStorageDirectory().getPath() + "/time.txt", "rwd"); breakpoint.seek(0); // 准备从time.txt开头读取未下载完成的线程个数 String s = null; if ((s = breakpoint.readLine()) != null) { runningThread = Integer.valueOf(s); } --runningThread; breakpoint.seek(0); // 尝试读取后文件指针变化,再设置为0,从0处开始写入 breakpoint.write(String.valueOf(runningThread).getBytes()); breakpoint.close(); if (runningThread == 0) { for (int i = 0; i < threadCount; ++i) { File deleteFile = new File(Environment.getExternalStorageDirectory().getPath() + "/" + i + ".txt"); System.out.println(deleteFile.toString()); deleteFile.delete(); } new File(Environment.getExternalStorageDirectory().getPath() + "/time.txt").delete(); } } } } catch (Exception e) { e.printStackTrace(); } finally { close(raff); close(raf); close(in); close(br); } }}
ConnectionUtils.java
package com.example.multi_threadbreakpointdowload;import android.os.Environment;import java.net.HttpURLConnection;import java.net.URL;public class ConnectionUtils { static HttpURLConnection connectNetSettings(String path) throws Exception { URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); return conn; } static String getFileName(String path) { int index = path.lastIndexOf("/") + 1; return Environment.getExternalStorageDirectory().getPath() + "/" + path.substring(index); } static void close(T t) { try { if (t != null) { t.close(); } } catch (Exception e) { e.printStackTrace(); } }}
运行结果如下:
出现断点时,断点下载也测试成功,进度条也从断点开始加载显示
当然为了应对极度变态的断电情况出现的,所有线程都执行完了,准备去删除txt文件的时候没有执行完,导致还剩余txt文件,下次再下载的时候就会出问题,所以添加了“重新下载”按钮,就把txt文件全部删掉再开始下载。
用android实现多线程下载(OkHttp):
由于篇幅原因,OkHttp实现的直接放在github,和用HttpURLConnection实现的效果完全相同
地址https://github.com/liuchenyang0515/MultithreadBreakpointDowload1
===========================Talk is cheap, show me the code=========================
转载于:https://www.cnblogs.com/lcy0515/p/10807874.html
更多相关文章
- 从J2EE转向Android的第九天-----文件存储
- Android线程间通信的Message机制
- Android的线程使用来更新UI----View的几种更新方法(Thread、Hand
- Android笔记四 虚拟机Dalvik、Android各种java包功能、Android相
- android保存手势操作到文件&读取识别手势
- Android之子线程更新UI