読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

Androidで,自動起動する常駐型サービスのサンプルコード (アプリの裏側で定期的にバッチ処理)

Android

重要なお知らせ:

この記事で公開した情報は,AndroidのMVCフレームワーク「Android-MVC」の機能の一部として取り込まれました。

より正確な設計情報や,動作可能な全ソースコードを閲覧したい場合,「Android-MVC」の公式ページより技術情報を参照してください。


AndroidのMVCフレームワーク - 「Android-MVC」
http://code.google.com/p/android-mvc-...


Androidアプリのシステム・アーキテクチャにおいて,

もし「表側」の主要な構成要素がActivityだとしたら,

「裏側」の主要な構成要素は,「サービス」によるバックグラウンド処理だろう。


ちょうどLinuxで言うところのデーモン・プロセスのように,サービスは裏側で動作し続ける。


アプリから起動・停止などの制御を受けたり,データ連携する事も可能だ。

端末の電源がONになったり再起動したタイミングで,自動的に起動する事もできる。

UIを邪魔せずに,ユーザに存在を意識させることなく,影でバッチ処理をしてくれる役割を担っている。


このような「常駐型・定期実行型・自動起動型・アプリ連携型」のサービスを,

Androidで実装するためのシンプルなテンプレート・コードを下記に記載する。


各クラスは,再利用しやすいように,基底クラスと具体的なクラスに分割してクラス設計してある。

※↑ もくじジェネレータ で自動生成


※もし,「サービス」や「ブロードキャスト」「レシーバ」などのAndroid用語がわからない場合,下記エントリの一覧表にある説明を参照のこと。

WebアプリとAndroidアプリのアナロジー (「Androidのアレは,Webで例えるなら○○だ」)
http://language-and-engineering.hatenablog.jp/entry/20110813/p1

  • バッチ処理:Serviceアプリ
  • OS起動時に自動開始するサービスやデーモン:BOOT_COMPLETEDに応答するブロードキャストレシーバー


(1)マニフェスト定義


Manifest.xmlのapplicationタグ内に:

        <!-- 端末起動時にバッチを呼ぶレシーバ -->
        <receiver android:name=".bat.OnBootReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"></action>
            </intent-filter>
        </receiver>


        <!-- サンプルのサービス -->
        <service android:name=".bat.SamplePeriodicService"
        ></service>


(2)レシーバ

レシーバ基底クラス:

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

/**
 * 端末起動時にサービスを自動開始させるためのクラス。
 * @author id:language_and_engineering
 *
 */
public abstract class BaseOnBootReceiver extends BroadcastReceiver
{

    // ブロードキャストインテント検知時
    @Override
    public void onReceive(final Context context, Intent intent) {

        // 端末起動時?
        if( Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()))
        {
            new Thread(new Runnable(){
                @Override
                public void run()
                {
                    onDeviceBoot(context);
                }
            }).start();

        }

        // NOTE:
        // ・このメソッド終了時点でこのオブジェクトは消滅
        // ・このメソッドはメインスレッドで呼ばれ,10秒以上の長い処理は禁物
    }


    /**
     * 端末起動時に呼ばれるメソッド。
     * メインスレッド上ではないので,実行時間の心配はない。
     */
    protected abstract void onDeviceBoot(Context context);

}


具体的なレシーバクラス:

/**
 * 端末起動時の処理。
 * @author id:language_and_engineering
 *
 */
public class OnBootReceiver extends BaseOnBootReceiver
{
    @Override
    protected void onDeviceBoot(Context context)
    {
        // サンプルのサービス常駐を開始
        new SamplePeriodicService().startResident(context);
    }

}


(3)サービス

サービス基底クラス:

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;

/**
 * 常駐型サービスの基底クラス。
 * @author id:language_and_engineering
 *
 */
public abstract class BasePeriodicService extends Service
{

    /**
     * サービスの定期実行の間隔をミリ秒で指定。
     * 処理が終了してから次に呼ばれるまでの時間。
     */
    protected abstract long getIntervalMS();


    /**
     * 定期実行したいタスクの中身(1回分)
     * タスクの実行が完了したら,次回の実行計画を立てること。
     */
    protected abstract void execTask();


    /**
     * 次回の実行計画を立てる。
     */
    protected abstract void makeNextPlan();


    // ---------- 必須メンバ -----------


    protected final IBinder binder = new Binder() {
        @Override
        protected boolean onTransact( int code, Parcel data, Parcel reply, int flags ) throws RemoteException
        {
            return super.onTransact(code, data, reply, flags);
        }
    };


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


    // ---------- サービスのライフサイクル -----------


    /**
     * 常駐を開始
     */
    public BasePeriodicService startResident(Context context)
    {
        Intent intent = new Intent(context, this.getClass());
        intent.putExtra("type", "start");
        context.startService(intent);

        return this;
    }


    @Override
    public void onStart(Intent intent, int startId) {

        // サービス起動時の処理。
        // サービス起動中に呼ぶと複数回コールされ得る。しかし二重起動はしない
        // @see http://d.hatena.ne.jp/rso/20110911

        super.onStart(intent, startId);

        // タスクを実行
        execTask();

        // NOTE: ここで次回の実行計画を逐次的にコールしていない理由は,
        // タスクが非同期の場合があるから。
    }


    /**
     * サービスの次回の起動を予約
     */
    public void scheduleNextTime() {

        long now = System.currentTimeMillis();

        // アラームをセット
        PendingIntent alarmSender = PendingIntent.getService(
            this,
            0,
            new Intent(this, this.getClass()),
            0
        );
        AlarmManager am = (AlarmManager)this.getSystemService(Context.ALARM_SERVICE);
        am.set(
            AlarmManager.RTC,
            now + getIntervalMS(),
            alarmSender
        );
        // 次回登録が完了

    }


    /**
     * サービスの定期実行を解除し,サービスを停止
     */
    public void stopResident(Context context)
    {
        // サービス名を指定
        Intent intent = new Intent(context, this.getClass());

        // アラームを解除
        PendingIntent pendingIntent = PendingIntent.getService(
            context,
            0, // ここを-1にすると解除に成功しない
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT
        );
        AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
        alarmManager.cancel(pendingIntent);
            // @see http://creadorgranoeste.blogspot.com/2011/06/alarmmanager.html

        // サービス自体を停止
        stopSelf();
    }

}


具体的なサービスクラス:

import android.content.Context;
import android.util.Log;

/**
 * 常駐型サービスのサンプル。定期的にログ出力する。
 * @author id:language_and_engineering
 *
 */
public class SamplePeriodicService extends BasePeriodicService
{
    // 画面から常駐を解除したい場合のために,常駐インスタンスを保持
    public static BasePeriodicService activeService;


    @Override
    protected long getIntervalMS() {
        return 1000 * 10;
    }


    @Override
    protected void execTask() {
        activeService = this;


        // ※もし毎回の処理が重い場合は,メインスレッドを妨害しないために
        // ここから下を別スレッドで実行する。


        // ログ出力(ここに定期実行したい処理を書く)
        Log.d("hoge", "fuga");

        // 次回の実行について計画を立てる
        makeNextPlan();
    }


    @Override
    public void makeNextPlan()
    {
        this.scheduleNextTime();
    }


    /**
     * もし起動していたら,常駐を解除する
     */
    public static void stopResidentIfActive(Context context) {
        if( activeService != null )
        {
            activeService.stopResident(context);
        }
    }

}

具体的なタスクの中で…

  • 任意のIntentを発行すれば、Activity を起動したりする事もできる。
  • ネットワークやデータベースにアクセスすれば、バックグラウンドでユーザが意識しないうちに、定期的にデータ処理を済ませる事もてきる。

(4)画面側のイベント

Activityで,レイアウトで2つボタンを用意して,それぞれサービスの開始と終了ができるようにしておく。


    // サンプルのサービス常駐を開始
    new SamplePeriodicService().startResident(context);

〜〜

    // サンプルのサービス常駐を解除
    SamplePeriodicService.stopResidentIfActive(context);


(5)動作確認

アプリを実行すると,まだ「端末の起動」というブロードキャストは発生していないので,何も起こらない。


サービス起動ボタンを押すと,LogCat内に,10秒に一度のログ出力処理がバックグラウンドで行なわれる。

サービス解除ボタンを押すと解除される。


サービスを起動した状態で,実機を再起動してみる。

そのまま,DDMS経由でLogCatを眺めていると,10秒に一度のログ出力がちゃんと行われている。

つまり,実機のBOOTに関するブロードキャストが受理され,何もしなくても自動的にサービスの常駐が始まったのだ。


その状態で,アプリを起動して,サービス解除ボタンを押下すれば,またサービスが止まる。


常駐型サービスが,端末の起動と共に自動的に動作し,アプリ側からも制御できている事が分かる。

補足

参考:

システム起動時に常駐するサービスを作成する
http://d.hatena.ne.jp/yujimny/2011020...

  • システム起動時にはACTION_BOOT_COMPLETEDがブロードキャストで飛んで来る


Androidで常駐型サービス
http://gsbina.com/?p=339

  • タスクの定期実行はAlarmManagerを活用
  • 電源を付けたときにサービスも起動してくれるようにブロードキャストを拾うクラスを作る


ゼロから始めるAndroid開発【サービス編】
http://labs.techfirm.co.jp/android/y-...

  • Api Demoに近い,最も無難なサンプルコード


今回の掲載コードは基礎のひな型に過ぎないが,実用的なServiceの設計方法についてリンク集。



タスクを実行させる際のスレッドの注意点:

落ちないServiceの作り方
http://www.grandnature.net/blog/archi...

  • 定期的に処理を行う場合は、自前で監視スレッドを長生きさせる方針は避ける。onStart→作業用Thread作成して処理→AlarmManagerで次回の起動を設定→時間が来たら再度onStart,の繰り返し
  • サービスの終了は,stopServiceインテントの使用は避ける。onStartなどのイベント時に対処するのがよい


単純なサービス
http://android.keicode.com/basics/ser...

  • サービスプログラムの本質とは,アクティビティのライフサイクルと別のライフサイクルを持っていること。停止し忘れとか,非同期にしてなかったので固まったとかのトラブルに注意


Android起動時に自動起動するアプリで例外→リブートを繰り返す
http://d.hatena.ne.jp/androidzaurus/2...

  • BOOT_COMPLETEDの受信アプリに例外バグがあって端末が起動すらしなくなってしまうというトラブルに注意


ローカルサービスとリモートサービス:

AndroidのServiceについて
http://d.hatena.ne.jp/adsaria/2010091...

  • Activityと一緒のアプリ(=同一パッケージ内)として使う「ローカルサービス」と,別のアプリケーションとして動かす「リモートサービス」の2種類
  • 起動は,アクティビティやレシーバによってstartService・bindServce
  • サービスがローカルな場合は,別プロセスでも別スレッドでもなく,そのアプリの一部として動く。だから,サービス内で別スレッドを起動しない限りANR回避の手段にもならない。(ふつうのデーモンの意味でのサービスとは異なる)


別プロセスで動作させているServiceでGPSを使う方法
http://d.hatena.ne.jp/terurou/2011082...

  • AndroidManifestでandroid:process=":remote"のように指定すると,サービスは別プロセスになる


単一性の保証:

Android Service
http://typea.info/tips/wiki.cgi?page=...

  • サービスの多重起動の心配は無い。statrService() を多重で呼び出してもネストされない。何回サービスをスタートしたとしても問題無いし、一度の Context.stopService() やstopSelf()でサービスは停止する


Serviceの多重起動 / Serviceがすでに起動している状態で同じサービスを起動したら
http://d.hatena.ne.jp/rso/20110911

  • onCreate()は最初の一回だけ実行される。onStart()はstartService()のたびに毎回実行される。


関連する記事:

Androidアプリで,Google Maps API+GPS+Geocoderを使って,現在地の地図と地名を表示させよう
http://language-and-engineering.hatenablog.jp/entry/20110828/p1


Androidで,音声入力と音声合成をシンプルに記述するためのライブラリ案
http://language-and-engineering.hatenablog.jp/entry/20121107/AndroidTTSAndSpe...


Androidアプリで,HTTP通信のPOSTリクエストをする汎用クラス (文字化け無し+非同期タスク)
http://language-and-engineering.hatenablog.jp/entry/20111121/p1