Xamarin.FormsでBackgroundタスク
https://robgibbens.com/backgrounding-with-xamarin-forms/robgibbens.com
この記事と動画のとおりにXamarin.FormsでAndroid,iOSの両方でBackgroundタスクを実行するテストを行った.
MessagingCenterを使用し,以下を行っている.
- Android,iOSの各プロジェクトで実装したタスク,サービスを開始,停止
- Sharedが送信,Nativeが受信
- タスク,サービスからのデータの送受信
- Nativeが送信,Sharedが受信
- 受信したらUIを更新.
0.25秒ごとに数字をカウントアップしていく単純なもの.
「START LONG RUNNING TASK」ボタンを押すと開始する.
このアプリが非アクティブのときにも動いていることがわかるように10カウント毎に通知されるようにしてある.
日アクティブでも動作していることがわかる.
問題
しかし,一定時間(1分ぐらい?)経過すると,カウントが停止する.
これは,Android 8.0(APIレベル26以降)ではバッテリーやCPUなどのリソースの節約のために一定時間でバックグラウンドのタスクを終了するようになったため.
もともと,バックグラウンドでも位置情報を取得し続けるために調べはじめたことで,やはり同様に位置情報の取得に関しても制限がかかるようになっていた.
解決の方針
以下のページに詳しく書いてある.
JobSchedulerを使う
例えば,
多くの場合、アプリではバックグラウンド サービスを JobScheduler ジョブに置き換えることができます。
とあることから,Serviceを使って動かし続けるのではなくJobSchedulerで定期的にスポット的に実行することで実現できるし,それがより良い方法のようだ.
FCM(Firebase Cloud Messaging)を使う
FCMを使ったPush通知の仕組みを使うと,アプリを実行していなくてもFCMを介したメッセージの受信ができる.
それを起点とした処理を記述することができるので,FCMから定期的にメッセージを送ればバックグラウンドで定期継続実行が実現できる.
FCM+Azure Notification Hubsを使ったPush通知の実装方法は公式ドキュメントに詳細に解説されていて,そのとおりに行えば難しくはない.
しかし,当然ネットワークにつながっている必要があったり,台数とか金額とか,ちょっとローカルでバックグラウンド処理したいだけなので,要件には合わない.
Bound Serviceを使う
バインドされたサービスというものがあり,以下のようにも書かれている.
注: これらのルールは バインドされたサービスには一切影響を与えません。 アプリでバインドされたサービスを定義している場合、アプリがフォアグラウンドにあるかどうかに関係なく、別のコンポーネントをそのサービスにバインドできます。
バインドされたサービスとは何かについては以下のページに書かれている.
Xamarin.Androidでの実装についてはこちら.
何やら複雑そうだ. これはサービスを(端末内で)サーバーのように動作させて,リクエストとそのレスポンスという形式のようだ.
Foreground Serviceを使う
Youtubeとか動画,音楽再生アプリで再生中に上に通知が出て,実行中だと表されるもの.
ForegroundServiceの実装方法. シンプルでわかりやすい↓
ただし,NotificationをStartForegroundメソッドにわたす際には,Channelを作成しておく必要があるので注意.
private void CreateNotificationChannel() { _notificationManager = (NotificationManager)Android.App.Application.Context.GetSystemService(Android.App.Application.NotificationService); if (Build.VERSION.SdkInt >= BuildVersionCodes.O) { var channelNameJava = new Java.Lang.String(_channelName); var channel = new NotificationChannel(_channelId, channelNameJava, NotificationImportance.Default) { Description = _channelDescription, }; _notificationManager.CreateNotificationChannel(channel); } _channelInitialized = true; }
いまのところ
サーバーのように待ち受けている必要も,リアルタイムである必要もないので,定期実行のJobSchedulerがおそらくシンプルなのではないかと思う.
結果
JobScheduler
JobSchedulerを使ったところ,一定時間経過してもバックグラウンドで動作し続けたが,最低実行間隔は後述のとおり15分間であることを忘れないように.
この記事の冒頭部分に,
Google I/O 2018において発表された「JetPack」において、Androidのバージョンに応じてそれらの処理を切り替えるWorkManagerが含まれているので、今後はそちらを利用していくべきかと思います。こちらを利用すると、内部的には以下のように処理が切り替わるそうです。
とあるので,WorkManagerを使わなければならない可能性が出てきた.
上記記事は最低実行間隔について検証もしている.
定期実行する場合、ジョブID毎に実行間隔を指定でき、最小間隔は約15分。それより小さい時間間隔を指定しても15分に設定される。 実行間隔は前後し、正確な周期は保証されない。 最大実行時間は約10分間。(5.1.1までは1分間だった模様) DOZE状態では動作しなくなり、一定のタイミングで訪れるメンテナンスウィンドウでまとめて実行される。 同時に実行可能なジョブの数には上限があり、環境によって異なる。
なんと,一回のジョブの最大実行時間は10分ということなので,一回のJobでループ回しっぱなしというのは当然途中で止まる.
しかも,OSが省エネ動作を行うのでその実行間隔はだんだん開いたりするようだ.
Jobの設定についてはJobInfoクラスのBuilderメソッドで実行間隔などを指定できるので,それを使う.
JobInfo info = new JobInfo .Builder(0) // JobID=0の指定 .setPersisted(true) // 端末再起動後も実行する .setPeriodic(0, JobInfo.getMinFlexMillis()) // 実行間隔=0と実行遅延許容時間の指定 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_NONE) // ネットワーク状態と無関係に実行 .build(); scheduler.schedule(info);
ふむふむ.
あと,端末再起動後もJobを実行するには以下のパーミッションを得る必要があるようだ. このあたりは試していない.
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
↑setPersisted(true)
にすると実行時エラーになる.RECEIVE_BOOT_COMPLETEDにチェックつけてるのに.
JobSchedulerについてはこちらも詳しい.
他にも参考 Xamarin.Androidでの実装についてはこちら.
やってみた結果
やってみたが,非アクティブ時には定期的には実行されなかった.
どういうタイミングかはよくわからなかったが,気がついたら実行されていた.
Foreground Service
foregroundServiceを使ったらできた.
詳しくは別で書く.
こちら,とても詳しい qiita.com
Androidの各種サービスについて
OnStartCommand
について説明がある.
ソースコード
以下の3つのブランチがある
- foreground_service
- foregroundサービスとしてタスクを実行
- use_job_service
- JobServiceを使った定期実行
- 15分間隔
- master
- 普通のService
- バックグラウンド1分で停止