Androidアプリの4大要素 Service【Android Tips】


1
Androidアプリの4大要素とインテント
Service
この節では、Androidアプリケーションの4大要素のうちの1つ”Service”を紹介します。 Serviceはバックグラウンドで処理を行うためのコンポーネントです。画面の状態に依存せず、なんらかの処理(一般的には処理に長い時間がかかるもの)を実行する場合に用いられます。
Service概要
Serviceはバックグランドで処理を行うためのコンポーネントで、ファイルのダウンロードなどのライフサイクルの長い処理、音楽の再生などの画面に依存しない処理を行いたい場合に使用します。
これらの処理をActivityで行うと、Activityを閉じてしまう(ユーザーの操作ではなくとも、TELがかかってきた等の外的なイベントによっても)と処理が不意に強制終了してしまうことになります。

ファイルダウンロード処理をActivityで実装した場合は以下のようになります。



何らかの要因により他のActivityが起動し、ダウンロードActivityが停止状態に遷移、さらにAndroidシステムにより強制終了されてしまうと、ダウンロード処理が途中で停止してしまう。

一方、Service実装した場合は以下のようになります。


ダウンロードActivityからダウンロードServiceを起動し、その中で実際のダウンロード処理を行います。
先ほどと同様に何らかの要因により他のActivityが起動、さらにはAndroidシステムによってダウンロードActivityが強制終了したとしても、ダウンロードServiceは処理を続けることができる。

このようにバックグランドで動き続けているServiceはすでにAndroid標準でいくつか組み込まれており、センサーや位置情報などが該当します。

Serviceの起動方法とライフサイクル
Serviceの起動は基本的にActivityから行い、起動方法には以下の2種類が存在します。
● インテントを利用する方法
● インタフェースと結合する「バインド」による起動方法

 



Intentを利用しServiceを起動する方法の場合は、Serviceに送ることのできるメッセージは起動時の一方通行のみで、ServiceからActivityに対してメッセージを送ることは基本的にできません。この起動方法はContextクラスに定義されているstartService()メソッドを使用します。

Intent intent = new Intent(this, MP3PlayService.class);
startService(intent);

 

一方、バインドを利用した起動方法の場合は、ActivityとService間でメッセージ授受用のインタフェースができ、Serivce起動後もデータの受け渡しが可能となります。この起動方法は、ContextクラスのbindService()メソッドを使用します。

ServiceにはActivityと同様にライフサイクルが存在し、どちらの起動方法を使用したかによって、Serivceライフサイクルが異なってくるため注意が必要です。

Serviceの状態遷移に応じて呼び出されるメソッドは全部で5種類あります。
● onCreate()メソッド
● onStartCommand()メソッド
● onBind()メソッド
● onUnbind()メソッド
● onDestroy()メソッド

これらのメソッドが呼び出されるタイミングは下図のようになります。

 



まず、Serviceは終了させるためのメソッドを明示的に呼び出さない限りプロセスとして残り続けるということを念頭に置いておきます。
終了させるためのメソッドはIntentによる起動方法の場合、stopService()メソッド、stopSelf()メソッドが当たります。
stopSerivce()メソッドがこのSerivceオブジェクト外部から停止させる場合に使うメソッド、stopSelf()メソッドはこのSerivceオブジェクト内部から呼び出す場合に使うメソッドです。
AndroidにおけるServiceはこれらのメソッドを呼び出さない限り、仮に行うべき処理が終わったとしてもずっとプロセスが残り続けてしまう作りとなっています。

 

では、以上のことを踏まえ、各メソッドが呼び出されるタイミングについて詳しく見ていきます。

1. onCreate()メソッド

Serviceが最初に起動した時に、1回限りのセットアップ処理を実行するために呼び出される(onStartCommand()やonBind()の前に呼び出される)。Serivceが起動中に再度startService()メソッドやbindService()メソッドを実行した場合はこのメソッドは実行されない。



2. onStartCommand()メソッド

他のコンポーネント(Activity等)がstartService()メソッドを使用した場合に呼び出されるメソッド。一度このメソッドが実行されるとServiceが開始され、無期限にバックグランドで実行される。
このメソッドを実装した場合、作業が完了したらService内でstopSelf()メソッドもしくは他のコンポーネントからstopService()メソッドを呼び出し、Serviceを停止させる必要がある。

なお、Android 1.6以下でアプリケーション開発を行う場合は、onStartCommad()メソッドの代わりにonStart()メソッドを使用する。(onStartCommand()メソッドは2.0以降でないと使えません。)

このメソッドは戻り値にintを返す。この戻り値によって、Serviceが強制終了された場合のService再開の挙動を制御することができる。

START_NOT_STICKY
Serviceを再起動しない。Serviceの実行を避けるためのもっとも安全なオプション。

START_STICKY
Serviceを再起動し、onStartCommand()を呼び出すが、最後のIntentは再配信されない。
Serviceを開始するペンディングインテントがない場合は、引数のIntentをnullにしてonStartCommand()メソッドを呼び出す。

※ペンディングインテント(PendingIntent)
Intentを即時に発行するのではなく、タイミングを指定して発行する仕組み。

START_REDELIVER_INTENT
Serviceを再起動し、初回起動時と同じIntentでonStartCommand()メソッドを実行する。



3. onBind()メソッド

他のコンポーネント(Activity等)がbindService()を使用した場合に呼び出されるメソッド。
クライアント(他のコンポーネント)がServiceと通信するために使用するインタフェースをIBinderを戻り値に返すことにより提供する必要がある。このメソッドはServiceクラスに定義されている抽象メソッドのため実装必須だが、バインドによるService起動を許可したくない場合はnullを返却するように実装する。



4. onUnbind()メソッド

ServiceがunbindService()メソッドでバインド解除されたときに呼び出される。



5. onDestroy()メソッド

ServiceがstopService()メソッドもしくはstopSelf()メソッドで終了したとき、またはonUnbind()メソッドでバインド解除された後に呼び出される。
スレッドや登録したリスナー、レシーバ、その他のリソースの破棄を行う。


以下に独自Serviceクラスの実装時の注意点をピックアップしました。



新しくコンポーネントを追加した場合、マニフェストファイルへの追記を忘れずに行いましょう。

Serviceの破棄
Androidシステムはメモリ等のリソースに余裕がなくなり、ユーザが使用しているActivity用にシステムリソースを確保する必要が発生したとき、Serviceを強制的に停止させることがあります。Activityでも行われていた強制終了と同じイメージです。

Serviceは長時間実行されていると徐々に優先順位が下がっていき、相対的に強制終了される確率が高くなるように設計されています。
ただし、強制終了されたとしてもリソースに余裕ができると、サービスを再起動しようとします。(onStartCommand()の戻り値によって再起動するかしないか、どのように再起動するかなどは決まります。)

では、常駐型(メール受信の確認、RSSの更新チェック等)のアプリを作成するにはどのようにしたらよいのでしょうか?
ポイントとなるのはServiceで行う処理の実行頻度です。

 

メール受信確認やRSSの更新チェック等であれば、5分・10分に1回程度の頻度で処理を行えばよいので、処理をしたらその都度Serviceを停止し、Serviceを常時起動しておく必要がありません。
この場合は、AlarmManagerクラスを使うという手があります。
このAlarmManagerクラスはWindowsにおけるタスクスケジューラのような機能を実現するクラスで、指定した日時にIntentを発することが可能です。これを利用してServiceを起動するということですね。

 

では、もっと高頻度、それこそ常時起動のServiceを作りたいときはどうすればよいでしょうか?
これにはServiceをフォアグランド実行するという方法があります。startForeground()メソッドを使いサービスを起動することでフォアグラウンドサービスとなり、明示的に停止処理を行わない限り停止することはほぼありません。
ただし、あくまでも停止し難いというだけですので、システムによる再起動を想定したり、万一停止したときの再起動方法を実装しておくことは重要なポイントです。
(上記のAlarmManagerクラスで定期的に起動チェックを行う、Activityが起動したら同じように起動チェックを行うといった具合です。)

Androidユーザーインターフェースのシングルスレッドモデル
バックグラウンドで処理を行う別の方法として、Threadクラスを使ったマルチスレッドプログラムという方法があります。
しかし、別スレッドをActivity上から生成し開始した場合、この別スレッドはActivityのライフサイクルから逃れることはできずActivityが強制終了されるとやはり処理を停止してしまいます。

 

Serviceを使用する場合でも注意が必要です。
BroadcastReceiverのところでもonReceiver()メソッド内の処理はUIスレッドと同一スレッド上の処理となるため、素早く終える必要があることを紹介しましたが、実はこれがServiceにも当てはまります。
Serviceの処理もUIスレッド上で行われるため、Service内であっても時間のかかる処理は別スレッドを生成して処理を行う必要があるのです。

 

AndroidアプリのUIはシングルスレッドモデルを採用しています。別スレッドからUIを直接操作(TextViewの表示文字を変える、Toastを出力する等)してしまうと、Exceptionが発生しアプリが強制終了する作りとなっています。

ただし、ユーザビリティーの向上のためにはマルチスレッドは必要不可欠(ファイル読込やダウンロード処理中にUIがフリーズしてしまっては困りますよね)であるため、AndroidではHandlerというクラスを使用することで、別スレッドからUIを操作できるようにしています。

このHandlerクラスは以下のような処理を行っています。


●Handlerを使うと、別スレッドからUIスレッドの持つキューにジョブを登録できる。
●キュー内のジョブはUIスレッドにより順番に実行される。

 

使用例を見てみましょう。
まずは別スレッドから直接UIを操作するNG例です。

public void onClick(View v) {
    new Thread(new Runnable() {                     //新規スレッドを作成
        public void run() {
            Bitmap bitmap = loadImageFromNetwork(); //ネットワーク上から画像をダウンロード
            imageView.setImageBitmap(bitmap);       //画像をImageViewに設定(Exceptionが発生)
        }
    }).start();
}

onClick()メソッドが実行されると別スレッドが起動し、ImageViewに表示している画像が切り替わるというプログラムです。
loadImageFromNetwork()メソッドは別途定義されているネットワーク上から画像をダウンロードするメソッドです。(ここでは割愛しています。)
画像が切り替わる処理(5行目)がUIを操作しているため、例外が発生しアプリが強制終了してしまいます。

 

では次にOK例です。

final Handler handler = new Handler(); // Handlerオブジェクトの取得
 
public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            final Bitmap bitmap = loadImageFromNetwork();

            Runnable r = new Runnable() { // キューに登録するジョブをRunnableオブジェクトとして生成
                public void run() {
                    imageView.setImageBitmap(bitmap);
                }
            };
            handler.post(r); // キューに登録
        }
    }).start();
}

まずUIスレッド上でHandlerオブジェクトを生成します。(1行目)
onClick()メソッドが実行されると別スレッドが起動する点は同じですが、画像が切り替わる処理(10行目)がRunnableオブジェクト内にラッピングされています。このRunnableオブジェクトがUIスレッドで実行されるジョブになります。
あとは、Handlerオブジェクトを使ってキューに登録を行い、UI処理をUIスレッドに任せます。

Serviceを利用したMediaPlayerアプリ
では、MP3ファイルを再生するMediaPlayerアプリをServiceを使って作成してみましょう


STARTボタンをクリックするとMP3ファイルの再生が始まり、PAUSEボタンをクリックすると再生が止まります。

まずは、マニフェストファイルからです。

“AndroidManifest.xml”

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.service"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="7"
        android:targetSdkVersion="15" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/title_activity_main" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name=".MP3PlayService"></service>
    </application>
</manifest>

追加しなくてはいけないのは23行目です。
MP3ファイルを再生するためのServiceクラスを追加しています。

 

次に、MainActivityのレイアウトXMLファイルです。
“res/layout/activity_main.xml”

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <Button
        android:id="@+id/button1"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="START" />

    <Button
        android:id="@+id/button2"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="PAUSE" />

</LinearLayout>

LinearLayoutの子要素として、”Button”が2つ定義されています。

“src/com.example.service/MainActivity.java”

package com.example.service;
// 中略
public class MainActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button1 = (Button)findViewById(R.id.button1);
        button1.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, MP3PlayService.class);
                startService(intent);
            }
        });

        Button button2 = (Button)findViewById(R.id.button2);
        button2.setOnClickListener(new OnClickListener() {
            @Override

            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, MP3PlayService.class);
                intent.putExtra("PAUSE", true);
                startService(intent);
            }
        });
    }
}

STARTボタンをクリックしたときの処理が15、16行目です。明示的なIntentを作成し、startService()メソッドを実行しているだけです。
PAUSEボタンをクリックしたときの処理が25から27行目です。こちらも同じく明示的なIntentを作成し、startService()を実行していますが、Intentに付加情報が設定されています。“PAUSE=true“すなわち”停止しなさい”という命令をServiceに送っていることになります。

 

次に実際にMP3ファイルの再生を行うMP3PlayServiceです。
“src/com.example.service/MP3PlayService.java”

package com.example.service;
// 中略
public class MP3PlayService extends Service implements OnCompletionListener{
    private static final String TAG = "MP3PlayService";
    private MediaPlayer mp = null;

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

    @Override
    public void onCreate() {
        Log.e(TAG, "onCreate() called.");

        mp = MediaPlayer.create(this, R.raw.music);
        mp.setOnCompletionListener(this);
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e(TAG, "onStartCommand() called.");

        if(intent.getBooleanExtra("PAUSE", false)){
            pauseMusic();
        }else{
            startMusic();
        }

        return START_REDELIVER_INTENT; // 再起動する
        //return START_NOT_STICKY; // 完全停止する
    }

    @Override
    public void onCompletion(MediaPlayer mp) {
        Log.e(TAG, "onCompletion() called.");
        stopSelf();
    }

    @Override
    public void onDestroy() {
        Log.e(TAG, "onDestroy() called.");

        pauseMusic();
        if(mp != null){
            mp.release();
            mp = null;
        }
        super.onDestroy();
    }

    private void startMusic(){
        if(!mp.isPlaying()){
            mp.start();
        }
    }

    private void pauseMusic(){
        if(mp.isPlaying()){
            mp.pause();
        }
    }
}

再生するMP3ファイルはresフォルダ以下にrawという新しいフォルダを作成し、その中にmusic.mp3というファイル名で保存してください。

MP3ファイルを再生するにはMediaPlayerクラスを使用します。
まずこのクラスはファイルを読み込み、再生準備を行うためのcreate()メソッドを実行する必要があります。(16行目)
onCreate()メソッド内で行われています。

 

MainActivityでSTARTボタンがクリックされIntentが発せられると、上記のonCreate()が実行されたのち、onStartCommand()メソッドが実行されます。このとき付加情報は設定されていないため、startMusic()メソッドが実行され、再生を開始するためのstart()メソッドが実行されます。(55行目)
これで、MP3ファイルの再生がはじまります。

 

次に、MainActivityでPAUSEボタンがクリックされると、付加情報が設定されたIntentが飛んできます。
このとき、Serviceはすでに実行中であるため、onCreate()メソッドは実行されずに、onStartCommand()メソッドが実行されます。
付加情報が設定されているため、pauseMusic()メソッドが実行され、再生を一時停止するためのpause()メソッドを実行されます。(61行目)
これで、MP3ファイルの再生が一時停止します。

 

最後までMP3ファイルが再生されると、 onCompletion()メソッドが実行されます。
これは、onCreate()の中でリスナーの登録を行っているために呼び出されるコールバックメソッドです。(17行目)
このメソッドの内ではstopSelf()メソッドを実行し、Serviceを停止しています。
この後、onDestroy()メソッドが実行され、読み込んだファイルを初期化し、オブジェクトを破棄するためのrelease()メソッドを実行されます。(47行目)

 

では、ServiceがAndroidシステムによって強制終了したときに、再起動するかどうか確認してみましょう。
MP3ファイルを再生している状態で、EclipseのDDMSパースペクティブから“com.example.service”プロセスを停止してみてください。
このような操作を行うと、Androidシステムによって強制終了したときと同じような状況を作り出すことができます。

しばらくするとServiceが再起動し、MP3ファイルの再生が再開するはずです。
これは31行目でonStartCommand()メソッドの戻り値に“START_REDELIVER_INTENT”を設定したためです。この戻り値を設定すると、Serviceが強制終了したときに、同じIntentで再起動するという挙動となりました。

 

では、コメントアウトを切り替え、“START_NOT_STICKY”を戻り値に設定しましょう。
先ほどと同じようにServiceを強制終了させると、今度はいくら待ってもServiceが再開しないはずです。この戻り値を設定すると、Serviceを再開しないという挙動になるのです。

  • このエントリーをはてなブックマークに追加

PAGE TOP