本文續接我上一篇文章《Android實戰:簡易斷點續傳下載器實現》
鏈接地址:http://www.lxweimin.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代碼如下
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
至于Item的布局,為了省功夫,就不寫了,大家可以去我的Github下載名為MultiDownload的項目來參考。
建立FileAdapter類
布局寫好了,但是ListView總是要有個Adapter類來綁定視圖,填充布局的不是么,所以接下來就開始寫FileAdapter了。
話說回來,ListView真的是個很重要的空間,不熟悉的小伙伴抓緊多看看怎么做吧。
創建一個繼承自BaseAdapter類的FileAdapter,里面擁有以下三個成員變量:
private Context mContext = null;
private List<FileInfo> mFilelist = null;
private LayoutInflater layoutInflater;
然后重寫構造函數:
public FileAdapter(Context mContext, List<FileInfo> 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<FileInfo> 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>();
// 初始化文件對象
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<ThreadInfo> 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<DownloadThread>();
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<Integer, DownloadTask> mTasks = new LinkedHashMap<Integer, DownloadTask>();
修改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就解決了。需要說明的是,寫這個布局與普通布局并無不同,布局代碼如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/file_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<ProgressBar
android:id="@+id/progressBar2"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2" />
<Button
android:id="@+id/start_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="start" />
<Button
android:id="@+id/stop_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="stop" />
</LinearLayout>
</LinearLayout>
布局寫好了,我們來定義如何操作這個視圖吧!
NotificationUtil工具類
在通知欄中我們要向Activity一樣找到這個布局,然后操縱它。但是不同的是通知欄使用的是RemoteViews遠程視圖這個API來控制視圖的。
另外在新建Notification對象的時候,要設置好幾個參數,這些參數是顯示在狀態欄中的。當QQ或者其他來通知的時候,許多時候并不是直接彈出對話框的,而是在狀態欄中彈出一個提示,這就是Notification設置的參數。
好了Notification介紹到這里,下面就是NotificationUtil的完整代碼:
public class NotificationUtil {
private Context mContext;
private NotificationManager mNotificationManager = null;
private Map<Integer, Notification> mNotifications = null;
public NotificationUtil(Context context) {
this.mContext = context;
// 獲得系統通知管理者
mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
// 創建通知的集合
mNotifications = new HashMap<Integer, Notification>();
}
/**
* 顯示通知欄
* @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,也可以通知我