Servce はバックグラウンドで作業をさせたい場合に使います。システムは直ぐにスリープに入れてしまいます。それでも裏で色々やりたい場合に有効です。例えば音楽の再生など
2021.2.1
Service
Serviceがバックグラウンドで動作するので、非同期と勘違いしそうですがそうではなく、Activityから表示UIを無くしたようなコンポーネントです。
サービスは、そのホスティング プロセスのメインスレッドで実行され、サービスが自身のスレッドを作成することはなく、別のプロセスで実行されることもありません
CPU を集中的に使う作業、Audioの再生やネットワーク作業など、を行う場合は、サービス内に新しいスレッドを作成してその作業を行う必要があります
Serviceの開始
Serviceを開始するには、
- startService()
- 開始後は、開始したActivityが破棄されても基本的には実行し続けられる、Activityとは別の独自のContextを持っている
- 呼び出し側からの制御は開始か停止させるのみで、タスクの終了をコールバックしないので自身で終了するなどの手当が必要
- bindService()
- 要求を送信したり、結果を取得したりとServiceを制御できますが、呼び出し元のActivityが終了すると一緒に終了してしまう
これにAndroid 8.0からの追加として、
startForegroundService()
を使ってバックグラウンド実行制限をある程度回避できます。
また、API 28からAndroidMnifestにpermission登録が必要になりました。
startService()
Activity からServiceを呼び出すのはIntent をセットして、startService(intent) で行います。
1 2 |
Intent intent = new Intent(getApplication(), TestService.class); startService(intent); |
Activity から Activity に遷移させる場合は
startActivity(intent)でしたので、startService(intent)に変わっただけとわかりやすです
一方、サービス側は
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class TestService extends Service { @Override public void onCreate() { super.onCreate(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { // to do something return START_NOT_STICKY; } @Override public void onDestroy() { super.onDestroy(); } @Override public IBinder onBind(Intent intent) { return null; } } |
Service を継承して
- onCreate()
- これはActivity同様、最初だけ呼ばれて終わりなので初期化だけです。
- onStartCommand()
- サービスで実行させたいコードはここに記述
- 戻り値は:
- START_NOT_STICY:強制終了しても再起動しない
- START_STICKY:強制終了されても自動的にサービスが再起動、最後のインテントは再配信しない
- START_REDELIVER_INTENT:強制終了されても自動的にサービスが再起動、サービスに最後に配信されたインテントで onStartCommand() を呼び出す
- onDestroy()
- onBind()
- bindService() で呼び出した場合
onStartCommand() ではなく
onBind() がcallbackされます
- bindService() で呼び出した場合
Manifest にサービスクラスの定義を忘れないように
AndroidManifest.xml
1 2 |
<service android:name=".TestService" /> |
Serviceの終了
startService()で始めたServiceは勝手には止まりません。
MainActivityからstopService(intent)で止める
1 2 3 |
Intent intent = new Intent(getApplication(), TestService.class); // Serviceの停止 stopService(intent); |
ミュージック再生などのタスクが終了したのを受けてstopSelf()を実行することもできます。
1 |
stopSelf(); |
Oreoからバックグラウンドに対する制限ができて
アプリがバックグラウンドに移行すると数分間のウィンドウが提供され、アプリはそのウィンドウ内でサービスを作成して使用できます。
そのウィンドウの終了時に、アプリはアイドル状態であると見なされます。 システムはこの時点で、アプリがサービスのService.stopSelf()
メソッドを呼び出したかのように、アプリのバックグラウンド サービスを停止します。
バックグラウンド実行制限 | Android Developers
またマルチタスクメニューからアプリをユーザーが終了させることもあります。
startForegroundService()
バックグラウンドでアプリが実行できないと、一番困るのはmusic系のアプリでしょうか、それについては
バックグラウンド実行制限 | Android Developersにはこのような一文があります。
Android 8.0 では、追加機能があります。システムは、バックグラウンド アプリによるバッグラウンド サービスの作成を許可しません。
そのため、Android 8.0 では、フォアグラウンドで新しいサービスを作成する Context.startForegroundService() メソッドが新たに導入されています。システムによってサービスが作成されると、アプリは、サービスの startForeground() メソッドを 5 秒以内に呼び出して、
その新しいサービスの通知をユーザーに表示します。
アプリが startForeground() を制限時間内に呼び出さない場合、サービスが停止し、アプリが ANR になります。
ということでこれを使ってMediaPlayerでをServiceで音楽再生してみます。
サンプルコード
ActivityからServiceを開始し、Service内で5秒以内にstartForegroundを呼び出します。
これでステータスバーにはアイコンが表示されてForgroundにいるかのようになります
マニュフェストには、
- FOREGROUND_SERVICEのpermission設定
- <service … />の追加
- バックグラウンドに入った後で、通知からActivityを起動させるための<activity>設定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" ... <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <application ... > <activity ... android:taskAffinity="" android:excludeFromRecents="true"> ... </activity> <service android:name=".TestService" /> </application> </manifest> |
MainActivityではServiceの開始とストップのためのボタンを設置
MainActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
//package your.package.name; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.content.Intent; import android.widget.Button; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button buttonStart = findViewById(R.id.button_start); buttonStart.setOnClickListener( v -> { Intent intent = new Intent(getApplication(), TestService.class); // Serviceの開始 startService(intent); }); Button buttonStop = findViewById(R.id.button_stop); buttonStop.setOnClickListener(nv -> { Intent intent = new Intent(getApplication(), TestService.class); // Serviceの停止 stopService(intent); }); } } |
サービス側では、MediaPlayerを使ってmp3を再生させてみます。
MediaPlayerについてはこちらで解説しています。
Rawフォルダーを作成して適当なmp3の音楽ファイルを入れておきます。
以下のJavaクラスを作成しコーディング
TestService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
//package your.package.name; import androidx.annotation.Nullable; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.media.MediaPlayer; import android.os.IBinder; import android.util.Log; import android.widget.Toast; public class TestService extends Service { private MediaPlayer mediaPlayer; private int counter = 0; @Override public void onCreate() { super.onCreate(); Log.d("debug", "onCreate()"); // ..\res\raw\sample.mp3 mediaPlayer = MediaPlayer.create(this, R.raw.sample); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d("debug", "onStartCommand()"); Context context = getApplicationContext(); String channelId = "default"; String title = context.getString(R.string.app_name); // 通知からActivityを起動できるようにする Intent notifyIntent = new Intent(this, MainActivity.class); // Set the Activity to start in a new, empty task notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntent.getActivity( this, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); // Notification Channel 設定 NotificationChannel channel = new NotificationChannel( channelId, title , NotificationManager.IMPORTANCE_DEFAULT); if(notificationManager != null){ notificationManager.createNotificationChannel(channel); Notification notification = new Notification.Builder(context, channelId) .setContentTitle(title) // android標準アイコンから .setSmallIcon(android.R.drawable.ic_media_play) .setContentText("MediaPlay") .setAutoCancel(true) .setContentIntent(pendingIntent) .setWhen(System.currentTimeMillis()) .build(); // startForeground startForeground(1, notification); audioStart(); } return START_NOT_STICKY; //return START_STICKY; //return START_REDELIVER_INTENT; } private void audioStart(){ counter++; Log.d("debug","audioStart: "+counter); if(mediaPlayer != null){ // ループ mediaPlayer.setLooping(true); // ループしない //mediaPlayer.setLooping(false); // 再生する mediaPlayer.start(); // トースト String str ="Start Walking\n(c)Music-Note.jp"; Toast toast = Toast.makeText(this, str, Toast.LENGTH_LONG); toast.show(); // 終了を検知するリスナー mediaPlayer.setOnCompletionListener(mp -> { Log.d("debug","end of audio"); audioStop(); // Service終了 stopSelf(); }); } } private void audioStop() { // 再生終了 mediaPlayer.stop(); // リセット mediaPlayer.reset(); // リソースの解放 mediaPlayer.release(); mediaPlayer = null; } @Override public void onDestroy() { super.onDestroy(); Log.d("debug", "onDestroy()"); if(mediaPlayer != null){ Log.d("debug","end of audio"); audioStop(); } // Service終了 stopSelf(); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } } |
レイアウトです。簡単にするためLinearLayoutを使いました。
activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/button_start" android:text="@string/start" android:layout_margin="40dp" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.4" /> <Button android:id="@+id/button_stop" android:text="@string/stop" android:layout_margin="40dp" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.6" /> </androidx.constraintlayout.widget.ConstraintLayout> |
strings.xml
1 2 3 4 5 |
<resources> <string name="app_name">YourAppName</string> <string name="start">Start Service</string> <string name="stop">Stop Service</string> </resources> |
これでアプリを終了、マルチタスクメニューから削除してもmp3が再生されています。
ただし、あまり調子に乗っていると、Systemから「○○が電池をしようしています」とユーザーにチクられます。
注意点:簡単なAudio再生をServiceとForegroundでやった例で、実際には検討すべき点があります
- あくまでメインスレッドでのAudio再生なので、別スレッド作成の必要性があるか検討
- システムにより強制終了させられた場合に、START_STICKYあるいはSTART_REDELIVER_INTENTを使用して再起動させるか検討
- 今更ですがAndroid12から、フォアグラウンド サービスの起動に関する制限があります。非推奨ではないのですが…
注: あるアプリが、別のアプリが所有しているフォアグラウンド サービスを起動するために Context.startForegroundService() を呼び出すとき、両方のアプリが Android 12 をターゲットとしている場合にのみ、このページで説明する制限が適用されます。
関連ページ:
References:
サービス | Android Developers
バックグラウンド実行制限 | Android Developers
Service | Android Developers