Android 多线程文件断点下载器实现(造轮子系列)(二)
来源:互联网 发布:js中的延时函数 编辑:程序博客网 时间:2024/06/04 17:40
1.开始
如果对断点续传没有了解的,可以看看我的上一篇博文——断点续传实现
上次完成了断点下载相关的功能,这次开始进行任务并行相关的扩展。
任务并行需要完成的功能:一定数量的任务并行下载,超过额定值的任务暂停等待。有一个很好的方法能完成这种要求,那就是concurrent包下提供的线程池。
2.任务抽象
用到线程池,那么就要用到runnable并进行相关的调用,为了提高抽象等级,把下载任务相关的属性和方法抽象成一个抽象类,供下载任务类实现。可以看到,基类是要求子类实现Runnable接口的。
abstract public class TransferTask implements Runnable{ protected long taskSize; protected long completedSize; protected String url; protected String fileName; protected String saveDirPath; protected OkHttpClient client; //下载的文件 RandomAccessFile file; //任务状态 int state = LoadState.PREPARE; public long getTaskSize() { return taskSize; } public long getCompletedSize() { return completedSize; } public String getFileName() { return fileName; } public void setFileName(String fileName) { this.fileName = fileName; } public void setSaveDirPath(String saveDirPath) { this.saveDirPath = saveDirPath; } public int getState() { return state; } public void setState(int state) { this.state = state; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getSaveDirPath() { return saveDirPath; } @Override abstract public void run();}
具体实现类的run()方法,使用上次写下的代码就可以了。
3.DownloadManager
完成了简单的抽象,就有了可供调度的线程。
考虑到使用线程池,那么就需要一个专门的类管理线程池,并对相关的下载任务进行分配和需要的操作。
于是定义一个DownlaodManager,因为线程池无法获取线程的状态和改变线程内的变量,所以Manager出了维护一个线程池外,还要维护一个任务的列表,在线程执行完毕或取消后,将对应的任务移出列表。设置的回调如下
interface CompletedListener{ void isFinished(String url);}
除了这些,还要考虑到后台线程和前台交互,用来显示进度或者提示相关信息。于是也需要维护一个UI线程的Handler,并用回调更新界面。
这样的应用,比较适合使用单例模式。具体分析写在注释里
/** * Created by pxh on 2016/2/15. * 管理下载任务 */public class DownloadManager implements DownloadTask.CompletedListener{ static DownloadManager mManager; Context context; //数据库相关的操作类和实体类 static private DaoMaster daoMaster; static private DaoSession daoSession; private DownloadEntityDao downloadDao; //下载路径 String downLoadPath = ""; //可并行线程数 private int nThread; //Activity或fragment实现的接口,方法为OnUIUpdate(),是在UI线程运行的方法 DownloadUpdateListener mDownloadUpdate; private Handler mHandler; //维护的任务相关队列,保存未完成的任务 LinkedList<TransferTask> taskList; //用于调度的线程池 ExecutorService executorService; //私有构造方法 private DownloadManager(Context context, int nThread) { this.context = context; this.nThread = nThread; //得到UI线程的Handler,用于更新UI mHandler = new Handler(Looper.getMainLooper()); //初始化nThread大小的线程池,超过nThread的任务挂起 executorService = Executors.newFixedThreadPool(this.nThread); taskList = new LinkedList<>(); //将数据库中的未完成任务读取出来,并存入到taskList中 getDownloadTask(); downloadDao = getDaoSession(context).getDownloadEntityDao(); } //获得实例前先进行一次init,因为下载任务在后台执行,为了防止内存泄漏,Context最好为Application的Context而不是Activity的 static public void init(Context context) { if (mManager == null) synchronized (DownloadManager.class) { if (mManager == null) mManager = new DownloadManager(context, 3); } } static public void init(Context context, int nThread) { if (mManager == null) synchronized (DownloadManager.class) { if (mManager == null) mManager = new DownloadManager(context, nThread); } } static public DownloadManager getInstance() { if (mManager == null) throw new NullPointerException(); return mManager; } public void addTask(String url, String fileName) { //DownloadTask为TransferTask的实现类 DownloadTask task = new DownloadTask(fileName, url, SDCardUtils.getSDCardPath() + downLoadPath, downloadDao); //注册完成事件,方便任务完成后将实例移出实例集合 task.setCompletedListener(this); //将任务加入队列,并在线程池中执行 taskList.add(task); executorService.execute(task); } /** * 可以获得当前为下载完成的任务列表及其相关信息 * * @return */ public LinkedList<TransferTask> getTaskList() { return taskList; } //可以根据url获取相应的任务 private DownloadTask getTask(String url) { for (TransferTask task : taskList) { DownloadTask dTask = (DownloadTask) task; if (dTask.getUrl().equals(url)) { return dTask; } } return null; } //下载完成,将相应的任务移出taskList @Override public void isFinished(String url) { Log.v("task finished", "task : " + url + " download completed"); DownloadTask task = getTask(url); if (task != null) taskList.remove(task); else Log.e("isFinished", "task=null"); } //使用弱引用,防止内存泄漏 public void setUpdateListener(DownloadUpdateListener updateListener) { WeakReference<DownloadUpdateListener> reference = new WeakReference<>(updateListener);//prevent memory leak this.mDownloadUpdate = reference.get(); } /** * 获取DaoMaster * * @param context * @return */ public static DaoMaster getDaoMaster(Context context) { if (daoMaster == null) { DaoMaster.OpenHelper helper = new DaoMaster.DevOpenHelper(context, "downloadDB", null); daoMaster = new DaoMaster(helper.getWritableDatabase()); } return daoMaster; } /** * 获取 DaoSession * * @param context * @return */ public static DaoSession getDaoSession(Context context) { if (daoSession == null) { if (daoMaster == null) { daoMaster = getDaoMaster(context); } daoSession = daoMaster.newSession(); } return daoSession; } //将数据库中的未完成任务读取出来,并存入到taskList中 private void getDownloadTask() { DownloadEntityDao downloadEntityDao = getDaoSession(context).getDownloadEntityDao(); List<DownloadEntity> entityList = downloadEntityDao.loadAll(); for (DownloadEntity entity : entityList) { Log.e("dao", entity.toString()); if (entity.getCompletedSize().equals(entity.getTaskSize())) { //handle already downloaded files } else taskList.add(new DownloadTask(downloadEntityDao, entity)); } } public interface DownloadUpdateListener { void OnUIUpdate(); }}
4.简单使用
在Activity中,只需要如下代码就可以得到Manager的实例
DownloadManager.init(this.getApplicationContext());downloadManager = DownloadManager.getInstance();
一般情况下,都是使用ListView或者RecyclerView显示下载的信息,这时候就可以通过
downloadManager.getTaskList()
获得任务列表的引用
并且可以在OnUIUpdate()方法中随任务下载更新列表
@Overridepublic void OnUIUpdate(){ adapter.notifyDataSetChanged();}
这里写了一个例子,Activity如下
public class MainActivity extends AppCompatActivity implements TaskConfirmDialog.InputCompletedListener, DownloadManager.DownloadUpdateListener{ private ListView listView; DownloadManager downloadManager; protected Adapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); listView = (ListView) findViewById(R.id.listView); DownloadManager.init(this.getApplicationContext()); downloadManager = DownloadManager.getInstance(); downloadManager.setUpdateListener(this); setListViewAdapter(); verifyStoragePermissions(this); } private static void deleteFilesByDirectory(File directory) { if (directory != null && directory.exists() && directory.isDirectory()) { for (File item : directory.listFiles()) { item.delete(); } } } void setListViewAdapter() { adapter = new Adapter(this, downloadManager.getTaskList()); listView.setAdapter(adapter); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main_menu, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_add_task: TaskConfirmDialog dialogFragment = new TaskConfirmDialog(); android.app.FragmentManager manager = getFragmentManager(); dialogFragment.show(manager, ""); break; } return super.onOptionsItemSelected(item); } @Override public void inputCompleted(String url, String fileName) { url = "http://apk.hiapk.com/web/api.do?qt=8051&id=716"; String url1 = "https://github.com/nebulae-pan/OkHttpDownloadManager/archive/master.zip"; String url2 = "https://github.com/bxiaopeng/AndroidStudio/archive/master.zip"; String url3 = "https://github.com/romannurik/AndroidAssetStudio/archive/master.zip"; String url4 = "https://github.com/facebook/fresco/archive/master.zip"; String url5 = "https://github.com/bacy/volley/archive/master.zip"; downloadManager.addTask(url, "123.apk"); downloadManager.addTask(url1, "1.zip"); downloadManager.addTask(url2, "2.zip"); downloadManager.addTask(url3, "3.zip"); downloadManager.addTask(url4, "4.zip"); downloadManager.addTask(url5, "5.zip"); } @Override public void OnUIUpdate() { adapter.notifyDataSetChanged(); } /** * just sample */ static class Adapter extends BaseAdapter { LinkedList<TransferTask> data; Context context; public Adapter(Context context, LinkedList<TransferTask> data) { this.data = data; this.context = context; } @Override public int getCount() { return data.size(); } @Override public Object getItem(int position) { return data.get(position); } @Override public long getItemId(int position) { return 0; } @Override public View getView(final int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = ((Activity) context).getLayoutInflater().inflate(R.layout.item_download, parent, false); } final TransferTask tf = data.get(position); //if taskSize isn't initial complete,post to getView ((TextView) convertView.findViewById(R.id.title)).setText(tf.getFileName()); ((ProgressBar) convertView.findViewById(R.id.progressBar)).setProgress((int) (tf.getTaskSize() > 0 ? 100 * tf.getCompletedSize() / tf.getTaskSize() : 0)); if (tf.getState() == LoadState.PREPARE) { (convertView.findViewById(R.id.operation)).setEnabled(false); ((Button) convertView.findViewById(R.id.operation)).setText("connecting"); } if (tf.getState() == LoadState.PAUSE) { ((Button) convertView.findViewById(R.id.operation)).setText("start"); } if (tf.getState() == LoadState.DOWNLOADING) { (convertView.findViewById(R.id.operation)).setEnabled(true); ((Button) convertView.findViewById(R.id.operation)).setText("pause"); } (convertView.findViewById(R.id.operation)).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (tf.getState() == LoadState.DOWNLOADING) DownloadManager.getInstance().pauseTask(position); else if (tf.getState() == LoadState.PAUSE) DownloadManager.getInstance().restartTask(position); } }); return convertView; } }}
5.运行效果与问题解决
具体运行效果如下
第一个截图是同时有三个任务可以下载,其余的任务等待
如果前面的任务暂停,后面的任务开始进行
不过在更新界面的时候发现了一个问题,在每接受一个数据块后更新,导致多任务下更新速率极快,大于了界面刷新速度。
想到的解决办法有两个
- 接收数据块后挂起一段时间,降低更新速率
- 格外开启一个更新线程,固定的时间想UI线程发送更新信息
第一种方法实现简单,但会导致加载变慢。
这里放下我想到的第二种方法
//在DownloadManager下加入这两个属性final Object updateLock = new Object();//更新界面进程的互斥锁boolean isUpdating = false; //当前是否更新/** * 需判断状态,全部暂停后停止更新界面 */Runnable updateUIByOneSecond = new Runnable(){ @Override public void run() { synchronized (updateLock) { if (isUpdating) { mHandler.post(new Runnable() { @Override public void run() { if (mDownloadUpdate == null) return; mDownloadUpdate.OnUIUpdate(); } }); ifNeedStopUpdateUI(); mHandler.postDelayed(this, 1000); } } }};protected void startUpdateUI(){ synchronized (updateLock) { if (!isUpdating) { isUpdating = true; new Thread(updateUIByOneSecond).start(); } }}protected void stopUpdateUI(){ synchronized (updateLock) { isUpdating = false; }}//检查是否需要停止更新protected void ifNeedStopUpdateUI(){ for (TransferTask task : taskList) { if (task.getState() == LoadState.DOWNLOADING || task.getState() == LoadState.PREPARE ) return; } stopUpdateUI();}
实现思路就是,在addTask(),reStartTask()这些会需要界面更新的任务中加入startUpdateUI()器界面更新线程。
界面更新线程updateUIByOneSecond()每秒执行一次,会判断当前状态是否需要执行更新,如果需要,向UI线程的Handler发送更新信息,并在发送完毕后,检查所有任务的状态,如果不存在正在下载,或正在连接准备中的任务,就结束更新。这样就能实现统一的列表更新。
6.结束
这次实现了多任务并行下载,之后就要向最后一步:多线程下载,发起挑战了。
如果有大神,希望能看看我的博文或在github上的代码,给我提出一写建议。
- Android 多线程文件断点下载器实现(造轮子系列)(二)
- Android 多线程文件断点下载器实现(造轮子系列)(一)
- Android基于HTTP协议的多线程断点下载器的实现(二)
- Android多线程实现文件断点下载
- Android-多线程断点下载详解及源码下载(二)
- [造轮子]Android多线程下载
- (Android小应用)在Android中实现多线程断点下载(连载二)
- android(25)(android下实现多线程断点下载)
- OSS实现多文件多线程的断点下载(java)
- 多线程断点文件下载实现
- 3多线程断点下载一个文件(android工程:java实现)
- Android多线程断点下载文件
- android(24)(用java实现多线程断点下载)
- android 多线程断点下载实现
- Android实现多线程断点下载
- Android实现多线程断点下载
- 多线程断点下载实现(J2SE版本)
- java(android)多线程下载,和多线程断点下载
- ECNU_OJ_1007
- 使用Fiddler对手机应用进行抓包测试 (试了一下, 靠谱)
- 错误信息收集
- 【Vuforia】基于Vuforia增强现实开发(一)之识别图像播放声音
- JavaScript 原型模式的理解
- Android 多线程文件断点下载器实现(造轮子系列)(二)
- python爬取某个贴吧帖子留下的邮箱,并自动发送相应链接
- LeetCode题解--8. String to Integer (atoi)
- Python multiprocessing threading
- Android学习之界面篇(一)Android Animation简单介绍
- [Commons]——队列包装
- CSDN-markdown学习ing
- C++中的OOP
- java线程FAQ