本文续接我上一篇文章《Android实战:简易断点续传下载器实现》
链接地址:http://www.jianshu.com/p/5b2e22c42467
本项目Github地址:
https://github.com/liaozhoubei/MultiDownload
说到多线程下载,也许大家会觉得很迷惑,但多线程的原理实际上与单线程下载的原理并无区别。
多线程下载只需要确定好下载一个文件需要多少个线程,一般来说最好为3条线程,因为线程过多会占用系统资源,而且线程间的相互竞争也会导致下载变慢。
其次下载的时候将文件分割为三份(假设用3条线程下载)下载,在java中就要用到上次提到的RandomAccessFile这个API,它的开始结束为止用以下代码确定:
conn.setRequestProperty("Range", "bytes=" + start + "-" + end)
最后就是断点续传了,只需要才程序停止下载的时候记录下最后的下载位置就好了,当下次下载的时候从当前停止的位置开始下载。
OK,那么现在就开始我们的多线程下载+通知栏控制的实战之旅吧!
多线程断点续传下载
我们这次要做的并非简单的多线程下载,而是要做到多文件多线程的同时下载
重写布局
这次下载需要展示多个下载的文件,所以使用ListView控件,界面效果如下
每个ListView的item都很简单,基本上只需要将上次写的下载界面搬过来就好了。
新建一个Layout,命名为item,将中的界面布局剪切过来,然后在中设置一个ListView空间。
activity_main.xml代码如下
至于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一样,我们首先要定义自己的通知栏布局,布局效果如下
这是一个很简单的布局,一个TextView,一个ProgressBar,两个Button就解决了。需要说明的是,写这个布局与普通布局并无不同,布局代码如下:
布局写好了,我们来定义如何操作这个视图吧!
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,也可以通知我