您的位置:首页 > 移动开发 > Android开发

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上的代码,给我提出一写建议。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: