AndroidのServiceについて

Androidのサービスについて、ちょっと調べてみた。
サービス自身を、それを使うActivityと一緒のアプリケーションとして使うローカルサービスと別のアプリケーションとして動かすリモートサービスがあるようだが、今回はローカルサービスについて。

【補足:2010.09.17】
Remote Serviceについては“AndroidのRemote Serviceについて(+作り方)”にメモしておいた。(ただし、Service全般とLocal Serviceについてはここにメモしている。)

Androidのページ:http://developer.android.com/intl/ja/reference/android/app/Service.htmlに詳しく書いてある。要点だけを私なりにまとめてみた。(といっても和訳らしきものになってしまったが、和訳するつもりではないので、内容の正確さは保証しない。あくまでも私が理解した内容である。あくまでも個人的に後から読み直せるように備忘録としてメモっている。)

Serviceとは何か?

まず、Serviceが以下のことでは“ない”ということに気をつけよう:

  • Serviceは個別のプロセスではない。Serviceオブジェクトは独自のプロセスで実行すること意味しているのではなく、アプリケーションの一部分として同じプロセスで動く。(コメント:リモートサービスとして別プロセスとして動かすこともできるのでないか?)
  • Serviceはスレッドではない。つまり(Application Not Respondingエラーを回避するために)メインスレッドの仕事を肩代わりするわけではない。(コメント:と言ってもサービスの中でスレッドを作ることは出来るのではないか?)

Serviceそれ自身は大変シンプルで、次の2つの仕組みを提供する:

  • アプリケーションが何かをバックグラウンドで実行することをシステムに対して伝える仕組み(ユーザがアプリケーションと直接対話していない場合でも)。これは Context.startService() を呼び出すことで実現し、Service自身または何か他のものが明示的にServiceを中断するまで動き続けるようにシステムに依頼する。
  • アプリケーションがその機能の一部を他のアプリケーションから見えるようにする仕組み。これは Context.bindService() を呼び出すことで実現し、Serviceと情報をやりとりするように長期間のコネクションを作ることができる。

Service自身は大変シンプルであり、ServiceをローカルなJavaオブジェクトとして直接メソッドを呼び出す単純な方法から、AIDLを利用したリモート・インタフェースを提供する方法まで、必要に応じて単純な、もしくは複雑な方法でServiceと情報をやりとりできる。

Serviceのライフサイクル

システムは2つの方法でServiceを実行することができる。一つは誰かが Context.startService() を呼び出す。するとシステムがServiceを探し(Serviceを生成し必要に応じて onCreate() メソッドを呼び出し)、クライアントが渡した引数でServiceの onStartCommand(Intent, int, int) メソッドを呼び出す。Serviceは Context.stopService() または stopSelf() が呼び出されるまで走り続ける。複数の Context.startService() 呼び出し(つまり対応する複数の onStartCommand()呼び出し)はネストされないので、何回スタートされても1回のContext.stopService() または stopSelf()で停止する。ただし、ServiceがstopSelf(int)を使っても、開始されたintentが処理されるまでは停止しない。

起動されたServiceには実行方法を決定する更に2つのモードがあり、onStartCommand()の返り値で決まる:START_STICKY は明示的に起動され、明示的に停止されるサービスが使う。一方、START_NOT_STICKY または START_REDELIVER_INTENT は送られてきたコマンドを処理する間だけ実行するServiceが使う。

もう一つはクライアントがContext.bindService()を使って、Serviceとの永続的な接続を取得する。もし、Serviceが既に実行されていなければ、まずServiceを生成する(その過程でonCreate()を呼び出す)が、 onStartCommand()は呼び出さない。Service側の onBind(Intent) の返り値として、クライアントは IBinderオブジェクトを受け取り、Serviceに対してコールバックできる。(たとえクライアントがServiceのIBinderを保持していなくても)コネクションが確立している限りServiceは実行を続ける。一般的にはIBinderはAIDLを使ったより複雑なインタフェースで使われる。

このようにServiceは起動され、Serviceにバインドされたコネクションを持つことができる。Serviceが明示的に起動されるか、 Context.BIND_AUTO_CREATEフラグを持った1つもしくは複数のコネクションがある限りシステムはServiceを実行し続けるように保つ。どちらの状態も保てなくなると(明示的な中断があるか、全てのコネクションがなくなるか)、ServiceのonDestroy()メソッドが呼び出され、Serviceは事実上終了する。onDestroy()からリターンするまでに全ての後片付け(スレッドの停止、レシーバの登録解除)が完了していなければならない。

プロセスのライフサイクル

Serviceが明示的に起動されてから、もしくはクライアントによりバインドされてから、AndroidシステムはServiceを実行しているプロセスを出来る限り長期間存続させようとする。メモリが不足して既存のプロセスをkillする必要がでると、Serviceを実行しているプロセスの優先順位は次のように扱われる可能性が高い:

  • もしServiceが現在 onCreate()、onStartCommand()、onDestroy()のコードを実行中である場合は、実行中のコードをkillすることのないように、Serviceを実行しているプロセスはフォアグラウンド・プロセスになる。
  • もしServiceが起動されているが、Serviceを実行しているプロセスが、ユーザが画面で見ているプロセスに比べ重要ではない見なされる場合、ただし、画面に現れてない他のどのプロセスよりも重要と見なされる場合。一般にはごく少数のプロセスだけが画面上でユーザに見えているので、この場合は、余程メモリが不足しない限りはServiceはkillされない。
  • もしクライアントがServiceにバインドしている場合には、バインドしているなかの最も重要なクライアントよりも重要でなくなることはない。つまり、クライアントの一つが画面に表示されていれば、サービス自身も画面に表示されているのと同等に扱われる。
  • 起動されているServiceは startForeground(int, Notification) API を使い、Serviceをフォアグランド状態に置くことができ、そうすることでシステムはServiceをユーザが認識中であると見なし、メモリ不足でkillする候補には入れない。(それでも、フォアグランドのアプリケーションによる極端なメモリ不足でServiceがkillされる論理的な可能性はあるが、実際的には懸念しなくても良い。)

つまり、まとめると、

  • サービスはリモートだけではなく、ローカルなものもあり、従来のコンピュータで言う“サービス”とはちょっと違うよ。
  • サービスの起動にはstartServiceによる方法とbindServceによる方法の2種類があるよ。
  • startServiceによる起動は、stopServiceなどが呼び出されるまでサービスが動き続けるよ。
  • bindServieはサービスの起動とサービスへのコネクッションの確立を一気に行うよ。だけど、コネクションが切れたところでサービスも終了するよ。
  • bindServiceではサービス側からクライアント側にIBindオブジェクトが伝えられるよ。IBindオブジェクトにはサービスのオブジェクトが含まれ、これを使ってサービスにコールバックできるよ(サービスのメソッドを呼び出せる)。(ローカル・サービスの場合)

ローカルサービスに関しては大体、こんな内容だと思う。

なお、ローカルサービスに関しては ApiDemoの中の App/Service/Local Service Binding と App/Service/LocalServiceConroller にサンプルがある。この2つのクライアントは実際には1つのjavaプログラムとして書かれていて com.example.android.apis.app/LocalServiceActivities.java として提供されている。実質的に2つのプログラムを1にまとめた形で、1つはbindServiceによるサービスの起動と呼び出し、もう1つはstartServiceによるサービスの起動(とその維持)をデモしている。ちなみにサービス本体のプログラムは別ファイルの com.example.android.apis.app/LocalService.java に書かれている。

ApiDemoを起動してリストのAppからServiceを選択する。中ほどに“Local Service Binding”と“Local Service Controller”がある。先ず、Bindingを起動すると[Bind Service]と[Unbind Service]のボタンが現れ、Bind Serviceをクリックすると通知領域に△アイコンが表示されサービスが起動したことがわかる(下のスクリーンショットの赤い丸の部分)。Unbindボタンをクリックするとバインドが切られサービスも終了するので△アイコンが消える。またUnbindボタンをクリックしなくてもでDパッドの[戻る]ボタンでクライアントのActivityを中断しても、バインドが切られるのでサービスも終了することがわかる。

次に“Local Service Controller”を起動すると[Start Service]と[Stop Service]のボタンが現れ、[Start Service]をクリックすると通知領域に△アイコンが表示されサービスが起動したことが分かる。Stopをクリックすればサービスは終了するが、Stopをクリックせずに[戻る]ボタンでAppメニューに戻っても△アイコンが表示されているのでサービスは動作を続けていることが分かる。


その状態で再び“Local Service Binding”を起動する。Bindをクリックすれば、サービスに接続するのはさっきと同じだが、今度はUnbindをクリックしてバインドを切ってもサービスは動き続けている。startServiceでサービスを起動したので、バインドが切れても(stopServiceを呼び出すまでは)サービスは動き続けていることがわかる。

IBinderによるコールバックのサンプル

ローカルサービスではバインドにより接続が確立すると、サービスよりIBinderが渡され、これを使ってクライアントからサービスに対してコールバックできる。その実験をしてみた。SDKのサンプル、LocalService.java と LocalServiceActivities.javaをチョットだけ変更して実験する。

まず、サーバ(LocalService.java)の“LocalService”というクラスの最後に次のフィールドとメソッドを追加する。

private int counter = 0;
public int returnInt() {
	return counter++;
}

クライアントから呼び出されるとカウンタの値を返すという単純なメソッドである。

次にクライアント(LocalServiceActivities.java)を変更する。
Bindingクラスの中のmConnectionというフィールドの定義の中に、onServiceConnectedというメソッドがあり、その中でIBinderからサービスのオブジェクトを取得する“mBoundService = ....”という行(103行目)がある。その行の次にLogを使ってメッセージを出力する1行を加える。(次の例は“mBoundService = ....”の行も含んでいる。

mBoundService = ((LocalService.LocalBinder)service).getService();
Log.d("++++++", "counter=" + mBoundService.returnInt());

(この行を追加すると“import android.util.Log”が必要になる。)本来であれば、“Get Counter”のようなボタンを付ける方がカッコいいのではあるが、ここではあくまでコールバックの確認をするためだけなのでLogを使った。

以上の変更を加えてApiDemoをrunする。Bindでコネクションを確立する度に、サービスすから取得したカウンターの値をログに表示する。ただ、Bind ServiceボタンとUnbind Serviceボタンだけでコネクションの接続・切断を繰り返すと、常に帰って来る値は1になる。これは毎回サービスが中断されるためだ。Start Serviceボタンで一旦、サービスを起動しておけば、ind ServiceボタンとUnbind Serviceボタンをクリックする度にカウンターの値は上がってゆく。これはサービスが中断されずに走っているため。

android@android:~$ adb logcat
	:
D/++++++  (  492): counter=0
D/++++++  (  492): counter=1
D/++++++  (  492): counter=2
D/++++++  (  492): counter=3
	:

簡単だがローカルサービスへのコールバックの実験ができた。ローカルサービスの場合、クライアントもサービスも同じアプリケーションの中で実装される。なのでわざわざサービスを使わなくても、サービスの機能をActivityで実現して、そのオブジェクトを直接呼び出せば良いことになる。サービスを使うメリットとすれば、[戻る]ボタンや、他のアプリケーションが起動して画面を取られたとしても、サービスとして動作を続けられる、ということだろう、か。