こんにちは、開発の小野知之です。
先月の続き、後編です。
今回は余力不足で挿し絵が無いです。後で追加するかもしれません。
ところでこの話、ウィジェット作成時に発生した現象ではありますが、結論から言えば Activity を使った普通のアプリでも同様に発生する可能性があります。Android アプリを開発する方は、知っておくと何か役に立つことがあるかもしれません。
【前編のあらすじ】
終了したはずのアプリのプロセスが突如復活するという怪現象が発生。
一度は原因が判明し、対策も万全と思われたが、平穏な日々はそう長くは続かなかった。
消滅したはずのプロセスが、いったいどこから、どうやって蘇ったのか。そして、復活を阻止する手段はあるのか…?
PID;5 深淵
5-1: ルート
ここまでにわかった、復活現象発生までの流れをもう一度整理してみます。
- アプリが終了すると、そのプロセスは、終了したときの状態を保持したまま、「キャッシュしたプロセス」としてメモリー上に残っている。
- 端末の時刻は、OSの自動修正機能により不定期に変更(修正)される。その通知は、キャッシュ上のプロセスにも届く。
- キャッシュに残っている私のウィジェットのプロセスは、時刻変更の通知を受けると「描画処理」が呼び出される。そして停止していた「描画サービス」が再開され、「実行中」一覧に再び現れる。
これと、現在の最新の構造概要を見比べてみます。
この構造なら、ウィジェット終了時に「終了フラグ」が ON になっているのだから、時刻変更の通知が来ても「描画サービス」が再開されることは無いはずです。実際に今まで何度も手動で時刻を変更し、決して復活しないことを確認してきています。
ところがこれでも一部の端末では、描画サービスが再開され、プロセスが復活することがあるのです。
まだどこかに別の復活ルートがあるようです…。
5-2: クリア
改めてソースファイルを見なおしましたが、可能性としてはもう、
『時間が経過すると、キャッシュしたプロセスは、中身がクリア(初期化)されてしまうことがある』
というくらいしか考えられませんでした。
つまり、「終了フラグ」がいつのまにか OFF になり、その状態で時刻変更の通知が来れば、プロセスは復活してしまいそうです。
問題はこの「クリアされる」現象の正体です。これがわからないと、対策しようが無さそうです。
そして、あれこれと調べて、気になる点見つけました。
5-3: 残骸
Android OS は、システムの空きメモリーが不足すると、「キャッシュしたプロセス」を自動的に解放していきます。解放されたプロセスは、システム設定の「アプリケーション」-「実行中」にある「キャッシュしたプロセス」一覧からも消えます。
しかし、もしもこの時、
『解放されたプロセスは、変数などの中身がクリアされるものの、実行プロセス自体はまだメモリー上のどこかに残っている』
としたら…?
そしてその「残骸」にも通知が届き、反応するとしたら…?
5-4: 不可視
「いやいや、いくらなんでもそんな都合の良い話は無いだろう。」
そう思いますよね。
なにしろ、OS がプロセスを「解放した」はずなのに、まだメモリー上の見えないどこかに残っているだなんて。
でも、試す方法はあります。
「キャッシュしたプロセス」一覧でアプリの詳細を開くと、「停止」というボタンがあります。これを押すと、OS が行うのと同様に、そのプロセスは解放され、一覧から消えます。この、不可視になった状態で、時刻変更の通知を送ってみればいいのです。
「ここまで来たらもう、何でも試してみよう…」
PID;6 虚空
6-1: 消滅
まず改めて、「終了フラグ」の効果を確認してみます。
ウィジェットを終了させ、「キャッシュしたプロセス」一覧に名前がある状態で、端末の時刻を変更してみます。
「復活…しない。よしOKだ。」
「終了フラグ」の効果が想定通りに現れています。
では次に、「キャッシュしたプロセス」一覧からアプリの詳細を開き、「停止」ボタンを押します。アプリ名が一覧から消えます。
完全に消滅したようにしか思えないのですが、本当にこの状態からでも復活するのでしょうか…?
6-2: 蘇生
端末の時刻を変更し「実行中」一覧を見ると…
「…あ、アプリ名がある! 復活している!」
なんと…。
「キャッシュしたプロセス」は、解放しても、中身がクリアされたプロセスの残骸がまだ、見えないどこかに存在し続けていたのです。
そして、時刻変更の通知はこの「残骸」にも届いており、それがきっかけで「蘇生」していたのです!
6-3: 不死鳥
これが、「終了フラグ」で対策しても復活してしまうことがある現象の正体のようです。
端末を使い続け、他のアプリにより端末の空きメモリーが減ってくると、OS によってキャッシュのプロセスが自動的に解放されていきます。しかし解放されたプロセスは、中身がクリアされ停止してもなお、まだメモリー上で眠り続けており、通知を受けて復活もするのです。
「こいつは…不死鳥!?」
しかし、ようやくこれで、次の対策手段が見えてきました。
PID;7 封印
7-1: 阻止
キャッシュしたプロセスは、解放された後も、中身がクリアされた状態でまだメモリー上に残っているらしいことがわかりました。
この残骸に OS から時刻変更の通知が来ても、「終了フラグ」は OFF なので正しく機能せず、プロセスは復活してしまいます。
ならば、「まだクリアされていない状態」かどうかを判別できれば、時刻の変更通知を適切に無視することで、残骸からの復活を阻止できそうです。
そこで、「終了フラグ」と同様に、メンバー変数として「起動フラグ」を追加する方法を考えました。このフラグを、初期状態では OFF にしておき、「起動処理」の中で ON にすれば、クリアされているかどうかを区別できるはずです。
7-2: 再戦
こうして、「起動」と「終了」、両方の出入り口にフラグを立てることになりました。
アプリが起動する前、または終了後に OS により解放された状態では、いずれのフラグも OFF になっています。
そして、「起動フラグ」が OFF、または「終了フラグ」が ON のときは、「起動中ではない(アプリ終了後である)」と判断し、時刻変更の通知が来ても無視すれば良いはずです。
二つのフラグを反映した構造概要は下記のようになりました。
アプリが起動し「起動処理」が正常に実行されたときのみ、「起動フラグ」は ON になります。
アプリが終了し「終了処理」が正常に実行されたときのみ、「終了フラグ」は ON になります。
この対策で、謎の不死鳥プロセスとの再戦に挑みます!
※ここでは、「起動フラグ」と「終了フラグ」を別々に用意していますが、実際には他の同様の方法でも可能なはずです。
5-3: 完璧
まず、ウィジェットを一旦起動し、終了します。
「キャッシュしたプロセス」にアプリ名が現れるので、詳細を開いて「停止」。一覧から名前が消えたことを確認します。
そして、端末の時刻設定を変更し、「実行中」一覧を開くと…。
「現れ…ない! 復活しないぞ!!」
直後に「キャッシュしたプロセス」一覧を開くと、そこにはこのウィジェットの名前が再び現れていました。
つまり、時刻変更の通知により起こされたプロセスが、二つのフラグの判定処理により復活を阻止され、そのままキャッシュへ移されてしまったというわけです。
今度こそきっと、完璧です!
5-4: 終焉
二本のフラグによる対策を施したウィジェットを、10台以上のテスト用端末にインストールし、何度も動作確認を行いました。そして、ウィジェットを終了してから何日も何日も、監視を続けました。
しかしもう二度と、復活現象が発生することはありませんでした。
更に、ある程度の時間が経過すると、端末の時刻を変更しても「キャッシュしたプロセス」にすら現れなくなることも確認できました。
見えないどこかに眠っているプロセスも、いずれはOSに本当に削除され、跡形もなく消滅してしまうのでしょう。
今度こそ「封印」に成功したのです!
長い戦いの日々が、ようやく終焉を迎えました。
第二第三の不死鳥プロセスが現れぬことを願いつつ…。
~ 完 ~
まとめ
まとめ-1: 同様の現象
今回は、「ウィジェット」に対する「時刻変更の通知」がきっかけで、アプリ終了後にプロセスが復活する現象が発生しました。
しかし、これは一例に過ぎません。ウィジェットではない通常のアプリでも、または他の種類の通知であっても、同様の現象が発生する可能性があります。
例えば、Intent を sendBroadcast() で投げて連携する2つのアプリがあったとします。
この場合、Intent の受け取り側アプリが終了していたとしても、投げたIntent が、メモリー上にまだ残っているプロセスに届いてしまう場合があります。
もしも受け取り側アプリが、Intent を受け取ることでサービスを開始するような作りになっていた場合、プロセスは復活してしまうわけです。
「受け取り側アプリが終了していれば無視されるだけだろう」と思って余計な Intent を投げたりしないよう、注意してください。
特に、OS からの通知に反応するようなアプリの場合は、単独アプリであっても発生します。今回紹介したような何らかの対策が必要だろうと思います。
まとめ-2: プロセスの状態推移
今回の件に関連し、Android OS のプロセスの状態推移について、参考になる解説文書があるので紹介します。
https://developer.android.com/guide/components/processes-and-threads.html?hl=ja
この「プロセス」の章を見ると、プロセスが5つの状態に分類されていることがわかります。
今回の件と絡めて簡単にまとめると、こんな感じです。
Android OSは、システムの空きメモリー量が不足すると、優先度の低いプロセスから順に停止・解放し、空きメモリーを確保する。優先度の高い順に書くと下記のように分類される。
1.Foreground process
ユーザーが画面で現在操作しているプロセス。
2.Visible process
操作はしていないが画面に見えているプロセス。
3.Service process
見えていないが、Service を実行することでバックグラウンドで動き続けているプロセス。たとえば音楽再生やファイルダウンロードなど。
システム設定の「アプリケーション」-「実行中」一覧に表示されるのは、このような Service を実行しているアプリの名前。
4.Background process
画面に見えず、かつ Service も実行されていないプロセス。
他アプリに切り替えられ裏に回ったアプリ(ブラウザやメーラーなど)や、終了したウィジェットなど。
「キャッシュしたプロセス」一覧は、この Background process を表示している。
画面から見えなくなったときの状態(変数や確保したメモリーなど)を保持しているため、再度実行された時に素早く起動し、直前の状態を復元する。
5.Empty process
Background process (キャッシュしたプロセス)がOS によって解放させられたもの。
空きメモリーを確保するため、確保したメモリーなどがすべて解放(クリア)され停止している、「空の」プロセス。
アプリの再実行を少しでも早くするためにメモリー上に残っているが、システムの空きメモリーが不足すると OSにより最優先で削除される。
今回の件は、この 4 と 5 の状態にあるプロセスに対し、端末の時刻変更の通知が送られたことで、ウィジェットの描画サービスが再び動き始め、3 の状態に推移したというわけです。
また、Empty process は最優先で削除されていくので、時刻変更の通知が届く前に完全に消滅している場合もあります。当然ですが、この場合にはもう復活しません。
まとめ-3: 覚えておいてください
ところで、Background process や Empty process にも端末の時刻変更などの「通知」が届くことや、それがきっかけで Service が再開される可能性については、ほとんど紹介されていないようです。
わかってしまえば、どのような状態のプロセスであっても、OS から「起動しろ」という「通知」を受け取れるようになっているのだから、当然かと言えば当然です。
しかし、完全に消滅したように見えるプロセスに対し、再起動とは全く無関係であろう通知でも届くということには、実際に遭遇してみないとなかなか気付きにくいのではないでしょうか。
しっかり対策をしていないと、終了したはずのプロセスがいつのまにか「実行中」に復活し、CPUタイムやバッテリーを消費し続けるという非常に困ったことにもなりかねません。
是非、Background process と Empty process について、覚えておいてください。
[おわり]