Servce はバックグラウンドで作業をさせたい場合に使います。システムは直ぐにスリープに入れてしまいます。それでも裏で色々やりたい場合に有効です。例えば音楽の再生など
2024.1.1
Service
Serviceがバックグラウンドで動作するので、非同期と勘違いしそうですがそうではなく、Activityから表示UIを無くしたようなコンポーネントです。
サービスは、そのホスティング プロセスのメインスレッドで実行され、サービスが自身のスレッドを作成することはなく、別のプロセスで実行されることもありません
CPU を集中的に使う作業、Audioの再生やネットワーク作業など、を行う場合は、サービス内に新しいスレッドを作成してその作業を行う必要があります
Serviceの開始
Serviceを開始するには、
- startService()
- 開始後は、開始したActivityが破棄されても基本的には実行し続けられる、Activityとは別の独自のContextを持っている
- 呼び出し側からの制御は開始か停止させるのみで、タスクの終了をコールバックしないので自身で終了するなどの手当が必要
- bindService()
- 要求を送信したり、結果を取得したりとServiceを制御できますが、呼び出し元のActivityが終了すると一緒に終了してしまう
これにAndroid 8.0からの追加として、
startForegroundService()
を使ってバックグラウンド実行制限をある程度回避できます。
また、API 28からAndroidMnifestにpermission登録が必要になりました
さらにAPI34からはフォアグラウンド サービスタイプの設定が必要です
startService()
Activity からServiceを呼び出すのはIntent をセットして、startService(intent) で行い
アプリの安全性のため明示的インテントを使用
1 2 3 |
val intent = Intent(application, TestService::class.java) // Serviceの開始 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 |
class TestService : Service() { override fun onCreate() { super.onCreate() } override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { // ... return START_NOT_STICKY } override fun onDestroy() { super.onDestroy() } override fun onBind(intent: Intent): IBinder? { 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <application ... <activity ... </activity> <service android:name=".TestService" android:foregroundServiceType="mediaPlayback" android:exported="false" /> </application> </manifest> |
Serviceの終了
startService()で始めたServiceは勝手には止まりません。
MainActivityからstopService(intent)で止める
1 2 3 |
val intent = Intent(application, TestService::class.java) // Serviceの停止 stopService(intent) |
サービスが自身で止められるようにするには、stopSelf()を使います
1 |
stopSelf() |
Oreoからバックグラウンドに対する制限ができて
アプリがバックグラウンドに移行すると数分間のウィンドウが提供され、アプリはそのウィンドウ内でサービスを作成して使用できます。
そのウィンドウの終了時に、アプリは?アイドル状態?であると見なされます。 システムはこの時点で、アプリがサービスの?Service.stopSelf()
?メソッドを呼び出したかのように、アプリのバックグラウンド サービスを停止します。
バックグラウンド実行制限 | Android Developers
またマルチタスクメニューからアプリをユーザーが終了させることもあります。
startForegroundService()
バックグラウンドでアプリが実行できないと、一番困るのはmusic系のアプリでしょうか、それについては
バックグラウンド実行制限 | Android Developersにはこのような一文があります。
システムは、バックグラウンド アプリによるバッグラウンド サービスの作成を許可しません。
そのため、Android 8.0 では、フォアグラウンドで新しいサービスを作成する Context.startForegroundService() メソッドが新たに導入されています。システムによってサービスが作成されると、アプリは、サービスの startForeground() メソッドを 5 秒以内に呼び出して、
その新しいサービスの通知をユーザーに表示します。
アプリが startForeground() を制限時間内に呼び出さない場合、サービスが停止し、アプリが ANR になります。
ということでこれを使ってMediaPlayerでをServiceで音楽再生してみます。
サンプルコード
ActivityからServiceを開始し、Service内で5秒以内にstartForegroundを呼び出します。
これでステータスバーにはアイコンが表示されてForgroundにいるかのようになります
MainActivityではServiceの開始とストップのためのボタンを設置
MainActivity.kt
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 |
//package com.example.kotlinserviceapp import android.content.Intent import android.os.Bundle import android.widget.Button import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContentView(R.layout.activity_main) ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets } val intent = Intent(application, TestService::class.java) val buttonStart = findViewById<Button>(R.id.button_start) buttonStart.setOnClickListener { intent.putExtra("REQUEST_CODE", 1) // Serviceの開始 startService(intent) } val buttonStop: Button = findViewById(R.id.button_stop) buttonStop.setOnClickListener { // Serviceの停止 stopService(intent) } } } |
サービス側では、MediaPlayerを使ってmp3を再生させてみます。
MediaPlayerについてはこちらで解説しています。
Rawフォルダーを作成して適当なmp3の音楽ファイルを入れておきます。
以下のKotlinクラスを作成しコーディング
TestService.kt
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 |
//package com.example.kotlinserviceapp import android.app.* import android.content.Intent import android.media.MediaPlayer import android.os.IBinder import android.util.Log import android.widget.Toast class TestService : Service() { private lateinit var mediaPlayer: MediaPlayer override fun onCreate() { super.onCreate() Log.d("debug", "onCreate()") // ..\res\raw\sample.mp3 mediaPlayer = MediaPlayer.create(this, R.raw.sample) } override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { Log.d("debug", "onStartCommand()") val context = applicationContext val channelId = "default" val title: String = getString(R.string.app_name) val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val pendingIntent: PendingIntent = Intent(this, MainActivity::class.java).let { notificationIntent -> PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } // Notification Channel 設定 val channel = NotificationChannel( channelId, title, NotificationManager.IMPORTANCE_DEFAULT ) notificationManager.createNotificationChannel(channel) val notification = Notification.Builder(context, channelId) .setContentTitle(title) // android標準アイコンから .setSmallIcon(android.R.drawable.ic_media_play) .setContentText("MediaPlay") .setContentIntent(pendingIntent) .setAutoCancel(true) .setWhen(System.currentTimeMillis()) .build() // startForeground startForeground(1, notification) audioStart() return START_NOT_STICKY //return START_STICKY; //return START_REDELIVER_INTENT; } private fun audioStart() { // ループ mediaPlayer.isLooping = true mediaPlayer.start() // トースト val str = "Start Walking\n(c)Music-Note.jp" val toast: Toast = Toast.makeText(this, str, Toast.LENGTH_LONG) toast.show() // 終了を検知するリスナー mediaPlayer.setOnCompletionListener { Log.d("debug", "end of audio") audioStop() // Service終了 stopSelf() } } private fun audioStop() { // 再生終了 mediaPlayer.stop() // リセット mediaPlayer.reset() // リソースの解放 mediaPlayer.release() } override fun onDestroy() { super.onDestroy() Log.d("debug", "onDestroy()") audioStop() // Service終了 stopSelf() } override fun onBind(intent: Intent): IBinder? { return null } } |
マニュフェストに追加するものは、
- FOREGROUND_SERVICEのpermission設定
- <service … />の追加とサービスタイプの指定
- android:foregroundServiceType=”mediaPlayback”
- バックグラウンドに入った後で、通知からActivityを起動させるための<activity>設定
- android:taskAffinity=””
- android:excludeFromRecents=”true”
AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <application ... <activity ... android:taskAffinity="" android:excludeFromRecents="true" ... </activity> <service android:name=".TestService" android:foregroundServiceType="mediaPlayback" android:exported="false" /> </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">KotlinServiceApp</string> <string name="start">Start Service</string> <string name="stop">Stop Service</string> </resources> |
これでアプリを終了、マルチタスクメニューから削除してもmp3が再生されています。
フォアグラウンドサービスはAPIによって変更が多々あるので確認が必要です
ただし、あまり調子に乗っていると、Systemから「○○が電池をしようしています」とユーザーにチクられます。
関連ページ:
References:
サービス | Android Developers
バックグラウンド実行制限 | Android Developers
Service | Android Developers