2016/07/22

Service

Androidでは、よくあるViewが表示されるアプリとは別に、Serviceというバックグラウンド処理を行うクラスを実装することができる。
Serviceはバックグラウンド処理を行うのに都合がいい機能が幾つか実装されている。


前置き


  1. Activityがバックグラウンドに移動しても、Serviceは動き続ける。
  2. Activityが終了した場合でも動き続けるような設定ができる。(正確には、Activity終了時にServiceを立ち上げ直しているもよう)
  3. Serviceの立ち上げ方は2種類あり、それによって挙動やライフサイクル、Activityとの通信方法などが異なる。
  4. ServiceはActivityなどと別のプロセスではない
  5. Serviceは多重起動はできない。

4に関しては、Activityと同じスコープで実装・ビルドしている以上、そうなのかなと思ったが、
よく考えたら、Activityが落ちた後もずっと動いてるし、その後またActivityが立ち上がったらどうなるの...?と思った。

Serviceはライフサイクルが特殊なので、いろいろテストをして確認した結果も踏まえて、使用方法をまとめた。




立ち上げ方法とライフサイクル


Serviceは主にActivityから立ち上げるが、Context.startService()かContext.bindService()で起動する。
この立ち上げ方によって、挙動がいくらか異なる。


startService()


基本的に、立ち上げっぱなしのServiceとして利用される起動方法。
基本的にActivityからServiceへの通知手段がない。
やたら「基本的に」と書いたのは、同じプロセス空間にいる以上、工夫すれば通知は割とどうにでもなるため。

こんな感じで立ち上げる。
startService(new Intent(MainActivity.this, TestService.class));

立ち上げ時には
onCreate()
onStartCommand()
で起動。

すでに立ち上がっている際に、さらにstartService()を行うと
onStartCommand()
のみが呼び出される。

終了はこんな感じ。明示的にstopを行う。
stopService(new Intent(MainActivity.this, TestService.class));
Service自身がstopSelf()を呼んでもいい。

終了時には
onDestroy()
が呼ばれる。


また大きな特徴としては、onStartCommand()の返り値で挙動を設定できる。

  • START_NOT_STICKY
    既定値。サービスが強制終了された際には、再起動を行わない。
  • START_STICKY
    強制終了された際などに、OSがサービスを再起動する。タスクキラーで殺してもすぐに立ち上がってくる様は、さながらゾンビ。
    再起動した際のIntentがnullになっている。
  • START_REDELIVER_INTENT
    これも強制終了された際などに、OSがサービスを再起動する。
    ただし、再起動した際のIntentに初回起動時のIntentが入る。

他にもありますが、主に使用するのは上記3つかと。
詳細は公式を参照。


bindService()


サービスにバインドした際に、BinderというインスタンスをServiceからActivityに渡すことができる。
(Binderを通じて、ServiceのインスタンスをActivityに渡すことができるため、ActivityからServiceの関数を呼ぶことができる。)
また、サービスはバインドされている数をカウントしており、これが0になるとDestroyされる、という特徴がある。

バインド時には、ServiceConnectionクラスを実装したクラスのインスタンスを指定する必要がある。
Connectionは、Activity内にこんな感じで定義するといい。

private ServiceConnection mConnection = new ServiceConnection() {
    public void onServiceConnected(ComponentName className, IBinder service) {
        ...
    }

    public void onServiceDisconnected(ComponentName className) {
        ...
    }
};


こんな感じでバインドする。
bindService(new Intent(MainActivity.this, TestService.class), mConnection, BIND_AUTO_CREATE);

立ち上げ時には
onCreate()
onBind()
で起動。

すでに立ち上がっている際に、さらにbindService()を行っても、何も起こらない。

バインド解除はConnectionを指定する。
unbindService(mConnection);

アンバインド時には
onUnbind()
が呼ばれる。

Serviceは複数のコンポーネントからバインドが可能だが、
各コンポーネントからバインド解除されてバインド数が0になったら、インスタンスが終了する。

終了時には
onDestroy()
が呼ばれる。

ここで、ちょっと特殊な仕様だが、onUnbind()の返り値をtrueにすると、次回の接続からは
onBindではなく、onRebind()が呼ばれる、とのこと。
なるほどなるほど、と思って試してみると、普通にonBind()が呼ばれる...

これはおそらく、テストしているアプリケーションのMainActivityからしかバインドしておらず、
バインド解除した際に、バインド数が0になりServiceがDestroyされ、再バインド時にまたCreateされているためではないだろうか?
複数コンポーネントで検証する必要があろうが、めんどくさいのでパス。

また、onBind()の返り値として、IBinderを拡張したクラスを返すと、
それがActivity側から参照できる。
(実際には、ServiceConnectionのonServiceConnected()の引数として渡される)
BinderはServiceとActivityの一般的な通信手段として使用される。
(BinderにServiceのインスタンスを持たせる、という手段がよくとられる)


あと、この立ち上げ方はonStartCommandが呼ばれないため、
上のstartService()の項目で紹介した、再起動時の挙動を指定できない
試しに、アプリを立ち上げてServiceをバインドして、Activityを落としてみたら、
Serviceの動作は止まっているようだ。
ただ、onUnbindやonDestroyのLogは吐かなかった。一体どうなっているのやら・・・


実装方法


Serviceのクラス自体は、android.app.Serviceを継承して実装する。
onCreate()やonStartCommand()、onBind()、onDestroy()を主にオーバーライドして実装を行うが、これらがどのタイミングで動作するかは立ち上げ方法などによる。

実装例:
public class TestService extends Service {

    Timer mainTimer = new Timer();

    public class TestServiceLocalBinder extends Binder
    {
        TestService getService() { return TestService.this; }
    }

    @Override
    public void onCreate()
    {
        //定期実行のためのタイマー
        mainTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                Log.d("ServiceTest", "process:" + this.hashCode());
            }
        }, 0,1000);

    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId)
    {
        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent var1)
    {
        return new TestServiceLocalBinder();
    }

    @Override
    public void onRebind(Intent intent)
    {

    }

    @Override
    public boolean onUnbind(Intent intent){
        //return trueだと次回バインド時にonRebindになる
        return true;
    }

    @Override
    public void onDestroy()
    {
        mainTimer.cancel();
    }
}

おまけでTimerを使って定期実行する機能をつけてみた。
どうせこんな感じで使うんでしょ?フフ...


Activity - Service間のやりとり


ActivityとServiceはやりとりできるのか?
色々な記事を読むと、「bindしたServiceはBinderを通してやりとりできるけど、
startServiceに関しては、立ち上げっぱなしとして使うのが普通だよ」的な説明が書いてある。

それ以外にも、Handlerを拡張実装して、メッセージをやりとりする、というAndroidでは一般的な通信方法もよく見られる
公式もサンプルとして載せている)。

中にはBroadcastReceiverを登録してトリガーにする、と言った例もある。
これは、ActivityレスのServiceを作るなど、ちょっと特殊な例。


ただ...ぶちゃけstaticとしてInstanceを保持すればいいような気もするんだよね。
そう思って、Service内に

public static TestService instance = null;
...

public TestService()
{
    TestService.instance = this;
}

とか何とかして使ってみると、罠があった。
startServiceなどを行った直後は、startのリクエストをするのみとなっていて、すぐにインスタンスが作成されるわけではないらしい。
言われてみればそうだね。
つまり
startService(new Intent(MainActivity.this, TestService.class));
TestService.instance.hoge();
などするとinstanceがnullなので例外が発生する。

なかなかままならない。まぁnullチェックすればいいだけなんだけど...


ライフサイクル考


一般的に説明されているライフサイクルに加えて、いろいろ試してみた結果を記載する。
(OSバージョンやデバイスによっても挙動が異なるかも...)

基本的には上で説明しているように、startServiceをした方がAcrivityとライフサイクルを切り離せる。
(Acrivityが落ちても立ち上がる。さらに自身が強制的に落ちても立ち上がり続けるジョー・ヤブーキーのようなServiceが実装できる)
bindServiceではこれができない。

ただし、startService()したのちに、そのServiceに対してbindService()をすることができ、このように立ち上げると、両方の性質を併せ持つServiceの実装が可能。
このServiceがDestroyされるのは、両方の終了条件を満たした時。
つまり、バインド数が0で、かつ、stopService()される必要がある。


Activityが落ちても動き続けるServiceの需要は高いかと思う。
おさらいだが、これにはまずstartServiceを行う必要がある。
ただ、startServiceではActivityとServiceの通信がしづらい(とされている)
この場合は、上記start、bindのどちらも行うといいのかもしれない。

あと、Serviceは結構ポンポン落ちるものらしいので、ヘルスチェックや再起動設定には気を使った方がいいかも。


※ここから下は少々マニアックなので、飛ばしても構いません。※

ここまで書いて、「ん?Activityが落ちても動き続けるServiceは、Activityが落ちてもServiceのインスタンスは生きていて、
Activityを再起動してからそのServiceにbindすると、同じインスタンスが参照できるって、妙じゃないか?」
と思った。

なんのこっちゃ、と思われそうなのでまとめよう。

最初に立ち上げたActivityをActivity1としよう。
Activity1から起動されたService、これをService1としよう。Activity1からService1のインスタンスにはBinderを通せばアクセスできるので、同じメモリ空間内にいるはず。
(基本的には、同じプロセス内にいないと、互いのインスタンスにはアクセスできない)
Activity1が死ぬ。Service1は残っている。
新しいActivityを起動する。Activity2としよう。Activity2からService1にbindして、Binderからインスタンスを取得できるので、Activity2はService1のインスタンスにアクセス可能。
つまり、Activity1、Service1、Activity2は同じメモリ空間、同じプロセス内にいる。

Activity1を落としたのに、新しく立ち上げたActivity2と同じプロセスにいるのは、マルチタスクアプリケーションの常識からはかなり妙(だよね?)。
プロセスがずっと生きているとか?そのプロセス内でActivityを再起動しているとか?

いろいろ実験した結果、面白い挙動がわかった。

ログなどで検証すると、Activity1を落とした際に、Service1も終了している。
Service1は再起動設定を行っているため、数秒後に立ち上がる。(ここでプロセスが変わっている。ServiceもService2となっている)
Activity2を立ち上げたら、Service2の動作しているプロセス内でActivity2が動作している

つまりServiceがあらかじめ動いていて、Activityが立ち上がったらそのServiceのプロセス内で立ち上がるような仕組みらしい。
Activity1とActivity2は違うプロセスで動作していたが、そんなことがどうでもよくなるようなビックリ仕様だ。


0 件のコメント:

コメントを投稿