看效果图
项目结构图
这个Demo 主要是通过线程池管理ThreadPoolManager 控制 下载任务线程 DownLoadManager.DownLoadTask , 下载任务线程通过HttpUrlConnection 去网络加载数据 ,下载状态更基于下载观察者 DownLoadManager 去刷新 对应的 UI控件
每一个下载任务是有唯一性的,可以通过 判断下载Url或者 下载id是否一样即可认为是同一个下载任务
我这里是通过下载id ,下载JavaBean 如下:
public class DownloadInfo implements Serializable {
/*****数据库自增 id****/
public int _ID;
/***下载任务id 下载任务唯一标识 , 也可以直接用下载地址****/
public int id;
/**** 下载状态*****/
public int downloadState = DownLoadManager.STATE_NONE;// 下载的状态
/*****下载连接****/
public String downloadUrl;
/****下载文件名****/
public String fileName;
/*****下载文件保存路径****/
public String fileSavePath;
/****当前下载状态***/
public int progress;
/****文件的长度****/
public int fileLength;
/***文件的icon*****/
public String icon;
/***文件后缀名 .apk 或者 .mp4*****/
public String houzui;
@Override
public boolean equals(Object obj) {
if (obj!=null && obj instanceof DownloadInfo){
DownloadInfo newObj = (DownloadInfo) obj;
return newObj.id==id;
}
return super.equals(obj);
}
}
一般情况下,多线程下载包括多个下载状态, 每个下载状态对应不同的UI 表现
下载状态如下:
/**
* 下载管理类
* Created by YZJ on 2017/6/8: 19.
*/
public class DownLoadManager {
/*** 没有下载的状态*/
public static final int STATE_NONE = 0;
/*** 等待中*/
public static final int STATE_WAITING = 1;
/*** 下载中*/
public static final int STATE_DOWNLOADING = 2;
/*** 暂停*/
public static final int STATE_PAUSED = 3;
/*** 下载完毕*/
public static final int STATE_FINISH = 4;
/*** 下载失败*/
public static final int STATE_ERROR = 5;
/*** 已经安装 ,针对于apk*/
public static final int STATE_INSTALLED = 6;
}
下载线程池 ThreadPoolManager:
/**
*
* 下载线程池
* Created by YZJ on 2017/6/8: 19.
*/
public class ThreadPoolManager {
private final static ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 5,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
/*** 添加一个任务**/
public static void execute(BaseLoadTask task) {
if (task != null && !task.isCancle() && !executor.isShutdown()) {
executor.execute(task);
}
}
/*** 取消下载任务*/
public static void cancel(BaseLoadTask task) {
if (task != null && !executor.isShutdown()){
task.cancle();
executor.getQueue().remove(task);
}
}
下载任务线程线程:
/**
* Created by YZJ on 2017/6/8: 21.
* 下载任务基类
*/
public abstract class BaseLoadTask implements Runnable {
/***是否任务已经取消****/
private boolean cancle = false;
/***下载出错任务重复次数 (主要是网络异常导致的,并且没有取消)****/
private int retryTimes = 0;
public boolean isCancle(){
return cancle;
}
/****取消下载任务****/
public void cancle(){
cancle = true;
}
public void reduceRetryTimes(){
if (retryTimes==0){
return;
}
retryTimes--;
}
public int getRetryTimes() {
return retryTimes;
}
}
/*****下载管理类的 内部类 DownLoadManager.DownLoadTask下载任务工作线程*****/
public class DownLoadTask extends BaseLoadTask {
private DownloadInfo info;
public DownLoadTask(DownloadInfo info) {
/******克隆一个新的对象,最好不要在线程里持有原有的对象,导致其无法回收******/
this.info = info.clone();
}
@Override
public void run() {
info.downloadState = STATE_DOWNLOADING;
notifyDownloadProgressed(info);
downLoad();
}
/*******下载********/
void downLoad() {
if (isCancle()) {
/****任务已经被取消*****/
info.downloadState = STATE_PAUSED;
notifyDownloadProgressed(info);
return;
}
/******下载文件大小****/
int fileSize = 0;
/******已经下载的文件大小*****/
int hasLoadSize = 0;
FileInputStream fis = null;
HttpURLConnection connection = null;
try {
/******下载文件根目录****/
String loadpathRoot = DownloadUtil.getLoadPathRoot();
/*****临时下载文件后缀名为 .tmp ,下载完成后,要记得 把 .tmp去掉*****/
File tmpFile = new File(loadpathRoot, info.fileName + info.houzui + DownloadUtil.loadTmpName);
if (tmpFile.exists()) {
/********读取之前下载的文件*******/
fis = new FileInputStream(tmpFile);
int hasDownSize = fis.available();
hasLoadSize += hasDownSize;
}
URL url = new URL(info.downloadUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
/*****连接超时时间 1分钟*****/
connection.setConnectTimeout(60 * 1000);
/*****读取超时时间 1分钟*****/
connection.setReadTimeout(60 * 1000);
// connection.setRequestProperty("Range", "bytes=" + startIndex+ "-" + fileSize);
/*****开启断点续传*****/
connection.setRequestProperty("Range", "bytes=" + hasLoadSize + "-");
connection.connect();
int responseCode = connection.getResponseCode();
/****服务器请求全部资源 200 服务器请求部分资源 206 ****/
if (responseCode == 206 || 200 == responseCode) {
InputStream is = connection.getInputStream();
/*********** 文件大小 = 未下载完的大小 + 已经下载完的大小 ****************/
fileSize = connection.getContentLength() + hasLoadSize;
info.fileLength = fileSize;
RandomAccessFile randomAccessFile = new RandomAccessFile(tmpFile, "rw");
randomAccessFile.seek(hasLoadSize);
int tmpLen = -1;
/*****已经下载的文件大小*****/
int currentSize = hasLoadSize;
/*******记录上一次刷新时下载的大小******/
int preCurrentSize = 0;
byte[] buffer = new byte[8192];
/********* 有数据 ,并且没有被取消就是写文件数据**************/
while ((tmpLen = is.read(buffer)) != -1 && !isCancle()) {
currentSize += tmpLen;
randomAccessFile.write(buffer, 0, tmpLen);
info.progress = currentSize;
/*******通知所有下载观察者*******/
notifyDownloadProgressed(info);
preCurrentSize = currentSize;
float persent = (float) ((currentSize + 0.0) / fileSize * 100);
// Log.i("yzjTag", "当前大小: " + currentSize + " , 总大小: " + //fileSize + " , 百分比: " + persent);
}
is.close();
randomAccessFile.close();
/****标记任务已经取消***/
cancle();
/*****下载完成 ,当前下载字节刚好当于 文件大小,算是下载成功 ,否则算是下载失败******/
if (currentSize == fileSize) {
info.downloadState = STATE_FINISH;
// 下载完成,修改后缀名,把后缀名去掉
DownloadUtil.removeTmpName(tmpFile);
stopDownload(info);
} else if (currentSize > fileSize) {
/***下载错误 ,下载字节大小已经 大于文件大小 ,把文件删除,要重新下载***/
info.downloadState = STATE_ERROR;
tmpFile.delete();
info.progress = 0;
stopDownload(info);
} else {
/******暂停*******/
info.downloadState = STATE_PAUSED;
stopDownload(info);
}
}
} catch (Exception e) {
/***下载异常 没有被取消且重试次数要大于一 则开启重试, **/
if (!isCancle() && getRetryTimes() >= 1) {
/****重试次数减一***/
reduceRetryTimes();
if (connection != null) {
connection.disconnect();
connection = null;
}
DownloadUtil.closeIo(fis);
downLoad();
} else {
/****网络异常取消任务****/
cancle();
info.downloadState = STATE_ERROR;
/******,同时从下载队列移除*********/
stopDownload(info);
}
} finally {
cancle();
if (connection != null) {
connection.disconnect();
connection = null;
}
DownloadUtil.closeIo(fis);
}
}
}
在 下载任务线程中 DownLoadManager.DownLoadTask,状态以及进度的刷新 均通过观察者去 通知 注册了 观察的UI 控件,比如 (ListView GridView)BaseAdapter ,(RecycleView) RecyclerView.Adapter
public class DownLoadTask extends BaseLoadTask {
...
randomAccessFile.write(buffer, 0, tmpLen);
info.progress = currentSize;
/*******通知所有下载观察者*******/
notifyDownloadProgressed(info);
preCurrentSize = currentSize;
float persent = (float) ((currentSize + 0.0) / fileSize * 100);
.....
}
下载观察者管理 DownLoadManager
public class DownLoadManager {
...
private static List<DownloadInfo> downloadInfos = new ArrayList<>();
/*** 存储所有观察者 为什么用CopyOnWriteArrayList ,而不是 ArrayList ,可以去看看源码,ArrayList观察者频繁刷新
* 会抛出 ConcurrentModificationException 多线程问题,用线程安全的 集合
* ConcurrentHashMap 替代 HashMap原理一样
* */
private List<DownloadObserver> downloadObservers = new CopyOnWriteArrayList<>();
/**
* 存储下载中的任务
**/
private Map<Integer, DownLoadTask> downLoadTasksMap = new ConcurrentHashMap<Integer, DownLoadTask>();
/*******下载观察者******/
public interface DownloadObserver {
public void onDownloadStateProgressed(DownloadInfo updateInfo);
}
/******注册一个下载任务 观察者*******/
public void registerObserver(DownloadObserver observer) {
if (observer != null) {
synchronized (downloadObservers) {
if (!downloadObservers.contains(observer)) {
downloadObservers.add(observer);
}
}
}
}
/******注销一个下载任务 观察者*******/
public void unregisterObserver(DownloadObserver observer) {
if (observer != null) {
synchronized (downloadObservers) {
int index = downloadObservers.indexOf(observer);
if (index != -1) {
downloadObservers.remove(index);
}
}
}
}
/******注销所有观察者*******/
public void unregisterAll() {
synchronized (downloadObservers) {
downloadObservers.clear();
}
}
/**
* 当下载进度 和状态发送改变的时候回调
*/
public void notifyDownloadProgressed(final DownloadInfo item) {
/**
* 用ArrayyList的话 java.util.ConcurrentModificationException
*/
for (final DownloadObserver observer : downloadObservers) {
MyApplication.getIntance().getHandler().post(new Runnable() {
@Override
public void run() {
observer.onDownloadStateProgressed(item);
}
});
}
}
....
/*****下载任务工作线程*****/
public class DownLoadTask extends BaseLoadTask{...}
///接下来就是 下载线程任务的 控制 ,暂停 ,继续 的操作了
/*****取消所有下载任务*******/
public synchronized void cancleAllTask() {
for (DownLoadTask loadTask : downLoadTasksMap.values()) {
if (loadTask != null) {
/******取消对应的任务*******/
ThreadPoolManager.cancel(loadTask);
unregisterAll();
}
}
}
/*****开始下载一个任务*******/
public synchronized void startDownload(DownloadInfo item) {
if (item != null) {
/*******先检查任务是否已经存在*******/
if (downLoadTasksMap.containsKey(item.id)) {
DownLoadTask downLoadTask = downLoadTasksMap.get(item.id);
if (downLoadTask != null && !downLoadTask.isCancle()) {
/****,并且没有被取消,则 return******/
return;
} else {
/****,任务取消,则先移除这个任务******/
ThreadPoolManager.cancel(downLoadTask);
}
}
/*******开启新的下载任务*********/
DownLoadTask downLoadTask = new DownLoadTask(item);
/*****放入任务集合*****/
downLoadTasksMap.put(item.id, downLoadTask);
ThreadPoolManager.execute(downLoadTask);
item.downloadState = STATE_WAITING;
notifyDownloadProgressed(item);
}
}
/*** 暂停或者取消任务*/
public synchronized void stopDownload(DownloadInfo item) {
if (item != null) {// 修改下载状态
notifyDownloadProgressed(item);
DownLoadTask task = downLoadTasksMap.remove(item.id);// 先从集合中找出下载任务
if (task != null) {
ThreadPoolManager.cancel(task);// 然后从线程池中移除
}
}
}
}
上面的观察者,接下来需要 被观察者,也就是 UI控件需要注册观察者. 主要是这些控件(ListView GridView)BaseAdapter ,(RecycleView) RecyclerView.Adapter
观察者注册
@Override
protected void onCreate(Bundle savedInstanceState) {
/**** 注册观察者******/
adapter = new LoadBaseAdapter(getActivity(),MyApplication.getIntance().downloadInfos,listView);
listView.setAdapter(adapter);
/******注册下载通知观察者*****/
adapter.registerDownObserver();
}
@Override
protected void onDestroy() {
super.onDestroy();
/****注销下载通知观察者***/
if (adapter!=null){
adapter.unRegisterDownObserver();
}
}
然后就是 UI控件下载状态的更新了.
对于RecycleView 刷新item比较简单,RecycleView直接支持
public class LoadRecycleViewAdapter extends RecyclerView.Adapter<LoadRecycleViewAdapter.LoadHolder> implements DownLoadManager.DownloadObserver {
/*******注册观察者*******/
public void registerDownObserver() {
DownLoadManager.getDownInstance().registerObserver(this);
}
/*******注销观察者*******/
public void unRegisterDownObserver() {
DownLoadManager.getDownInstance().unregisterObserver(this);
}
@Override
public void onDownloadStateProgressed(final DownloadInfo updateInfo) {
final int index = datas.indexOf(updateInfo);
if (index >= 0) {
DownloadInfo item = datas.get(index);
item.updateDownloadInfo(updateInfo);
notifyItemChanged(index);
}
}
而对于ListView GridView 单个item刷新是比较麻烦点的 .网上也有很多种刷新的方法. 这里刷新方法是 ,在
BaseAdapter 中getView 方法时,把Holder 存在集合list里 ,但是也不能无限的往集合里面堆,Holder 也是需要回收的 .我们可以在ListView回收 View (View 绑定了Holder)的回调方法中,把 对应的Holder 从集合移除掉. 同时Holder 也绑定 唯一的DownloadInfo
接着就是从下载观察者回调中 得到 下载DownloadInfo, 遍历集合List (Holder> ,当Holder 绑定DownloadInfo 和当前的 载观察者回调中DownloadInfo时,刷新Holder
public class LoadBaseAdapter extends AbsAdapter<DownloadInfo> implements DownLoadManager.DownloadObserver, AbsListView.RecyclerListener {
public LoadBaseAdapter(Context context, List<DownloadInfo> data, ListView listView) {
super(context, data);
if (listView != null) {
//ListView itemView 回收监听
listView.setRecyclerListener(this);
}
holders = new CopyOnWriteArrayList<>();
}
/*****ListView 触发回收View的时候 ,同时回收我们的Holder****/
@Override
public void onMovedToScrapHeap(View view) {
// recycleViewBin
if (view != null && view.getTag() instanceof Holder) {
Holder holder = (Holder) view.getTag();
synchronized (holders) {
holders.remove(holder);
}
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
.....
holder.setData(item);
/**** 添加 Holder*****/
if (!holders.contains(holder)) {
holders.add(holder);
}
....
return convertView;
}
@Override
public void onDownloadStateProgressed(final DownloadInfo updateInfo) {
List<Holder> holders = getHolders();
for (final Holder holder : holders) {
final DownloadInfo item = holder.item;
if (item != null && item.equals(updateInfo)) {
/**** 更新item *****/
item.updateDownloadInfo(updateInfo);
/**** Holdr 更新*****/
holder.update();
}
}
}
}
/*******注册观察者*******/
public void registerDownObserver() {
DownLoadManager.getDownInstance().registerObserver(this);
}
/*******注销观察者*******/
public void unRegisterDownObserver() {
DownLoadManager.getDownInstance().unregisterObserver(this);
}
Holder
class Holder implements View.OnClickListener {
private DownloadInfo item;
public void setData(DownloadInfo item) {
....
this.item = item;
.....
}
/********更新数据*********/
public void update() {
switch (item.downloadState) {
case DownLoadManager.STATE_NONE:
pauseOrStart.setText(R.string.download);
break;
case DownLoadManager.STATE_WAITING:
pauseOrStart.setText(R.string.wait);
break;
case DownLoadManager.STATE_DOWNLOADING:
pauseOrStart.setText(R.string.stop);
progress.setProgress((int) (item.progress));
progress.setMax((int) (item.fileLength));
break;
case DownLoadManager.STATE_PAUSED:
pauseOrStart.setText(R.string.resume);
break;
case DownLoadManager.STATE_FINISH:
pauseOrStart.setVisibility(View.GONE);
install.setVisibility(View.VISIBLE);
progress.setVisibility(View.GONE);
break;
case DownLoadManager.STATE_ERROR:
pauseOrStart.setText(R.string.retry);
break;
case DownLoadManager.STATE_INSTALLED:
break;
default:
break;
}
}
}
对于ListView Adapter,我同时弄了个基于 万能适配的回收监听