aaynctask控制多个下载进度_Android实战:多线程多文件断点续传下载+通知栏控制...

本文介绍了如何在Android中实现多线程多文件的断点续传下载,并结合通知栏控制下载进度。通过使用ListView展示多个下载文件,每个文件的下载进度由一个独立的DownloadTask线程负责,使用FileAdapter绑定视图并处理点击事件。文章详细讲解了多线程下载的原理和实现,包括文件分块、进度更新、线程管理和断点续传。同时,利用Notification展示下载状态,实现了通知栏控制下载操作的功能。
摘要由CSDN通过智能技术生成

本文续接我上一篇文章《Android实战:简易断点续传下载器实现》

链接地址:http://www.jianshu.com/p/5b2e22c42467

说到多线程下载,也许大家会觉得很迷惑,但多线程的原理实际上与单线程下载的原理并无区别。

多线程下载只需要确定好下载一个文件需要多少个线程,一般来说最好为3条线程,因为线程过多会占用系统资源,而且线程间的相互竞争也会导致下载变慢。

其次下载的时候将文件分割为三份(假设用3条线程下载)下载,在java中就要用到上次提到的RandomAccessFile这个API,它的开始结束为止用以下代码确定:

conn.setRequestProperty("Range", "bytes=" + start + "-" + end)

最后就是断点续传了,只需要才程序停止下载的时候记录下最后的下载位置就好了,当下次下载的时候从当前停止的位置开始下载。

OK,那么现在就开始我们的多线程下载+通知栏控制的实战之旅吧!

多线程断点续传下载

我们这次要做的并非简单的多线程下载,而是要做到多文件多线程的同时下载

重写布局

这次下载需要展示多个下载的文件,所以使用ListView控件,界面效果如下

下载界面.png

每个ListView的item都很简单,基本上只需要将上次写的下载界面搬过来就好了。

新建一个Layout,命名为item,将中的界面布局剪切过来,然后在中设置一个ListView空间。

activity_main.xml代码如下

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent" >

android:id="@+id/list_view"

android:layout_width="match_parent"

android:layout_height="match_parent" />

至于Item的布局,为了省功夫,就不写了,大家可以去我的Github下载名为MultiDownload的项目来参考。

建立FileAdapter类

布局写好了,但是ListView总是要有个Adapter类来绑定视图,填充布局的不是么,所以接下来就开始写FileAdapter了。

话说回来,ListView真的是个很重要的空间,不熟悉的小伙伴抓紧多看看怎么做吧。

创建一个继承自BaseAdapter类的FileAdapter,里面拥有以下三个成员变量:

private Context mContext = null;

private List mFilelist = null;

private LayoutInflater layoutInflater;

然后重写构造函数:

public FileAdapter(Context mContext, List mFilelist) {

this.mContext = mContext;

this.mFilelist = mFilelist;

layoutInflater = LayoutInflater.from(mContext);

}

再将继承的getCount/getItem/getItemId三个方法的返回值写好,用于ListView找到各自的Item。

接下来就是重头戏,重写getView方法了!

我们先定义一个静态的ViewHolder内部类,这样在ListView属性的时候才不会重复创建对象,减轻内存压力,这个谷歌官方推荐的哦!

static class ViewHolder {

TextView textview;

Button startButton;

Button stopButton;

ProgressBar progressBar;

}

然后在getView中绑定布局item中的各个控件,并且设置按钮的点击事件,getView代码如下:

@Override

public View getView(int position, View convertView, ViewGroup parent) {

ViewHolder viewHolder = null;

final FileInfo mFileInfo = mFilelist.get(position);

if (convertView == null) {

convertView = layoutInflater.inflate(R.layout.item, null);

viewHolder = new ViewHolder();

viewHolder.textview = (TextView) convertView.findViewById(R.id.file_textview);

viewHolder.startButton = (Button) convertView.findViewById(R.id.start_button);

viewHolder.stopButton = (Button) convertView.findViewById(R.id.stop_button);

viewHolder.progressBar = (ProgressBar) convertView.findViewById(R.id.progressBar2);

viewHolder.textview.setText(mFileInfo.getFileName());

viewHolder.progressBar.setMax(100);

viewHolder.startButton.setOnClickListener(new OnClickListener() {

@Override

public void onClick(View v) {

Intent intent = new Intent(mContext, DownloadService.class); intent.setAction(DownloadService.ACTION_START);

intent.putExtra("fileInfo", mFileInfo);

mContext.startService(intent);

}

});

viewHolder.stopButton.setOnClickListener(new OnClickListener() {

@Override

public void onClick(View v) {

Intent intent = new Intent(mContext, DownloadService.class);

intent.setAction(DownloadService.ACTION_STOP);

intent.putExtra("fileInfo", mFileInfo);

mContext.startService(intent);

}

});

convertView.setTag(viewHolder);

} else {

viewHolder = (ViewHolder) convertView.getTag();

}

viewHolder.progressBar.setProgress(mFileInfo.getFinished());

return convertView;

}

最后再新建一个更新进度条的方法,在获得文件ID,和当前进度之后,直接更新进度条,代码如下:

public void updataProgress(int id, int progress) {

FileInfo info = mFilelist.get(id);

info.setFinished(progress);

notifyDataSetChanged();

}

好了,整个FileAdapter类就这样写完了!下面我们来修改一下MainActivity中的代码吧。

修改MainActivity代码

由于我们在FileAdapter中已经将布局写好了,而且点击事件和更新进度也是在FileAdapter中进行的,因此不需要在MainActivity中绑定按键了,现在可以将有关Button和ProgressBar的代码都删掉。然后在配置好ListView控件就可以了,代码如下:

private ListView listView;

private List mFileList;

private FileAdapter mAdapter;

private String urlone = "http://www.imooc.com/mobile/imooc.apk";

private String urltwo = "http://www.imooc.com/download/Activator.exe";

private String urlthree = "http://s1.music.126.net/download/android/CloudMusic_3.4.1.133604_official.apk";

private String urlfour = "http://study.163.com/pub/study-android-official.apk";

private UIRecive mRecive;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

// 初始化控件

listView = (ListView) findViewById(R.id.list_view);

mFileList = new ArrayList();

// 初始化文件对象

FileInfo fileInfo1 = new FileInfo(0, urlone, getfileName(urlone), 0, 0);

FileInfo fileInfo2 = new FileInfo(1, urltwo, getfileName(urltwo), 0, 0);

FileInfo fileInfo3 = new FileInfo(2, urlthree, getfileName(urlthree), 0, 0);

FileInfo fileInfo4 = new FileInfo(3, urlfour, getfileName(urlfour), 0, 0);

mFileList.add(fileInfo1);

mFileList.add(fileInfo2);

mFileList.add(fileInfo3);

mFileList.add(fileInfo4);

mAdapter = new FileAdapter(this, mFileList);

listView.setAdapter(mAdapter);

mRecive = new UIRecive();

// 注册广播接收器

IntentFilter intentFilter = new IntentFilter();

intentFilter.addAction(DownloadService.ACTION_UPDATE);

intentFilter.addAction(DownloadService.ACTION_FINISHED);

intentFilter.addAction(DownloadService.ACTION_START);

registerReceiver(mRecive, intentFilter);

}

现在整个视图终于搞好了,可以启动应用,看看视图是否显示正常了。当然啦,下载还没吧下载的代码改好,现在我们就来修改下载代码吧。

修改DownloadTask代码

既然是多线程下载,那么我们便要在下载的时候设置好线程数,首先添加一个int类型的threadCount的参数代码代表线程数,初始值为1。然后在DownloadTask的构造函数中添加threadCount变量,这样在开始下载的时候就能够确定需要多少个线程下载,代码如下:

public DownloadTask(Context comtext, FileInfo fileInfo, int threadCount) {

super();

this.mThreadCount = threadCount;

this.mComtext = comtext;

this.mFileInfo = fileInfo;

this.mDao = new ThreadDAOImple(mComtext);

}

然后我们要确认每个线程需要从文件的哪里开始下载。假设文件长度为10,分为3条线程下载,那么0-2是一份,3-5是一份,6-8是一份(java中从0开始),那么多出的一份怎么办?当然是在计算时,如果最后多出来,归最后的拿份,也就是6-9了。

我们将每个线程需要下载多少长度的文件计算好,就可以让每个文件开始自己的下载任务了,代码如下:

public void download() {

// 从数据库中获取下载的信息

List list = mDao.queryThreads(mFileInfo.getUrl());

if (list.size() == 0) {

int length = mFileInfo.getLength();

int block = length / mThreadCount;

for (int i = 0; i < mThreadCount; i++) {

// 划分每个线程开始下载和结束下载的位置

int start = i * block;

int end = (i + 1) * block - 1;

if (i == mThreadCount - 1) {

end = length - 1;

}

ThreadInfo threadInfo = new ThreadInfo(i, mFileInfo.getUrl(), start, end, 0);

list.add(threadInfo);

}

}

mThreadlist = new ArrayList();

for (ThreadInfo info : list) {

DownloadThread thread = new DownloadThread(info);

// 使用线程池执行下载任务

DownloadTask.sExecutorService.execute(thread);

mThreadlist.add(thread);

// 如果数据库不存在下载信息,添加下载信息

mDao.insertThread(info);

}

}

需要注意的是启动下载线程的时候在这里没有直接使用Thread.start()来启动,而是使用了线程池,因为线程过多,使用线程池便于管理。使用线程池非常简单,只需要在开始的时候定义一个线程池的成员变量:

public static ExecutorService sExecutorService = Executors.newCachedThreadPool();

然后使用

sExecutorService.execute(需要启动的线程);

这样就能够启动线程了,是一种很简单的用法。

然后我们还要定义一个同步方法,判断一个文件的全部线程是否都下载完成,如果下载完成就弹出Toast

public synchronized void checkAllFinished() {

boolean allFinished = true;

for (DownloadThread thread : mThreadlist) {

if (!thread.isFinished) {

allFinished = false;

break;

}

}

if (allFinished == true) {

// 下載完成后,刪除數據庫信息

mDao.deleteThread(mFileInfo.getUrl());

// 通知UI哪个线程完成下载

Intent intent = new Intent(DownloadService.ACTION_FINISHED);

intent.putExtra("fileInfo", mFileInfo);

mComtext.sendBroadcast(intent);

}

}

最后修改一下run方法中的代码,前面我们保存断点下载是整个文件的进度,现在保存下载是单个线程的进度,同时我们还要判断是否整个文件的所有线程是否完成的checkAllFinished方法添加进去,所以将部分代码修改为:

// 定义UI刷新时间

long time = System.currentTimeMillis();

while ((len = is.read(bt)) != -1) {

raf.write(bt, 0, len);

// 累计整个文件完成进度

mFinished += len;

// 累加每个线程完成的进度

threadInfo.setFinished(threadInfo.getFinished() + len);

// 設置爲500毫米更新一次

if (System.currentTimeMillis() - time > 1000) {

time = System.currentTimeMillis();

// 发送已完成多少

intent.putExtra("finished", mFinished * 100 / mFileInfo.getLength());

// 表示正在下载文件的id

intent.putExtra("id", mFileInfo.getId());

Log.i("test", mFinished * 100 / mFileInfo.getLength() + "");

// 發送廣播給Activity

mComtext.sendBroadcast(intent);

}

if (mIsPause) {

mDao.updateThread(threadInfo.getUrl(), threadInfo.getId(), threadInfo.getFinished());

return;

}

}

}

// 标识线程是否执行完毕

isFinished = true;

// 判断是否所有线程都执行完毕

checkAllFinished();

好了,这样整个DownloadTask的代码就修改完了,接下来我们开始修改DownloadService中的代码了。

DownloadService代码修改

在之前的代码中是使用单线程下载,现成我们设置成可以定义多少条线程下载,因为在Handler中的启动下载的时候需要添加线程数。

同时我们要在DownloadService定义一个Map的集合,用于管理下载线程,代码如下:

private Map mTasks = new LinkedHashMap();

修改Handler代码,在启动下载线程时,添加进下载集合中,代码如下:

switch (msg.what) {

case MSG_INIT:

FileInfo fileInfo = (FileInfo) msg.obj;

Log.i("test", "INIT:" + fileInfo.toString());

// 獲取FileInfo對象,開始下載任務

DownloadTask task = new DownloadTask(DownloadService.this, fileInfo, 3);

task.download();

// 把下载任务添加到集合中

mTasks.put(fileInfo.getId(), task);

break;

}

最后要修改onStartCommand代码,当我们点击停止的时候,要停止一个文件中每一个正在运行中的线程,在点击开始的时候要用线程池启动下载,代码如下:

if (ACTION_START.equals(intent.getAction())) {

FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");

Log.i("test", "START" + fileInfo.toString());

InitThread initThread = new InitThread(fileInfo);

DownloadTask.sExecutorService.execute(initThread);

} else if (ACTION_STOP.equals(intent.getAction())) {

FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");

DownloadTask task = mTasks.get(fileInfo.getId());

if (task != null) {

// 停止下载任务

task.mIsPause = true;

}

}

这样DownloadService中的代码也修改完了,只剩下最后修改MainActivity中的代码了

修改MainActivity代码

这次我们只需修改广播接收者的代码就可以了,但我们更新进度的时候不能按照单一文件的时候更新了,我们必须按照文件的id来更新进度,这时我们可以调用FileAdapter中的updataProgress方法(前面自己定义的)便可以更新。同时我们还要在文件完成时弹出文件已完成的Toast,因此要给广播增加Action。

在onCreate中修改注册广播的代码:

IntentFilter intentFilter = new IntentFilter();

intentFilter.addAction(DownloadService.ACTION_UPDATE);

intentFilter.addAction(DownloadService.ACTION_FINISHED);

registerReceiver(mRecive, intentFilter);

修改广播接收者的代码:

public void onReceive(Context context, Intent intent) {

if (DownloadService.ACTION_UPDATE.equals(intent.getAction())) {

// 更新进度条的时候

int finished = intent.getIntExtra("finished", 0);

int id = intent.getIntExtra("id", 0);

mAdapter.updataProgress(id, finished);

} else if (DownloadService.ACTION_FINISHED.equals(intent.getAction())){

// 下载结束的时候

FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");

mAdapter.updataProgress(fileInfo.getId(), 0);

Toast.makeText(MainActivity.this, mFileList.get(fileInfo.getId()).getFileName() + "下载完毕", Toast.LENGTH_SHORT).show();

}

}

大功告成!一个多线程多文件下载的项目就这样解决了,满满的成就感对不_。

但是且慢,我们还有一个通知栏没解决,等我们把通知栏做好再高兴也不迟

Notification通知栏的使用

在低版本中,Android使用通知栏是Notification这个API,但是在高版本中使用的是Notification.Builder这个API,两种区别不大,在这里使用的是低版本的Notification。

Notificaiton布局

使用通知栏必须要有个布局,但我们下拉通知栏的时候,如播放音乐,我们可以看到有上一首、下一首等按键。所以就像使用ListView一样,我们首先要定义自己的通知栏布局,布局效果如下

Notification.png

这是一个很简单的布局,一个TextView,一个ProgressBar,两个Button就解决了。需要说明的是,写这个布局与普通布局并无不同,布局代码如下:

android:layout_width="match_parent"

android:layout_height="match_parent"

android:orientation="vertical" >

android:id="@+id/file_textview"

android:layout_width="wrap_content"

android:layout_height="wrap_content" />

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:orientation="horizontal" >

android:id="@+id/progressBar2"

style="?android:attr/progressBarStyleHorizontal"

android:layout_width="0dp"

android:layout_height="wrap_content"

android:layout_weight="2" />

android:id="@+id/start_button"

android:layout_width="0dp"

android:layout_height="wrap_content"

android:layout_weight="1"

android:text="start" />

android:id="@+id/stop_button"

android:layout_width="0dp"

android:layout_height="wrap_content"

android:layout_weight="1"

android:text="stop" />

布局写好了,我们来定义如何操作这个视图吧!

NotificationUtil工具类

在通知栏中我们要向Activity一样找到这个布局,然后操纵它。但是不同的是通知栏使用的是RemoteViews远程视图这个API来控制视图的。

另外在新建Notification对象的时候,要设置好几个参数,这些参数是显示在状态栏中的。当QQ或者其他来通知的时候,许多时候并不是直接弹出对话框的,而是在状态栏中弹出一个提示,这就是Notification设置的参数。

好了Notification介绍到这里,下面就是NotificationUtil的完整代码:

public class NotificationUtil {

private Context mContext;

private NotificationManager mNotificationManager = null;

private Map mNotifications = null;

public NotificationUtil(Context context) {

this.mContext = context;

// 获得系统通知管理者

mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

// 创建通知的集合

mNotifications = new HashMap();

}

/**

* 显示通知栏

* @param fileInfo

*/

public void showNotification(FileInfo fileInfo) {

// 判断通知是否已经显示

if(!mNotifications.containsKey(fileInfo.getId())){

Notification notification = new Notification();

notification.tickerText = fileInfo.getFileName() + "开始下载";

notification.when = System.currentTimeMillis();

notification.icon = R.drawable.ic_launcher;

notification.flags = Notification.FLAG_AUTO_CANCEL;

// 点击通知之后的意图

Intent intent = new Intent(mContext, MainActivity.class);

PendingIntent pd = PendingIntent.getActivity(mContext, 0, intent, 0);

notification.contentIntent = pd;

// 设置远程试图RemoteViews对象

RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(), R.layout.notification);

// 控制远程试图,设置开始点击事件

Intent intentStart = new Intent(mContext, DownloadService.class);

intentStart.setAction(DownloadService.ACTION_START);

intentStart.putExtra("fileInfo", fileInfo);

PendingIntent piStart = PendingIntent.getService(mContext, 0, intentStart, 0);

remoteViews.setOnClickPendingIntent(R.id.start_button, piStart);

// 控制远程试图,设置结束点击事件

Intent intentStop = new Intent(mContext, DownloadService.class);

intentStop.setAction(DownloadService.ACTION_STOP);

intentStop.putExtra("fileInfo", fileInfo);

PendingIntent piStop = PendingIntent.getService(mContext, 0, intentStop, 0);

remoteViews.setOnClickPendingIntent(R.id.stop_button, piStop);

// 设置TextView中文件的名字

remoteViews.setTextViewText(R.id.file_textview, fileInfo.getFileName());

// 设置Notification的视图

notification.contentView = remoteViews;

// 发出Notification通知

mNotificationManager.notify(fileInfo.getId(), notification);

// 把Notification添加到集合中

mNotifications.put(fileInfo.getId(), notification);

}

}

/**

* 取消通知栏通知

*/

public void cancelNotification(int id) {

mNotificationManager.cancel(id);

mNotifications.remove(id);

}

/**

* 更新通知栏进度条

* @param id 获取Notification的id

* @param progress 获取的进度

*/

public void updataNotification(int id, int progress) {

Notification notification = mNotifications.get(id);

if (notification != null) {

// 修改进度条进度

notification.contentView.setProgressBar(R.id.progressBar2, 100, progress, false);

mNotificationManager.notify(id, notification);

}

}

}

通知栏的工具类已经写好了,现在就是使用它的时候了。我们要在Activity中点击下载的时候就弹出通知栏,下面我们就来修改DownloadService和MainActivity中的代码来启动通知栏吧。

修改DownloadService

要在点击开始下载,启动下载任务的时候弹出通知栏,我们所要知道的是如何收到开始的信号。

1、当点击开始下载的按键时,在FileAdapter的startButton传出一个ACTION_START的信号,并启动服务。

2、然后在DownloadService中的onStartCommand方法中接到信号,然后启动InitThread初始化线程。

3、在InitThread启动之后会获得FileInfo的实例,里面包含所要下载的文件的长度,然后InitThread通过Message将FileInfo实例传递给Handler。

4、在Hanlder中开启DownloadTask下载线程任务。

这个下载任务绕来绕去,还挺令人迷惑的,不过好在我们都知道它是走哪一条路了。所以在第4步,Handler开启下载任务的时候,我们就发出一个通知,告诉大家:下载已经开始啦!

代码如下:

Handler mHandler = new Handler() {

public void handleMessage(android.os.Message msg) {

switch (msg.what) {

case MSG_INIT:

···

// 发送启动下载的通知

Intent intent = new Intent(ACTION_START);

intent.putExtra("fileInfo", fileInfo);

sendBroadcast(intent);

break;

}

};

};

分析了一堆,我们终于获得了开始下载的通知了,然后就能在MainActivity中的广播接收器中接收到这条广播,然后弹出通知栏

MainActivity中开启通知栏

首先我们要接收到开启下载ACTION_START的这条广播,但是之前注册的广播接收器并没有包含这条广播,因此要添加这条代码:

intentFilter.addAction(DownloadService.ACTION_START);

然后我们需要一个NotificationUtil成员对象,在onCreate中初始化它。

最后我们修改广播接收者内部类的代码,代码如下:

class UIRecive extends BroadcastReceiver {

@Override

public void onReceive(Context context, Intent intent) {

if (DownloadService.ACTION_UPDATE.equals(intent.getAction())) {

// 更新进度条的时候

int finished = intent.getIntExtra("finished", 0);

int id = intent.getIntExtra("id", 0);

mAdapter.updataProgress(id, finished);

mNotificationUtil.updataNotification(id, finished);

} else if (DownloadService.ACTION_FINISHED.equals(intent.getAction())){

// 下载结束的时候

FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");

mAdapter.updataProgress(fileInfo.getId(), 0);

Toast.makeText(MainActivity.this, mFileList.get(fileInfo.getId()).getFileName() + "下载完毕", Toast.LENGTH_SHORT).show();

// 下载结束后取消通知

mNotificationUtil.cancelNotification(fileInfo.getId());

} else if (DownloadService.ACTION_START.equals(intent.getAction())){

// 下载开始的时候启动通知栏

mNotificationUtil.showNotification((FileInfo) intent.getSerializableExtra("fileInfo"));

}

}

}

这是我们开始下载的时候就能弹出通知栏,来下载进行时能更新通知栏的进度,最后下载完成能够自动取消通知栏。

一个多线程多文件外加通知栏显示的下载器终于完成了,可以直接测试了。

总结

一个小小的简陋的项目终于完成了!但是对于刚入门的小伙伴们相信还是废了不少的功夫。

在这个项目中,我们运用的不再是单一的组件只是,而是将组件综合运用起来,如何在listView中操作,数据库如何增删改查,Service如何与Activity通信,Notification通知栏又是怎样显示的····

这些组件我们都刷了一遍,相信下次再次使用的时候就不会像刚开始一样无从下手了。

这个项目看上去貌似不错,但仔细思量仍是有种种的不足之处,还拥有一些BUG待解决。而且在Activity与Service之间的通信用BroadCast广播,虽然会更简单些,但对于真正的项目而已可能不是这样的。

因为广播是系统组件,这样大材小用是资源的浪费,而且效率是偏低的。在一个项目中的单线程多进程中,应该使用Handler加上Messenger进行通信的,这有待于大家学习。

好了,话就说到这里,这个项目的Github地址是:

https://github.com/liaozhoubei/MultiDownload

欢迎大家下载,如果发现有BUG,也可以通知我

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值