一、前言
原理其實大家都懂,只不過沒動手實際好好的寫過,項目中也沒有涉及到用這塊內容,所以....所以被人問及細節時,就說不清個123了,為了一改我的慵懶,因此,我寫這篇文章,至少下次再被問起時,不會尴尬。
本篇文章會涉及到以下知識點:
1. Service (兩種啟動方法,對應的不同生命周期不同);
2. Binder;
3. Activity如何與Service交互;
4. Service如何更新帶進度條的狀態欄;
二、Service & Binder
2.1 Service
Service有兩個方法來啟動:startService 和 bindService,采用不同的方法,service的生命周期也不同(本篇只講同進程,不講跨進程):
1. startService啟動,其生命周期不會因啟動它的組件Destroy而消亡,而是依賴於mainThread(即應用主線程),一但主線程退出,即代表整個應用退出,因為Service就會Destroy。
2. bindService啟動,其生命周期依賴啟動它的組件,組件Destroy時,Service也隨之一起Destroy。
2.2 Binder
Binder是Android系統中一個重要的“設備”,之所以加引號,實際上它是虛擬出來的,類似於Linux中的塊設備,因此,它也是基於IO的。
Binder在Android中,是被用做進程間通信使用的,而且,Binder是Parcelable的,通過Transaction,與它的代理端,即Binder Server端交互,本章只是簡單的使用Binder來做同一進程中的線程間通信。
三、Activity與Service交互
Question:如何將Service用做後台下載,其生命周期不依賴啟動它的組件,且能夠與它的組件相互通信?
分析問題:
該問題,表述了三點信息:
1. 後台下載;
2. 生命周期不依賴其它組件;
3. 數據交互;
3.1 後台下載
通常,我們使用Service,會有這麼幾點需求:
1. 若是前台Service,一般是用來做類似於音樂播放器的;
2. 若是後台Service,則通常是用來和服務器進行交互(數據下載),或是其它不需要用戶參與的操作;
同一進程中,啟動Service,若直接與服務器交互,則很容易引起ANR,因為,Service是由mainThread創建出來,因此,此時Service是運行在UI主線程的,如果需要聯網下載,則需要開啟一個Thread,然後在子線程中來運行。在Service中創建/使用線程,與在Activity中一樣,無區別。
3.2 生命周期不依賴其它組件
這點,我前面說過了,使用startService來啟動該service就行;
3.3 數據交互
組件通常是Activity,可以通過bindService,當成功綁定時,可以獲取Service中定義後的一個IBinder接口,我們可以通過這個接口,返回該Service對象,從而,可以直接訪問該Service中的公有方法;
當Service想要把數據傳遞給某個組件時,最簡單最好的辦法就是通過Broadcast,在Intent中帶上數據,廣播給組件即可(記住,BroadcastReceiver中,onReceive也不能運行太久,否則也會ANR,只有10秒哦)。
四、Service刷新帶有進度條的狀態欄
通常,我們會發一些Notification到系統狀態欄上,以提醒用戶做一些事情,但是,如果大家仔細看了Notification的參數,就會發現裡面有一個RemoteViews類型的成員,是不是有點像在哪見過?對的,如果你做個Widget應用,那麼RemoteViews你應該很熟悉:
RemoteViews可以讓我們自定義一個View,裡面放一些小的控件,系統有定義的,不是所有的控件都能放!那麼,我們就可能自定義一個帶有ProgressBar的layout,然後綁定到Notification對象上,並通過NotificationManager來通知更新即可。
注:網上有提醒說,建議不要更新太頻繁,否則會使系統很卡!
五、用例子說話
本節,就將寫一個Demo,帶大家一起了解如何活用以上這些概念,能夠讓大家應用到將來自己的項目中。文件不多,三個類,一個Service,一個Activity,和一個任務類(因為我在Service中,創建了一個線程隊列,使用單線程來模擬)。
5.1 DownloadManagerActivity
對應的layout:
[html]
<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"
tools:context=".DownloadManagerActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="@string/hello_world" />
<Button
android:id="@+id/add_task"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/addTask"/>
<Button
android:id="@+id/cancel_task"
android:layout_toRightOf="@id/add_task"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancelTask"/>
</RelativeLayout>
裡面主要有兩個Button,一個告訴Service添加任務,一個告訴Service取消指定的任務。
[java]
public final static String TAG = "DownloadService";
private DownloadService mService = null;
private static int task_count = 0;
private final static String ACTION_UPDATE = "com.chris.download.service.UPDATE";
private final static String ACTION_FINISHED = "com.chris.download.service.FINISHED";
幾個對象,mService就是當bindService成功時,通過IBinder返回Service對象,ACTION_XXX用來接收Service發送的廣播,在Activity中動態注冊廣播。
[java]
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_download_manager);
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_UPDATE);
filter.addAction(ACTION_FINISHED);
registerReceiver(myReceiver, filter);
Intent it = new Intent(this, DownloadService.class);
startService(it);
Button add_task = (Button) findViewById(R.id.add_task);
add_task.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View arg0) {
TaskInfo ti = new TaskInfo();
ti.setTaskId(task_count++);
ti.setTaskName(TAG + ti.getTaskId());
ti.setProgress(0);
ti.setStatus(TaskInfo.WAITING);
mService.addTaskInQueue(ti);
}
});
Button cancel_task = (Button) findViewById(R.id.cancel_task);
cancel_task.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View arg0) {
int index = (int) (Math.random() * task_count);
mService.cancelTaskById(index);
}
});
}
一開始,動態注冊一下BroadcastReceiver,指定接收兩個ACTION;然後,startService啟動一個Service。自定義BroadcastReceiver:
[java]
private BroadcastReceiver myReceiver = new BroadcastReceiver(){
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(ACTION_UPDATE)){
int progress = intent.getIntExtra("progress", 0);
Log.d(TAG, "myReceiver - progress = " + progress);
}else if(intent.getAction().equals(ACTION_FINISHED)){
boolean isSuccess = intent.getBooleanExtra("success", false);
Log.d(TAG, "myReceiver - success = " + isSuccess);
}
}
};
在onResume時,去bindService:
[java]
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "Activity onResume");
Intent it = new Intent(this, DownloadService.class);
bindService(it, mServiceConn, BIND_AUTO_CREATE);
}
並在onDestroy時,unbindService,以及unregisterReceiver:
[java]
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(mServiceConn);
//stopService(new Intent(this, DownloadService.class));
unregisterReceiver(myReceiver);
}
ServiceConnection代碼:
[java]
public ServiceConnection mServiceConn = new ServiceConnection(){
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = ((DownloadService.ServiceBinder)service).getService();
Log.d(TAG, "onServiceConnected: mService = " + mService);
if(mService != null){
mService.notifyToActivity(false, true);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
mService = null;
}
};
如果成功了,就通過IBinder接口,獲得Service對象。
5.2 DownloadService
繼承Service類,override一些方法:
[java]
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "onBind");
return mBinder;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand");
return START_STICKY;
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate");
mBinder = new ServiceBinder();
mDownloadQueue = new ArrayList<TaskInfo>();
mNotificationManager = (NotificationManager) getSystemService(
android.content.Context.NOTIFICATION_SERVICE);
mNotification = new Notification();
mRemoteView = new RemoteViews(this.getPackageName(), R.layout.remote_view_layout);
}
@Override
public void onDestroy() {
super.onDestroy();
mBinder = null;
mDownloadQueue = null;
mNotificationManager = null;
mNotification = null;
mRemoteView = null;
Log.d(TAG, "onDestroy");
}
我們通過startService來啟動,因此,啟動流程為:onCreate -> onStartCommand(注:onStart在API5以後,就不在用了,取而代之的是onStartCommand)。
然後,我們bindService,此時service已經啟動,所以,只會調用onBind。
通常,我們應該在onCreate中,去完成一些初始化,而在onDestroy中,去釋放這些內存,因為一但Service運行起來,再去掉startService或bindService,系統就不會再去調用onCreate了,但是onStartCommand或onBind仍舊會被調用。
內部類ServiceBinder,只有一個公有方法,用來返回當前的Service對象:
[java]
public class ServiceBinder extends Binder{
public DownloadService getService(){
return DownloadService.this;
}
}
提供給外部組件的公有方法:
[java]
public void notifyToActivity(boolean update, boolean finished){
bNotifyWhenUpdate = update;
bNotifyWhenFinished = finished;
}
public void addTaskInQueue(TaskInfo ti){
if(mDownloadQueue != null){
mDownloadQueue.add(ti);
Log.d(TAG, "addTaskInQueue id = " + ti.getTaskId());
}
if(isRunning == false && mDownloadQueue.size() > 0){
startDownload();
}
}
public void cancelTaskById(int id){
Log.d(TAG, "cancelTaskById id = " + id);
for(int i = 0; i < mDownloadQueue.size(); i ++){
TaskInfo ti = mDownloadQueue.get(i);
if(ti.getTaskId() == id){
if(ti.getStatus() == TaskInfo.RUNNING){
ti.setStatus(TaskInfo.CANCELED);
}else{
mDownloadQueue.remove(i);
}
break;
}
}
}
三個方法:添加任務,取消任務,是否需要通知給已經綁定的組件。
接下來,就是我們的線程了,這裡的線程是單線程,使用私有的線程隊列
[java]
private void startDownload(){
if(isRunning){
return;
}
new Thread(new Runnable(){
@Override
public void run() {
while(mDownloadQueue != null && mDownloadQueue.size() > 0){
isRunning = true;
TaskInfo ti = mDownloadQueue.get(0);
while(ti.getProgress() < 100 && ti.getStatus() != TaskInfo.CANCELED){
Message msg = mHandler.obtainMessage(DOWNLOAD_STATUS_UPDATE, ti);
mHandler.sendMessage(msg);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ti.setProgress(ti.getProgress()+10);
}
if(ti.getProgress() == 100 && mDownloadQueue.size() == 1){
Log.d(TAG, ti.getTaskName() + " is finished!");
Message msg = mHandler.obtainMessage(DOWNLOAD_STATUS_SUCCESS, ti);
mHandler.sendMessage(msg);
}else if(ti.getStatus() == TaskInfo.CANCELED){
Log.d(TAG, ti.getTaskName() + " is canceled!");
}
if(mDownloadQueue != null){
mDownloadQueue.remove(ti);
}
}
isRunning = false;
}
}).start();
}
通過Thread.sleep(1000)來模擬網絡,並使用Thread / Handler的模式,來更新Notification的RemoteViews。
Handler的實現:
[java]
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch(msg.what){
case DOWNLOAD_STATUS_UPDATE:
{
mNotification.icon = R.drawable.ic_launcher;
mNotification.when = System.currentTimeMillis();
mNotification.tickerText = "開始下載...";
// 放置在"正在運行"欄目中
mNotification.flags = Notification.FLAG_ONGOING_EVENT;
TaskInfo ti = (TaskInfo) msg.obj;
Log.d(TAG, "update : progress = " + ti.getProgress());
mRemoteView.setImageViewResource(R.id.ivIcon, R.drawable.ic_launcher);
mRemoteView.setTextViewText(R.id.tvName, ti.getTaskName());
mRemoteView.setProgressBar(R.id.pbProgress, 100, ti.getProgress(), false);
mRemoteView.setTextViewText(R.id.tvProgress, ti.getProgress() + "%");
mNotification.contentView = mRemoteView;
mNotificationManager.notify(NOTIFY_ID, mNotification);
notifyUpdate(ti);
break;
}
case DOWNLOAD_STATUS_SUCCESS:
{
mNotification.flags = Notification.FLAG_AUTO_CANCEL;
mNotification.contentView = null;
Intent it = new Intent(DownloadService.this, DownloadManagerActivity.class);
PendingIntent pi = PendingIntent.getActivity(DownloadService.this, 0, it, PendingIntent.FLAG_UPDATE_CURRENT);
mNotification.setLatestEventInfo(DownloadService.this, "下載完成", "文件已下載完畢", pi);
mNotificationManager.notify(NOTIFY_ID, mNotification);
notifyFinished(true);
break;
}
case DOWNLOAD_STATUS_FAILED:
{
mNotification.flags = Notification.FLAG_AUTO_CANCEL;
mNotification.contentView = null;
Intent it = new Intent(DownloadService.this, DownloadManagerActivity.class);
PendingIntent pi = PendingIntent.getActivity(DownloadService.this, 0, it, PendingIntent.FLAG_UPDATE_CURRENT);
mNotification.setLatestEventInfo(DownloadService.this, "下載失敗", "", pi);
mNotificationManager.notify(NOTIFY_ID, mNotification);
notifyFinished(false);
break;
}
default:
break;
}
}
};
通知組件新的情況:
[java]
private void notifyUpdate(TaskInfo ti){
if(bNotifyWhenUpdate){
Intent it = new Intent(ACTION_UPDATE);
it.putExtra("progress", ti.getProgress());
DownloadService.this.sendBroadcast(it);
}
}
private void notifyFinished(boolean isSuccess){
if(bNotifyWhenFinished){
Intent it = new Intent(ACTION_FINISHED);
it.putExtra("success", isSuccess);
DownloadService.this.sendBroadcast(it);
}
}
5.3 TaskInfo類
[java]
package com.chris.download.service.Bean;
import java.io.Serializable;
public class TaskInfo implements Serializable {
private static final long serialVersionUID = -2810508248527772902L;
public static final int WAITING = 0;
public static final int RUNNING = 1;
public static final int CANCELED = 2;
private int taskId;
private String taskName;
private int progress;
private int status;
public int getTaskId() {
return taskId;
}
public void setTaskId(int taskId) {
this.taskId = taskId;
}
public String getTaskName() {
return taskName;
}
public void setTaskName(String taskName) {
this.taskName = taskName;
}
public int getProgress() {
return progress;
}
public void setProgress(int progress) {
this.progress = progress;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
}
六、總結
本篇只是帶大家入門,仍有許多可以改進的地方,如:使用多線程以及如何同步線程隊列,多線程對應在狀態欄上的多個RemoteViews更新,Activity中顯示下載任務隊列及其各任務的狀態等。