上方廣告

上方連結組

2013年9月4日 星期三

Android Service 介紹

我們在開發 Android 應用程式時, 常會遇到一些特殊需求, 會希望在 Activity 或者應用程式關閉時, 仍然能夠有些服務在進行。例如檢查 Email 是否有新的信件, 或者像即時通訊軟體一般, 一旦有新的訊息時能夠馬上通知使用者。但這時會遇上一個雞生蛋或蛋生雞的問題 : 你的應用程式可能已經被關閉了, 程式已經完全處於生命終結的狀態, 那又該怎麼提供服務呢 ?

為了滿足有這樣需求的應用程式開發, 在 Android 上就提供了 Service 這種機制, 這篇文章就是對 Service 進行一些簡單的介紹。

什麼是 Service ?

Service 簡單來說就是一個在背景執行的元件, 不需要與使用者互動, 所以也沒有操作介面。更重要的是, 它的生命週期是和 Activity 脫勾的, Activity 就算關閉了, Service 仍舊可以繼續運作。

Service 通常用在執行重覆性的事(例如每隔一段時間檢查是否有更新資料), 或者需要執行很久的事情上(例如透過 HTTP 下載大量的資料或檔案)。

Service 有兩種形式 :

Started
當 Service 一旦透過 Context.startService() 啟動後, 這個 Service 就可以無限期的在背景執行, 即使啟動這個 Service 的 Activity 或應用程式被關閉, 這個 Service 仍可以完成你所交付的工作。但一般來說, Service 並不會和它的開啟者有什麼互動, Service 完成後也不會回傳或通知它的開啟者。所以如果一旦 Service 執行完畢, Service 本身應該就會自動停止。

Bind
而 Service 的另一個角色就是負責跨 Process 的功能協作。各個不同的 client 可以透過 Context.bindService() 的方法來使用此應用程式在 Service 裡所提供的功能, 以達到兩個應用程式的互動。

一個 Service 可以被多個不同的應用程式透過 Context.bindService() 的方式綁定。只要有其它應用程式開始綁定這個 Service , 這個 Service 就會啟動。一直到所有的應用程式都不再使用這個服務, 這個服務就會自動被銷毀。

以上兩種形式其實並不衝突, 一個服務可以透過 startService() 的方式被啟動, 然後再被其它應用程式綁定。你只要同時實作 onStartCommand() 與 onBind() 兩個事件即可。

當然, 你也可以在 manifest 檔中將 Service 宣告成 private, 這樣其它應用程式就不能啟動這個服務了。


Thread ? Process ? Service ?

要瞭解 Service , 有許多看起來作用很像的名詞, 如果不先講清楚, 在學習過程中很容易產生概念上的混淆。所以邊簡單的解說一下 :

Process : 中文翻譯成進程或線程。
每一個應用程式啟動時, Android 就會將這個應用程式執行在它獨有的 Process 上。這點和 Linux 是一樣的(因為 Android 就是拿 Linux 改出來的嘛 )。大家看一下 windows 的工作管理員大概就可以理解 Process 是什麼東西了。
Windows  工作管理員中可以看到目前運行許多 Process

Thread : 執行緒
當一個 Process 啟動時, 作業系統會開啟一條執行緒, 負責 UI 處理和運算等所有工作, 這個執行緒稱為主執行緒(main-thread)。但如果我們在主執行緒上執行一件可能需要執行很久的工作, 例如 HTTP 資料傳輸或大量資料運算, 很可能會擔誤主執行緒上其它運算的效率或順暢度 (例如操作介面很可能變頓甚至無法運作)。 所以我們通常會在 Process 中建立其它的執行緒並將工作交付給它, 讓主執行緒上所負責的 UI 能夠正常操作, 以獲得最佳的使用者操作經驗。一般來說我們稱之為 Background Thread (背景執行)。

一個 Process 可以建立超過一個以上的 Thread。 但要注意的是, 雖然 Background Thread 不會影響主執行緒的正常運作, 但若建立過多的 Background Thread 則可能讓 CPU 忙於切換這 Thread 而導致整體運作效能下降。不過整體而言數十個 Thread 甚至數百個 Thread 都還是在可以接受的範圍(視 CPU 的能力與執行工作的性質不同數字會有落差)。

順帶一提, Android 在大部份的情況下是不允許在主執行緒上進行 HTTP 資料存取。一般會建議使用 AsyncTask 或 HandlerThread 方式來處理。

Thread 的生命週期會依附在其所屬的 Process 上。一旦關閉一個 Process, 其附屬運行中的所有 Thread 都會連帶被中止。

Service : 服務
Service 是 Android 提供出來的一個元件。它預設會建立在與 程用程式同一個 Process 的主執行緒上。這點很常被誤解。我們可以透過 Context.startService() 的方法要求系統將指定的工作加入排程, 若執行緒有空時則開始執行。並不會自動建立另一個 Thread 或 Process 去執行。

當然 Android 也有提供方法可以讓我們把 Service 建立在他獨立的 Process 上。這樣運作時就會在該 Process 的主執行緒上, 就不會和原用程式的主執行緒爭資源了。不過就算是使用 Service 獨立的 Process, 在使用 HTTP 時仍必須在 Background Thread 中呼叫。

而 Service 的另一個角色就是負責跨應用程式的功能協作。其它的應用程式可以透過 Context.bindService() 的方法來使用此應用程式在 Service 裡所提供的功能, 以達到兩個應用程式的互動。


我應該用 Service 還是用 Thread ?

Service 通常是用於可在背景執行,  鮮少甚至不需要與使用者產生互動的工作。例如在背景下載一個檔案之類的工作。即使是結束 Activity 時檔案仍未下載完成, 下載的工作也可以在 Service 中被執行完成。

但如果你需要的是希望當應用程式結束時就連帶中止的工作, 例如在程式執行時播放 MP3 當背景音樂, 但一旦使用者結束應用程式, 背景音樂也要被中止。這時你就應該是使用 Thread 而不是 Service 了。順帶一提, 在 Android 官方建議使用 AsyncTask 或者 HandlerThread 來取代 JAVA 傳統的 Thread。若有興趣的話可以看看官方Processes and Threading, 有比較詳細的資訊。


Service 的生命週期

一個 Service 可以被系統啟動的原因有兩個, 當有人呼叫 Context.startService() 方法時, 系統會呼叫 Service 物件內的 onCreate() 方法, 接下來會呼叫 onStartCommand(Intent, int, int) 方法讓 client 端提供參數。一直到 Context.stopService() 方法被呼叫, 或者是 Service 內自己呼叫 stopSelf() 方法才會停止。

另一個被啟動的方式是有其它的 Client 端透過 Context.bindService() 來取得與 Service 的聯結。如果該 Service 不在啟動狀態的話, 這種方式則會啟動這個 Service, onCreate() 的方式這時就會被呼叫, 但這種方式並不會觸發 onStartCommand() 事件。啟動這個 Service 的client 會透過 onBind(Intent) 方法取得 IBinder 物件。接下來就可以透過 IBinder 物件來取得 Service 的事件(透過 call back 的方法)。只要連線不被中斷, Service 就會保持運作。

但之前提到, Service 可以同時支援 Bound 與 Started 兩種模式。在這種情況, Service 會持續運作到被 started 模式被關閉, 同時 bound 模式中已經沒有任何連線時才會結束, 結束時就會觸發 onDestroy() 事件。

Service 的運作優先權相當的高, 一般來說除非系統資源耗畫, 不然 Android 不太會去主動關閉一個已被啟動的 Service。一旦系統有足夠的資源, 被 Android 關閉的 Service 也會被重新啟動。

因為預設 Service 的和應用程式預設是在同一個 process 的主執行緒中, 所以即使 Service 被啟動了, 一旦應用程式被關閉或清除時, Service 也會被清掉。所以我們可以讓 Service 活在自己一個獨立的 process 中。這時我們只要在 Manifest 檔設定 Service 時, 可以加上 process=":YourProcessName" 這個屬性。屬性值以冒號『:』開頭, Android 在啟動 Service 時就會讓它有獨立的 Process

其實一般來說很少需要特別指定另一個 Process 來執行 Service, 除非這個 Service 會對其它應用程式提供服務時才需要。

如何定義 Service


講了這麼多, 看都應該看到睡著了。但是說到怎麼寫, 則是完全沒有畫面一句都還沒提到。其實 Service 的定義很簡單, 大致上只需要三個步驟 :

1. 在 AndroidManifest.xml 檔中新增定義 :
<com.sample.MyService />
這裡的 android:name 必須是你自訂 Service 類別的完整名稱。

2. 實作一個繼承自 android.app.Service 類別的物件
public class MyService extends Service {
   @Override
   public IBinder onBind(Intent intent) {
      return null;
   }

   @Override
   public int onStartCommand(Intent intent, int flags, int startId) {
      //這裡實作你想做的工作

      return Service.START_STICKY;
    }
}

在 onStartCommand 中, 最後會回傳一個常數, 定義在 Service 類別中。這個回傳值是用在如果這個 Service 被 Android 作業系統終止, 你想要怎麼做。 常數值分別為 :

  • Service.START_STICKY : 
    • Service 如果被中止的話會自動重啟。用在 onStartCommand 方法中不需要依賴 Intent 傳入的資料就可以執行的時候( 重啟時重新傳入的 Intent 會是 null )。
  • Service.START_NOT_STICKY : 
    • Servcie 如果被中止的話不啟動, 用在 onStartCommand 方法中所執行的工作需要依賴 Intent 物件內帶進來的參數。
  • Service.START_REDELIVER_INTENT : 
    • 和 START_STICKY 差不多, 但 Android 會在中止 Service 之前將 Intent 保留下來, 等待重啟時再將原本的 Intent 物件交還給 onStartCommand 事件。


3. 在 Activity 中呼叫 startService(Intent) 來執行
 @Override
  public View onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    Intent intent = new Intent(this, MyService.class);
    this.startService(intent);
  }

基本上只要以上三個步驟就可以啟動一個 Service囉。


透過 Bind 方式來啟動 Service


但如果是希望採用 Bind 的方式來啟動一個 Service, 只要在實作 Service 改成以下方法 : 
public class MyService extends Service {
  private final IBinder mBinder = new MyBinder();
  private String info = "";

  @Override
  public IBinder onBind(Intent intent) {
    return mBinder;
  }

  public class MyBinder extends Binder {
    MyService getService() {
      return MyService.this;
    }
  }

  public String getInfo() {
    return info;
  }

} 

基本上就是實作 onBind(Intent) 事件。而 IBinder 介面則負責介接 client 端與 Service 的溝通。上面的作法是用在 client 端與 Service 是在同一個應用程式內時, 可以直接將 Service 的實體透過 Binder 的延伸方法的方式取得。

而 client 的部份, 必須先實作 android.content.ServiceConnection 介面, 如下所示
private ServiceConnection mServiceConnection = new ServiceConnection() {
  
  @Override
  public void onServiceConnected(ComponentName className, IBinder binder) {
    //這裡就可以取出 service 的實體
    service = ((MyService.MyBinder)binder).getService();
  }
 
  @Override
  public void onServiceDisconnected(ComponentName className) {
    service = null;
  }
} 

而負責啟動的 Activity 中, 我們可以在 onResume 與 onStop 的事件中管理與 Service 的連線
private ServiceConnection mServiceConnection = new ServiceConnection() {
  
  @Override
  public void onResume() {
    super.onResume();
    Intent intent = new Intent(this, MyService.class);
    this.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
  }
 
  @Override
  public void onPause() {
    super.onPause();
    this.unbindService(mServiceConnection);
  }
} 

但這個範例雖然可以透過 IBinder 取回 Service 的實體, 但只能用在同一個應用程式內, 跨不同的應用程式無法將 binder 轉型成指定的物件形態, 所以無法使用這個方法來溝通。所以跨應用程式的話我們通常會使用 Service 搭配 Broadcast 與 BroadcastReceiver 的方式來溝通, 就不在這裡說明了。

上面的完整程式可以在這裡下載, 有興趣的可以下載來看看。

關於 Service 的應用, 可以參考另一篇文章

參考資料
Android Developer : Services
vogella.com : Android Service Tutorial
Android Developer : Processes and Threads
Android Develper : Service

1 則留言:

  1. 可以透過aidl去bind不同APK的service.....
    跨process不只broadcast....

    回覆刪除