Android四大組件之-服務(part 1)

Android四大組件之-服務(part 1)。以下內容基於7.0系統。

簡介

服務是一個在後臺進行耗時操作的應用程序組件,它並不提供UI。服務可由另一個應用程序組件啟動,並且在後臺持續運行,即使用戶切換至其他的應用程序。應用程序組件可以與服務進行綁定以便與之交互,甚至進行IPC。

服務分為三種類型:

Scheduled(定時服務)

Started(啟動的服務)

一旦一個服務通過startService()啟動後,這個服務便可以不停地運行下去,即使啟動它的組件被摧毀。started service一般用來執行單一,且不需要給調用者返回結果的操作。如下載或上傳一個文件到雲。當這種Service的任務完成後,需要停止該服務。

Bound(綁定的服務)

一旦一個服務通過bindService()與一個應用程序組件綁定後,這個組件便可以與之交互,例如發送請求,接收結果甚至IPC。bound service的生命周期取決於它所綁定的組件的生命周期。多個組件可同時與一個服務綁定,一旦它們全部解綁,服務將會被系統回收。

啟動的服務和綁定的服務看起來好像不相關,實際上一個服務可以既是started也是bound的。

任何一個組件,甚至外部程序的組件都可以通過一個Intent來使用一個服務,就如同通過Intent啟動一個activity一樣。但如果你不想你的服務被外部程序所用,你可以在Manifest文件中將其聲明為private。

使用服務還是後臺線程?

明白這個問題首先應該清楚Service和Thread的定位。

首先Service是系統組件,Thread是程序執行的最小單位。就大部分場景而言,我們要討論的問題實際上是在Activity中創建Thread呢還是在Activity中創建Service然後再去Service中創建Thread。

對於前者,在Activity中創建Thread時,將不得不在onDestroy()方法中關閉線程,否則當Activity銷毀後Thread還在後臺跑,會出現內存泄露和僵屍線程。因此,Activity中Thread的生命周期是和Activity綁定的,也就是說當Activity退出後Thread中的工作也無法執行瞭。而且Thread無法和Activity進行方便的交互,同時該Thread無法被其他的組件調用。

而後者則不會出現這種情況。首先,started service有自己的生命周期,即使Activity被銷毀,也不影響Service的運行,實際上Service的優先級要高於後臺掛起的Activity。而bound service雖然生命周期與綁定的組件有關,但是其提供瞭方便交互的接口。這都是Service優於在Activity中直接創建Thread的地方。

因此,使用服務還是使用線程這個問題的答案取決於具體的場景需求。

這一個小節官方解釋的也不是特別清楚,我加入瞭較多自己的見解,參考瞭這篇文章,感謝原作者。

經過demo試驗,發現如果不再onDestroy中關閉線程,那麼即使Activity被銷毀,隻要其進程還沒來得及被系統銷毀,那麼線程中的任務還是會得到執行。但這無疑是不安全且可能造成內存泄露的。

幾個重要方法和服務須知

onStartCommand()

如果是通過onStart()方法啟動的服務,服務在創建之後會調用該方法。started service在完成工作之後需要調用stopService()或者stopSelf()來停止。註意,不是在服務的onDestroy()方法中調用停止服務,而是在onStartCommand()方法中的邏輯完成之後,返回之前。

如果是單綁定的服務,則不需要實現該方法。因為反正不會調用。

onBind()

通過onBind()方法綁定的服務會調用該方法。為瞭使服務和與之綁定的組件之間進行交互,必須要在onBind()方法中return一個IBinder對象。

如果不需要綁定服務,return null就行。

onCreate()

系統執行該方法來進行服務的一次性初始化工作。一次性的意思是:如果一個服務已經在running瞭,那麼這個方法不會再被調用。

onDestroy()

當一個服務被回收的時候會調用該方法,在該方法中應該進行一些資源回收工作,比如線程,監聽,廣播接收器等。

當系統內存很低並且需要為當前獲得用戶焦點的活動騰資源時,系統會強制停止一個服務。但如果該服務是和獲得用戶焦點的Activity相綁定的,那它被殺掉的可能性會低很多。如果該服務是一個前臺服務,那基本上不太可能被殺掉。當服務started之後,並且進行長時間的耗時操作時,系統會隨著時間的推移降低其在後臺任務中的優先級,那麼服務就極有可能被幹掉,因此如果啟動瞭一個started服務,必須想好如何讓它優雅地面對系統對其的重啟。因為當系統幹掉一個服務後,它會在資源變得不那麼緊張時重啟該服務,當然重啟的模式也取決於onStartCommand()方法的返回值。

註意:隻有started service才有重啟這一說,bound service的生命取決於其綁定的組件。

在Manifest中聲明一個服務

所有的服務都必須在Manifest文件中聲明。(好像廣播就不是,靜態註冊的需要,動態註冊的廣播不需要在Manifest中聲明)


  ...
  
      
      
      ...
  

不要使用隱式Intent,而要使用顯式Intent來啟動服務,並且不要給服務指定標簽。之所以不用隱式Intent啟動服務是因為你不知道除瞭你想要啟動的服務,還有沒有別的服務也能響應這個隱式Intent,而服務又是沒有UI的,用戶根本不知道是否啟用瞭對應的服務,還是意外啟動瞭別的服務。API21以上,在bingService()中傳入一個隱式Intent會拋出異常。

如果不希望本服務被其他程序所啟動,將其exported屬性置false即可。

雖然服務並不提供UI,但用戶是可以看到有哪些服務在運行的,最好在服務的description屬性對該服務進行一些描述,以免被用戶視為可疑服務而將其殺掉。

創建一個started service

所謂started service即通過startService()方法啟動的服務,通過該方式啟動的服務在完成onCreate()方法後會立即調用onStartCommand()方法。

當一個服務被啟動後,它就有瞭獨立的生命周期(不依賴與啟動它的組件)。啟動服務是在startService()方法中可以傳入一個Intent參數用來攜帶數據,服務會在onStartCommand()方法中收到該Intent。當服務完成工作後,應當停止自身,然後等待系統回收。

註意:服務運行在其聲明的程序所在的進程,並且默認運行在主線程中。因此如果服務中進行的是密集型操作或可能阻塞的操作,則有可能影響UI的響應。這時候最好開一個新線程。訪問網絡或進行IO讀寫更不用說瞭,在主線程中出現這類代碼直接拋異常。

通常來講,有兩種方法來實現一個started service

繼承IntentService

大部分的服務不需要能夠同時處理多個請求,因此使用IntentService會比較簡單。

IntentService會:

自動創建子線程,在onHandlIntent()方法中處理傳到onStartCommand()中的Intent。 創建一個隊列來保存傳入的Intent,每次隻派發一個Intent對象到onHandleIntent()方法中,即每次隻運行一個任務,沒有多線程問題。 在所有的Intent處理完之後自動停止服務。 對onBind()方法進行瞭默認實現:return null。因為不需要綁定嘛。 對onStartCommand()方法進行瞭默認實現:將Intent傳到一個工作隊列中,然後一個個傳給onHandleIntent()方法。

例:

public class HelloIntentService extends IntentService {

 /**
  * A constructor is required, and must call the super IntentService(String)
  * constructor with a name for the worker thread.
  */
 public HelloIntentService() {
     super("HelloIntentService");
 }

 /**
  * The IntentService calls this method from the default worker thread with
  * the intent that started the service. When this method returns, IntentService
  * stops the service, as appropriate.
  */
 @Override
 protected void onHandleIntent(Intent intent) {
     // Normally we would do some work here, like download a file.
     // For our sample, we just sleep for 5 seconds.
     try {
         Thread.sleep(5000);
     } catch (InterruptedException e) {
         // Restore interrupt status.
         Thread.currentThread().interrupt();
     }
 }
}

就是這麼簡單,隻需要提供一個構造方法,然後復寫onHandleIntent()來執行業務邏輯即可。IntentService其實就是Service的一個子類,如果你要重寫Service的其他方法,一定要super一下父類的該方法,不然IntentService中的一些代碼就被沖掉瞭。

繼承Service類

這種方式比較復雜,代碼量也更多,但是如果需要同時handle多個start request,即同時handle多個Intent的話,則隻能選擇該方式。

下面這段代碼的和上面的功能一樣,使用Handler強行實現瞭一個隊列。按照繼承Service來創建服務的場景來看,應該在onStartCommand()方法中針對每個Intent都開一個線程,才能實現同時處理多個start requests。

public class HelloService extends Service {
 private Looper mServiceLooper;
 private ServiceHandler mServiceHandler;

 // Handler that receives messages from the thread
 private final class ServiceHandler extends Handler {
     public ServiceHandler(Looper looper) {
         super(looper);
     }
     @Override
     public void handleMessage(Message msg) {
         // Normally we would do some work here, like download a file.
         // For our sample, we just sleep for 5 seconds.
         try {
             Thread.sleep(5000);
         } catch (InterruptedException e) {
             // Restore interrupt status.
             Thread.currentThread().interrupt();
         }
         // Stop the service using the startId, so that we don't stop
         // the service in the middle of handling another job
         stopSelf(msg.arg1);
     }
 }

 @Override
 public void onCreate() {
   // Start up the thread running the service.  Note that we create a
   // separate thread because the service normally runs in the process's
   // main thread, which we don't want to block.  We also make it
   // background priority so CPU-intensive work will not disrupt our UI.
   HandlerThread thread = new HandlerThread("ServiceStartArguments",
           Process.THREAD_PRIORITY_BACKGROUND);
   thread.start();

   // Get the HandlerThread's Looper and use it for our Handler
   mServiceLooper = thread.getLooper();
   mServiceHandler = new ServiceHandler(mServiceLooper);
 }

 @Override
 public int onStartCommand(Intent intent, int flags, int startId) {
     Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();

     // For each start request, send a message to start a job and deliver the
     // start ID so we know which request we're stopping when we finish the job
     Message msg = mServiceHandler.obtainMessage();
     msg.arg1 = startId;//在這裡將startId傳過去,原因下面的部分有介紹。
     mServiceHandler.sendMessage(msg);

     // If we get killed, after returning from here, restart
     return START_STICKY;
 }

 @Override
 public IBinder onBind(Intent intent) {
     // We don't provide binding, so return null
     return null;
 }

 @Override
 public void onDestroy() {
   Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();
 }
}

我個人認為上面這段代碼純粹為瞭展現IntentService的優越性而刻意在Service中實現瞭一個消息隊列(使用Handler的機制)。直接繼承Service的優勢在於能夠創建多個線程同時處理requests。

started service的重啟

如果查看過API的話,會發現onStartCommand()方法有一個int類型的返回值,這個返回值決定瞭started service被系統幹掉後將以什麼樣的方式重啟。該返回值必須是以下三種的其中之一:

START_NOT_STICKY

如果系統幹掉瞭該服務,在資源足夠的情況下不會立即重啟它,除非有一個Intent被傳過來瞭。這種方式可以避免服務在不需要的時候也在後臺跑。

START_STICKY

如果系統幹掉瞭該服務,那麼在資源可用時重啟它,並且調用onStartCommand()方法,但是不要把上一個Intent傳來瞭。實際上,系統會在調用onStartCommand()的時候傳一個null進去。這種模式適合媒體播放類的服務,這類服務在一直在後臺運行,並且等著分配工作。相比上一種模式,這種模式可以減去等待服務啟動的時間,響應上會更快。

START_REDELIVER_INTENT

如果系統幹掉瞭該服務,那麼在資源可用時重啟它,並且調用onStartCommand()方法,並且把上一個Intent傳進來。其他Intent則依次等候或者傳進來按照相應邏輯處理。這種模式比較適合下載類服務。當一個後臺下載服務由於長期運行被系統幹掉的時候,它可能還沒下載完,那麼當其重啟的時候,應當能自動開始之前的下載。這個時候接收之前的Intent,並對下載進度進行判斷之後就可以實現斷點續傳瞭。

開啟一個服務

上面說瞭這麼多,都是在創建一個started service,那如何開啟它呢?

//上面說瞭,不要用隱式Intent
Intent intent = new Intent(this, HelloService.class);
startService(intent);

started service和開啟它的組件之間的唯一紐帶就是startService()傳g進去的Intent參數。如果想讓服務在完成工作後返回一個結果,那麼可以在Intent中封裝一個PendingIntent,那麼當服務完成工作後可以使用這個PendingIntent對象創建一個廣播來傳遞結果。

PendingIntent與廣播???

對同一個服務的多次啟動請求會調用多次onStartCommand()方法,但是停止該服務隻需要調用一次stop方法。

停止一個服務

started service需要自己管理生命周期,系統不會去管它的生命周期,隻會在它stopSelf()或者被其他組件stopService()之後去銷毀它。

如果服務需要處理多個request,那什麼時候調用stopSelf()才是合適的呢?因為總有可能在處理完一個request之後準備停止服務的時候,服務中還在handle另一個request,這時候停止服務會漏掉request。這個時候可以在onStop(int)方法中傳入onStartCommand(Intent,int,int)方法參數中的第三個參數:startId,這個參數我沒看到在哪裡可以指定,應該是系統分配的。這樣就能保證一定會在最後一個request被處理完之後才真正調用stopSelf()。

創建一個綁定服務

綁定服務比較復雜,另開章節介紹

給用戶發通知

使用Toast 使用Status Bar

不多做介紹

前臺服務

前臺服務是一種告訴用戶『我』在運行的服務,這種服務幾乎不太可能被系統幹掉。但同時這個服務也必須在狀態欄顯示一個圖標並且顯示一條通知。除非該前臺服務被停止,否則該通知不會消失。

使用范例:

Intent notificationIntent = new Intent(this, ExampleActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);

Notification notification = new Notification.Builder(this)
    .setContentTitle(getText(R.string.notification_title))
    .setContentText(getText(R.string.notification_message))
    .setSmallIcon(R.drawable.icon)
    .setContentIntent(pendingIntent)
    .setTicker(getText(R.string.ticker_text))
    .build();

startForeground(ONGOING_NOTIFICATION_ID, notification);

註意:startForeground()方法傳入的id不能為0。

要移除這個通知,使用stopForeground()方法,該方法並不會停止這個服務,隻是將其從前臺移除。如果直接調用stopService之類的方法,那麼會既移除這個通知也會停止這個服務。

管理服務的生命周期

服務的生命周期比較簡單,但是因為服務不可見,所以處理起來也要格外小心。

服務的生命周期分兩路:

A started service

這種服務的生命周期從別的組件調用startService()方法那一刻開始,直到stopSelf()或者stopService()執行。

A bound service

這種服務的生命周期取決於其所綁定的組件的生命周期。一旦和任何一個組件綁定,這個服務的生命周期開始;一旦和最後一個組件解綁,這個服務的生命周期結束。

註意:

以上兩條生命周期路線並不是完全分開的,因為一個started service完全可以提供綁定服務。比如可以通過startService()方法啟動一個服務,那這個時候該服務是一個started service。當如果這個服務提供瞭綁定機制,在其已經啟動的情況下,另一個組件再去綁定它,那麼它又成瞭一個bound service。這個時候它的生命周期的結束已經不僅僅取決於stopService()或stopSelf()的調用瞭,還要看其上是否有組件與之綁定。如果所有的綁定組件都已解綁,這個時候stopService()或stopSelf()才能真正停止這個服務。

服務的生命周期方法

Just show me the code!

public class ExampleService extends Service {
    int mStartMode;       // indicates how to behave if the service is killed
    IBinder mBinder;      // interface for clients that bind
    boolean mAllowRebind; // indicates whether onRebind should be used

    @Override
    public void onCreate() {
        // The service is being created
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // The service is starting, due to a call to startService()
        return mStartMode;
    }
    @Override
    public IBinder onBind(Intent intent) {
        // A client is binding to the service with bindService()
        return mBinder;
    }
    @Override
    public boolean onUnbind(Intent intent) {
        // All clients have unbound with unbindService()
        return mAllowRebind;
    }
    @Override
    public void onRebind(Intent intent) {
        // A client is binding to the service with bindService(),
        // after onUnbind() has already been called
    }
    @Override
    public void onDestroy() {
        // The service is no longer used and is being destroyed
    }
}

不同於Activity的生命周期方法,不需要在這裡使用super調用父類的實現,因為父類的實現是空的。

服務的生命周期圖(來自官網)

和Activity一樣,Service的生命周期始於onCreate()方法終於onDestroy()方法。在onCreate()中可以進行一些初始化工作,比如開啟一個線程播放音樂。在onDestroy()中進行資源回收工作,如關閉線程。

一旦一個服務調用瞭onStartCommand()方法或者onBind()方法,其就處於活躍狀態瞭,

started service直到onDestroy()方法調用才退出活躍狀態。也就是說即使onStartCommand()方法返回,started service也處於活躍狀態。而bound service則不同,一旦bound service的onUnbind()方法return,其活躍狀態便結束瞭,接著便等待系統回收。

You May Also Like