Servce はバックグラウンドで作業をさせたい場合に使います。システムは直ぐにスリープに入れてしまいます。それでも裏で色々やりたい場合に有効です。例えば音楽の再生など
Oreoからはバックグラウンドの制限ができて、裏でごにょごにょ派にはイバラの道となりました。startForegroundService()を使うとか…
2021.1.1
Service
Serviceがバックグラウンドで動作するので、非同期と勘違いしそうですがそうではなく、Activityから表示UIを無くしたようなコンポーネントです。ですから別スレッドでの処理が必要なものはActivityと同じように扱います。
例としては、バックグラウンドでのネットからのデータダウンロードなどがあります。
別スレッド実装は IntentService を使うと簡単になります。
また、サービス | Android Developers にはこのようにあるので IntentService を推奨しているようでもあります。
開始されたサービスで同時に複数の要求を処理する必要があることはほとんどないため(実際には危険なマルチスレッド シナリオになります)、
IntentService
クラスを使用してサービスを実装するのが最適だと考えられます。
IntentServiceはこちらを参考に、
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()
- サービスで実行させたいコードはここに記述
- 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(); |
と言いながらも、昨今はAndroidのバッテリー消費にうるさくなっているのでしょうか、サービス | Android Developersにはこのような記述もあります。
Android システムは メモリが少なくなって、ユーザーが使用しているアクティビティ用のシステムリソースを回復させる必要が生じた場合のみ、サービスを強制的に停止させます…
またOreoからバックグラウンドに対する制限ができて
アプリがバックグラウンドに移行すると数分間のウィンドウが提供され、アプリはそのウィンドウ内でサービスを作成して使用できます。
そのウィンドウの終了時に、アプリは?アイドル状態?であると見なされます。 システムはこの時点で、アプリがサービスの?Service.stopSelf()
?メソッドを呼び出したかのように、アプリのバックグラウンド サービスを停止します。
バックグラウンド実行制限 | Android Developers
またマルチタスクメニューからアプリをユーザーが終了させることもあります。
startForegroundService()
バックグラウンドでアプリが実行できないと、一番困るのはmusic系のアプリでしょうか、それについては
バックグラウンド実行制限 | Android Developersにはこのような一文があります。
Android 8.0 では、追加機能があります。システムは、バックグラウンド アプリによるバッグラウンド サービスの作成を許可しません。
そのため、Android 8.0 では、フォアグラウンドで新しいサービスを作成する Context.startForegroundService() メソッドが新たに導入されています。システムによってサービスが作成されると、アプリは、サービスの startForeground() メソッドを 5 秒以内に呼び出して、
その新しいサービスの通知をユーザーに表示します。
アプリが startForeground() を制限時間内に呼び出さない場合、サービスが停止し、アプリが ANR になります。
ということでこれを使ってMediaPlayerでをServiceで音楽再生してみます。
サンプルコード
Activityから startForegroundService() でServiceを開始し、Service内で5秒以内に
startForegroundを呼び出します。これでステータスバーにはアイコンが表示されてForgroundにいるかのようになるということでしょう。
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 31 32 |
//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); intent.putExtra("REQUEST_CODE", 1); // Serviceの開始 //startService(intent); startForegroundService(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 |
//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()"); int requestCode = intent.getIntExtra("REQUEST_CODE",0); Context context = getApplicationContext(); String channelId = "default"; String title = context.getString(R.string.app_name); PendingIntent pendingIntent = PendingIntent.getActivity(context, requestCode, intent, 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: "+String.valueOf(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(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer 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; } } |
permissionとServiceの設定を入れます
AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" ... <!-- API 28 --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <application ... </activity> <service android:name=".TestService" /> </application> </manifest> |
レイアウトです。簡単にするため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から「○○が電池をしようしています」とユーザーにチクられます。
また、このstartForegroundService()もAndroid 8.0からなので、APIによる切り分けが必要になりそうです。
関連ページ:
References:
サービス | Android Developers
バックグラウンド実行制限 | Android Developers
Service | Android Developers