断点多线程下载的几个关键点:①:得到要下载的文件大小后,均分给几个线程。②:使用RandomAccessFile类进行读写,可以指定开始写入的位置。③:数据库保存下载信息,下一次继续下载的时候从数据库取出数据,然后从上次下载结束的地方开始。
这里我使用了FinalDb的数据库框架,同时在内存中存储了一份所有线程的下载信息,负责时时更新和查询下载进度。我测试用的是百度云网盘,文件大小50M左右。注意,线程中指定了开始结束下载位置的网络请求成功的返回码是206,并不是200。
效果图:
线程类:线程类负责具体的下载,线程的下载信息被存储到了数据库。线程开始下载时,根据线程ID查询自己的存储信息,然后开始从指定的位置下载和写入文件。完毕后根据自己的当前下载结果设置自己当前的下载状态。时时的下载进度存储只存储到了内存,只在本次下载结束才存储到数据库。
public class DownloadThread extends Thread { /** * 数据库操作工具 */ private FinalDb finalDb; /** * 下载状态:未开始 */ public static final int STATE_READY = 1; /** * 下载状态:下载中 */ public static final int STATE_LOADING = 2; /** * 下载状态:下载暂停中 */ public static final int STATE_PAUSING = 3; /** * 下载状态:下载完成 */ public static final int STATE_FINISH = 4; /** * 下载状态 */ public int downloadState; /** * 线程ID */ private int threadID; /** * 要下载的URL路径 */ private String url; /** * 本线程要下载的文件 */ public RandomAccessFile file; /** * 构造器 */ public DownloadThread(Context context, int threadID, String downloadUrl, RandomAccessFile randomAccessFile) { this.threadID = threadID; this.url = downloadUrl; this.file = randomAccessFile; finalDb = DBUtil.getFinalDb(context); } @Override public void run() { //数据库查询本线程下载进度 Listlist = finalDb.findAllByWhere(ThreadDownloadInfoBean.class, "threadID='" + threadID + "'"); //下载信息存放到内存 if (list.get(0) != null) { MapUtil.map.put(threadID, list.get(0)); } //取出实体类 ThreadDownloadInfoBean bean = MapUtil.map.get(threadID); Utils.Print("bean:" + bean.toString()); InputStream is; HttpURLConnection conn; try { Utils.Print("线程" + threadID + "开始连接"); cOnn= (HttpURLConnection) new URL(url).openConnection(); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); conn.setRequestMethod("GET"); //设置下载开始和结束的位置 conn.setRequestProperty("Range", "bytes=" + (bean.startDownloadPosition + bean.downloadedSize) + "-" + bean.endDownloadPosition); conn.connect(); if (conn.getResponseCode() == 206) { //更改下载状态 downloadState = STATE_LOADING; bean.downloadState = STATE_LOADING; Utils.Print("线程" + threadID + "连接成功"); is = conn.getInputStream(); // 1K的数据缓冲 byte[] bs = new byte[1024]; // 读取到的数据长度 int len; //从指定的位置开始下载 file.seek(bean.startDownloadPosition); // 循环读取,当已经下载的大小达到了指定的本线程负责的大小时跳出循环,线程之间负责的文件首尾有重合的话没有影响,因为写入的内容时相同的 while ((len = is.read(bs)) != -1) { //不用在这个循环里面更新数据库 file.write(bs, 0, len); //时时更新内存中的已下载大小信息 bean.downloadedSize += len; //如果调用者暂停下载,则跳出结束方法 if (downloadState == STATE_PAUSING) { Utils.Print("线程" + threadID + "暂停下载"); break; } } is.close(); file.close(); } else { Utils.Print("线程" + threadID + "连接失败"); } conn.disconnect(); //如果这个线程已经下载完了自己负责的部分就修改下载状态 if (bean.downloadedSize >= bean.downloadTotalSize) { bean.downloadState = STATE_FINISH; } else { bean.downloadState = STATE_PAUSING; } //内存中信息更新至数据库 finalDb.update(bean, "threadID='" + bean.threadID + "'"); } catch (IOException e) { Utils.Print("线程" + threadID + "IO异常"); e.printStackTrace(); } } }
线程信息的封装类:负责存储每个线程的ID,开始下载的位置,结束下载的位置,已经下载的大小,下载状态等;这个类用FinalDb数据库存储到数据库,一定要写get,set方法和空参构造器
@Table(name = "ThreadInfo") public class ThreadDownloadInfoBean { /** * id */ public int id; /** * 线程ID */ public int threadID; /** * 本线程时时下载开始的位置 */ public long startDownloadPosition; /** * 本线程时时下载结束的位置 */ public long endDownloadPosition; /** * 本线程负责下载的文件大小 */ public long downloadTotalSize; /** * 已经下载了的文件大小 */ public long downloadedSize; /** * 本线程的下载状态 */ public int downloadState; public ThreadDownloadInfoBean() { } public ThreadDownloadInfoBean(int downloadState, long downloadedSize, long downloadTotalSize, long endDownloadPosition, long startDownloadPosition, int threadID) { this.downloadState = downloadState; this.downloadedSize = downloadedSize; this.downloadTotalSize = downloadTotalSize; this.endDownloadPosition = endDownloadPosition; this.startDownloadPosition = startDownloadPosition; this.threadID = threadID; } public int getId() { return id; } public void setId(int id) { this.id = id; } public int getThreadID() { return threadID; } public void setThreadID(int threadID) { this.threadID = threadID; } public long getStartDownloadPosition() { return startDownloadPosition; } public void setStartDownloadPosition(long startDownloadPosition) { this.startDownloadPosition = startDownloadPosition; } public long getEndDownloadPosition() { return endDownloadPosition; } public void setEndDownloadPosition(long endDownloadPosition) { this.endDownloadPosition = endDownloadPosition; } public long getDownloadTotalSize() { return downloadTotalSize; } public void setDownloadTotalSize(long downloadTotalSize) { this.downloadTotalSize = downloadTotalSize; } public long getDownloadedSize() { return downloadedSize; } public void setDownloadedSize(long downloadedSize) { this.downloadedSize = downloadedSize; } public int getDownloadState() { return downloadState; } public void setDownloadState(int downloadState) { this.downloadState = downloadState; } @Override public String toString() { return "ThreadDownloadInfoBean{" + "id=" + id + ", threadID=" + threadID + ", startDownloadPosition=" + startDownloadPosition + ", endDownloadPosition=" + endDownloadPosition + ", downloadTotalSize=" + downloadTotalSize + ", downloadedSize=" + downloadedSize + ", downloadState=" + downloadState + '}'; } }
下载工具类:这个类负责得到下载文件大小,分配线程下载大小,管理下载线程
public class DownUtil { /** * 数据库操作工具 */ public FinalDb finalDb; /** * 下载状态:准备好 */ public static final int STATE_READY = 1; /** * 下载状态:下载中 */ public static final int STATE_LOADING = 2; /** * 下载状态:暂停中 */ public static final int STATE_PAUSING = 3; /** * 下载状态:下载完成 */ public static final int STATE_FINISH = 4; /** * 下载状态 */ public int downloadState; /** * context */ private Context context; /** * 要下载文件的大小 */ public long fileSize; /** * 线程集合 */ private ArrayListthreadList = new ArrayList<>(); /** * 构造器 */ public DownUtil(Context context) { this.cOntext= context; finalDb = DBUtil.getFinalDb(context); judgeDownState(); } /** * 初始化时判断下载状态 */ public void judgeDownState() { //取出数据库中的下载信息,存储到内存中 List list = finalDb.findAll(ThreadDownloadInfoBean.class); if (list != null && list.size() == DownloadActivity.threadNum) { for (int i = 0; i = spFileSize) { downloadState = STATE_FINISH; } else { downloadState = STATE_PAUSING; fileSize = spFileSize; } } /** * 点击了开始按钮 */ public void clickDownloadBtn() { if (downloadState == STATE_READY) { startDownload(); } else if (downloadState == STATE_PAUSING) { continueDownload(); } } /** * 进入应用第一次开始下载 */ private void startDownload() { //开启新线程,得到要下载的文件大小 new Thread() { @Override public void run() { try { HttpURLConnection conn; cOnn= (HttpURLConnection) new URL(DownloadActivity.url).openConnection(); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); conn.setRequestMethod("GET"); conn.connect(); if (conn.getResponseCode() == 200) { Utils.Print("DownUtil连接成功"); //得到要下载的文件大小 fileSize = conn.getContentLength(); //得到文件名后缀名 String cOntentDisposition= new String(conn.getHeaderField("Content-Disposition").getBytes("ISO-8859-1"), "UTF-8"); String fileName = "下载测试" + contentDisposition.substring(contentDisposition.lastIndexOf("."), contentDisposition.lastIndexOf("\"")); //得到存储路径 String sdCardPath = context.getExternalFilesDir(null).getPath(); DownloadActivity.fileName = sdCardPath + "/" + fileName; SPUtil.getInstance(context).saveString(DownloadActivity.FILE_NAME, DownloadActivity.fileName); SPUtil.getInstance(context).saveLong(DownloadActivity.fileName, fileSize); /* * 计算一下每个线程需要分担的下载文件大小 比如 总下载量为100 一共有三个线程 * 那么 线程1负责0-32,线程2负责33-65,线程3负责66-99和100, * 也就是说下载总量除以线程数如果有余数,那么最后一个线程多下载一个余数部分 */ //每个线程均分的大小 long threadDownSize = fileSize / DownloadActivity.threadNum; //线程均分完毕剩余的大小 long leftDownSize = fileSize % DownloadActivity.threadNum; //创建要写入的文件 RandomAccessFile file = new RandomAccessFile(DownloadActivity.fileName, "rw"); //设置文件大小 file.setLength(fileSize); //关闭 file.close(); for (int i = 0; i list = finalDb.findAll(ThreadDownloadInfoBean.class); for (int i = 0; i bean.downloadTotalSize &#63; bean.downloadTotalSize : bean.downloadedSize; totalSize += addSize; } } return totalSize; } /** * 下载信息监听器 */ private DownloadInfoListener downloadInfoListener; /** * 下载信息监听器 */ public interface DownloadInfoListener { void connectSuccess(); void connectFail(); void IOException(); } /** * 设置下载信息监听器 */ public void setDownloadInfoListener(DownloadInfoListener downloadInfoListener) { this.downloadInfoListener = downloadInfoListener; } }
页面Activity:负责展示下载进度等信息,提供操作页面
public class DownloadActivity extends BaseActivity { /** * 下载地址输入框 */ private EditText et_download_url; /** * 确定输入地址的按钮 */ private Button btn_download_geturl; /** * 进度条 */ private NumberProgressView np_download; /** * 开始下载的按钮 */ private Button btn_download_start; /** * 暂停下载的按钮 */ private Button btn_download_pause; /** * 取消下载的按钮 */ private Button btn_download_cancel; /** * 文件信息显示 */ private TextView tv_download_file_info; /** * 下载速度显示 */ private TextView tv_download_speed; /** * 显示下载信息 */ private TextView tv_download_speed_info; /** * 每隔一段时间刷新下载进度显示 */ private final static int WHAT_INCREACE = 1; /** * 得到了文件名称 */ private final static int WHAT_GET_FILENAME = 2; /** * downUtil连接失败 */ private final static int WHAI_CONNECT_FAIL = 3; /** * downUtilIO异常 */ private final static int WHAT_IO_EXCEPTION = 4; /** * 下载工具 */ private DownUtil downUtil; /** * 需要开启的线程数量 */ public static final int threadNum = 5; /** * 存放文件路径名称的SP键名 */ public static final String FILE_NAME = "fileName"; /** * 要下载的文件的url地址 */ public static String url = ""; /** * 文件下载路径和文件名称 */ public static String fileName; /** * 上次统计已经完成下载的文件大小 */ private long lastFinishedSize; private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case WHAT_INCREACE: updateView(); break; case WHAT_GET_FILENAME: tv_download_file_info.setText("下载路径:" + fileName); break; case WHAI_CONNECT_FAIL: tv_download_file_info.setText("连接失败"); break; case WHAT_IO_EXCEPTION: tv_download_file_info.setText("IO异常"); break; } } }; /** * 更新视图 */ private void updateView() { //当前已经完成下载的文件大小 long currentFinishedSize = downUtil.getFinishedSize(); //要显示的下载信息的文字 StringBuilder downloadInfo = new StringBuilder(); tv_download_speed.setText("当前下载速度:" + formateSize(currentFinishedSize - lastFinishedSize) + "/s"); //本次统计比上次统计增加了,更新视图,本次统计较上次统计没有变化,不做视图更新 if (currentFinishedSize > lastFinishedSize) { lastFinishedSize = currentFinishedSize; downloadInfo.append("下载进度:" + formateSize(currentFinishedSize) + "/" + formateSize(downUtil.fileSize) + "\n"); for (int i = 0; i= downUtil.fileSize) { tv_download_speed.setText("下载完毕"); handler.removeMessages(WHAT_INCREACE); return; } } handler.sendEmptyMessageDelayed(WHAT_INCREACE, 1000); } /** * 返回子类要显示的布局 R.layout...... */ @Override protected int childView() { return R.layout.activity_download; } /** * 强制子类实现该抽象方法,初始化各自的View */ @Override protected void findChildView() { et_download_url = (EditText) findViewById(R.id.et_download_url); btn_download_geturl = (Button) findViewById(R.id.btn_download_geturl); np_download = (NumberProgressView) findViewById(R.id.np_download); btn_download_start = (Button) findViewById(R.id.btn_download_start); btn_download_pause = (Button) findViewById(R.id.btn_download_pause); btn_download_cancel = (Button) findViewById(R.id.btn_download_cancel); tv_download_file_info = (TextView) findViewById(R.id.tv_download_file_info); tv_download_speed = (TextView) findViewById(R.id.tv_download_speed); tv_download_speed_info = (TextView) findViewById(R.id.tv_download_speed_info); } /** * 强制子类实现该抽象方法,初始化各自数据 */ @Override protected void initChildData() { downUtil = new DownUtil(this); lastFinishedSize = downUtil.getFinishedSize(); StringBuilder downloadInfo = new StringBuilder(); fileName = SPUtil.getInstance(this).getString(FILE_NAME, null); if (fileName != null) { downloadInfo.append("下载路径:" + fileName); if (downUtil.downloadState == DownUtil.STATE_FINISH) { np_download.setProgress(100); } else if (downUtil.downloadState == DownUtil.STATE_PAUSING) { np_download.setProgress((int) (downUtil.getFinishedSize() * 100 / downUtil.fileSize)); } tv_download_file_info.setText(downloadInfo); } } /** * 强制子类实现该抽象方法,设置自己的监听器 */ @Override protected View[] setChildListener() { downUtil.setDownloadInfoListener(new DownUtil.DownloadInfoListener() { @Override public void connectSuccess() { //不能在此更新,需要到主线程刷新UI handler.sendEmptyMessage(WHAT_GET_FILENAME); handler.sendEmptyMessage(WHAT_INCREACE); } @Override public void connectFail() { handler.sendEmptyMessage(WHAI_CONNECT_FAIL); } @Override public void IOException() { handler.sendEmptyMessage(WHAT_IO_EXCEPTION); } }); return new View[]{btn_download_start, btn_download_pause, btn_download_geturl, btn_download_cancel}; } /** * 子类在这个方法里面实现自己View的点击监听事件的相应 * * @param v 父类传递到子类的点击的View */ @Override protected void clickChildView(View v) { switch (v.getId()) { case R.id.btn_download_start: downUtil.clickDownloadBtn(); break; case R.id.btn_download_pause: downUtil.clickPauseBtn(); handler.removeMessages(WHAT_INCREACE); break; case R.id.btn_download_cancel: //停止发送消息 handler.removeMessages(WHAT_INCREACE); //暂停下载 downUtil.clickPauseBtn(); //重置状态 downUtil.downloadState = DownUtil.STATE_READY; //统计下载的大小归零 lastFinishedSize = 0; //重置文本信息 tv_download_speed.setText(""); tv_download_file_info.setText(""); tv_download_speed_info.setText(""); //进度条归零 np_download.setProgress(0); //清空内存数据 MapUtil.map.clear(); //sp清理 SPUtil.getInstance(this).removeAll(); //清空数据库 downUtil.finalDb.deleteAll(ThreadDownloadInfoBean.class); //删除文件 if (fileName != null) { File file = new File(fileName); if (file.exists()) { boolean delete = file.delete(); if (delete) { Utils.ToastS(this, "删除" + fileName + "成功"); } else { Utils.ToastS(this, "删除失败"); } } else { Utils.ToastS(this, "文件不存在"); } } else { Utils.ToastS(this, "文件不存在"); } break; case R.id.btn_download_geturl: String editTextUrl = et_download_url.getText().toString(); if (editTextUrl.trim().equals("")) { Utils.ToastS(this, "请输入地址"); } else { url = editTextUrl; } break; } } @Override protected void onDestroy() { handler.removeCallbacksAndMessages(null); downUtil.clickPauseBtn(); super.onDestroy(); } /** * 格式化数据大小 */ private String formateSize(long size) { return Formatter.formatFileSize(this, size); } }
布局
<&#63;xml version="1.0" encoding="utf-8"&#63;>
下载Activity的父类
public abstract class BaseActivity extends Activity { /** * 点击监听器,子类也可以使用 */ protected BaseOnClickListener OnClickListener= new BaseOnClickListener(); /** * 在onCreate方法里面,找到视图,初始化数据,设置点击监听器 */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(childView()); findView(); initData(); setListener(); } /** * 返回子类要显示的布局 R.layout...... */ protected abstract int childView(); /** * 找到需要的视图---网址:https://www.buzzingandroid.com/tools/android-layout-finder/ */ private void findView() { findChildView(); } /** * 初始化数据 */ private void initData() { // 初始化子类的数据 initChildData(); } /** * 对需要设置监听 的视图设置监听 */ private void setListener() { // 子类实现setChildListene()方法,返回一个View数组,里面包含所有需要设置点击监听的View,然后进行For循环,对里面所有View设置监听器 if (setChildListener() == null || setChildListener().length == 0) { return; } View[] viewArray = setChildListener(); for (View view : viewArray) { view.setOnClickListener(onClickListener); } } /** * 自定义的点击监听类 */ protected class BaseOnClickListener implements View.OnClickListener { @Override public void onClick(View v) { clickChildView(v); } } /** * 强制子类实现该抽象方法,初始化各自的View */ protected abstract void findChildView(); /** * 强制子类实现该抽象方法,初始化各自数据 */ protected abstract void initChildData(); /** * 强制子类实现该抽象方法,设置自己的监听器 */ protected abstract View[] setChildListener(); /** * 子类在这个方法里面实现自己View的点击监听事件的相应 * * @param v 父类传递到子类的点击的View */ protected abstract void clickChildView(View v); }
得到数据库的操作类
public class DBUtil { public static FinalDb getFinalDb(final Context context) { FinalDb finalDb = FinalDb.create(context, "REMUXING.db", false, 1, new FinalDb.DbUpdateListener() { @Override public void onUpgrade(SQLiteDatabase db, int oldVirsion, int newVirsion) { } }); return finalDb; } }
内存中存储下载信息的类
public class MapUtil { public static HashMapmap = new HashMap<>(); }
SP存储的工具类
public enum SPUtil { SPUTIL; public static final String NOTIFICATIONBAR_HEIGHT = "notificationbar_height"; private static SharedPreferences sp; public static SPUtil getInstance(Context context) { if (sp == null) { sp = context.getSharedPreferences("REMUXING", Context.MODE_PRIVATE); } return SPUTIL; } public void saveLong(String key, Long value) { sp.edit().putLong(key, value).apply(); } public Long getLong(String key, Long defValue) { return sp.getLong(key, defValue); } public void saveString(String key, String value) { sp.edit().putString(key, value).apply(); } public String getString(String key, String defValue) { return sp.getString(key, defValue); } public void removeAll() { sp.edit().clear().apply(); } }
工具类
public class Utils { /** * 打印 */ public static void Print(String message) { Log.e("TAG", "----- " + message + " ------") ; } /** * 短吐司 */ public static void ToastS(Context context, String message) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } }
进度条:这个可以忽略,用安卓原生的,代码看我的另一篇博客:Android自定义View实现水平带数字百分比进度条
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。