Android 多线程文件断点下载器实现(造轮子系列)(二)
2016-04-02 23:09
621 查看
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()方法中随任务下载更新列表
@Override public 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 studio 使用NDK工具实现JNI编程
- Futurice公司Android开发者总结的经验教训
- 解决Android Studio加载第三方jar包,出现包重复加载的问题
- android WindowManagerService addFakeWindow 研究
- Android开发:一个简单的画板
- Android常见问题集锦
- GitHub 上排名前 100 的 Android 开源库介绍
- Android应用自动更新功能的代码实现
- Android 学习记录-基础控件与布局
- android 系统中对条件查找命令
- Android Sparse*Array容器解析
- android查看源码的时候看不了
- android查看源码的时候看不了
- Android中的羊角符
- 【配置安装】Xamarin for Windows, 用C#开发android
- Android导入工程提示 Invalid project description 详解
- Android 使用android-support-multidex解决Dex超出方法数的限制问题,让你的应用不再爆棚
- Android帮助文档本地打开慢的解决方案
- 国内技术社区活跃的 Android 大神汇总
- Android开源项目分包方式学习(eoe、oschina、github)