【Androidアプリ開発ドキュメンタリー】 虚空より突如蘇る、不死鳥プロセス ~前編~

こんにちは、開発の小野知之です。

今回は、私が実際にAndroidアプリを開発していて遭遇した体験を、前後編の2回にわたってお送りします。アプリ開発される方はもちろん、Androidの内部動作について興味ある方にも、何かの参考になるかもしれません。

なお、挿し絵や説明図は全部、私が コミPo! で作りました。

PID:1 復活

1-1: 実行中

それは私が、Android のウィジェット・アプリを作成していたときのことです。

ウィジェットというのは、ホーム画面に置いて常駐させて使う、時計とかバッテリーメーターとかの、あれです。私が作ったものも、非常にシンプルな単なるアナログ時計でした。

ところがある日、おかしなことに気付きました。
ところがある日、おかしなことに気付きました。

アプリ開発に使っている Android 端末で、システム設定の「アプリケーション」-「実行中」一覧を見ると、そこには、私のウィジェットの名前があったのです。

使っていないのに。ホーム画面に、そのウィジェットの姿は無いのに。
使っていないのに…。
ホーム画面に、そのウィジェットの姿はないのに…。
通常の Activity のアプリ(ウェブブラウザなど)であれば、別におかしなことではありません。使っていなくても非表示のままで動いているアプリは多くあります。
しかしウィジェットは通常、意図的にサービスを常駐させない限り、ホーム画面から削除すれば動作を停止し「実行中」一覧からは消えるはずです。もちろん私のウィジェットもそのように作ってあります。

どうやら、私のウィジェットには何らかの「正常に終了しない不具合」があるらしく、画面に表示されないにもかかわらず内部的な処理のみが動き続けているようなのです。
「ゾンビ化」というやつですね。
しかも、この状態でもCPUタイムやバッテリーはしっかり消費し続けているので大迷惑です。

早速、原因を調べることにしました。

1-2: 確認

まずはとにかく、症状を再現させないことには始まりません。ウィジェットを終了(ホーム画面から削除)したあと、すぐに「実行中」一覧を確認すればわかるはずです。

しかし、何度試しても、再現しないのです。

ウィジェットを起動すると「実行中」一覧に名前が現れ、終了するとすぐに消えます。まったく問題ありません。10台以上の、Android バージョンやメーカーの異なる端末で試しましたが、いずれも終了処理は正常に動作しているようなのです。

『やはりあれは、たまたま運悪く何かが起こって、終了処理に失敗しただけなのかもしれない。』

そう思うしかありませんでした。

1-3: 復活

ところが、数日後。

前日までは無かったのに・・・。

一つの端末で「復活」、していたのです。

画面には表示されていないのに、「実行中」一覧に私のウィジェットの名前があったのです。
前日まではなかったのに…。
更に後日、他の端末でも復活していました。

ウィジェットを終了し、停止したはずのプロセスが、何かの「きっかけ」で数日後に再び動きはじめたのです。
「なんなんだ…こいつはいったい…!?」

1-4: 始まり

復活したプロセスは、見えないどこかで動いているので、普通に停止させることはできません。ところが、「実行中」一覧で選択して「停止」をしてもすぐにまた復活してしまいました。

そこで、「ダウンロード」一覧で選択し「強制停止」をしたところ、ようやく復活しなくなりました

謎のゾンビプロセスとの闘いが始まりました。

終了したはずのプロセスは、どこに残っているのか?
停止したはずのプロセスが、なぜ「復活」するのか?
不定期に発生する、復活の「きっかけ」とは?
復活を対策できるのか?

謎のゾンビプロセスとの闘いが始まりました。
※端末やAndroidのバージョンによっては、システム設定での表記が「アプリケーションの管理」「ダウンロード済み」「実行中のサービス」などのように若干異なる場合があります。

PID;2 輪廻

2-1: 流れ

まず、このウィジェットの処理の流れについて、簡単に紹介しておきます。
大きく分けて、3つの処理ブロックで構成されています。

時計ウィジェットの構造概要

ウィジェットをホーム画面へ設置すると「①起動処理」が実行されます。
OSのアラーム機能(タイマー処理)を使うことで、指定時刻(例えば1分後)になると「②描画処理」が呼び出されます。「②描画処理」では、「ウィジェット描画サービス」を実行することで時計の文字盤を描画します。そして次回のアラーム時刻をセットします。以降これを繰り返します。

ウィジェットをホーム画面から削除すると「③終了処理」が実行されます。アラームと描画サービスを停止し、ウィジェットは終了します。
構造は非常にシンプルです。
※実際にはウィジェットは複数同時に起動できます。この場合、「③終了処理」は、最後の1つのウィジェットが終了したときに実行されます。

2-2: どこから

終了したはずのアプリがどこから再び現れるのかは、すぐに見当がつきます。「キャッシュ」です。

Android OS は、アプリが終了したあとも、すぐにはそのアプリのプロセスをメモリーから解放しません。これは、そのアプリが再度起動されたとき、素早く処理を再開できるようにするための仕組みです。こうしてメモリーに残っているプロセスを「キャッシュしたプロセス」と呼びます。
※システム全体のメモリーが不足すると、OSは自動的に「キャッシュしたプロセス」を解放していきます。
Android 2.3 以降であれば、次の方法で「キャッシュしたプロセス」の一覧を確認できます。

  1. システム設定の「アプリケーション」-「実行中」一覧を表示する。
    ※「実行中のサービス」と表示されている場合もあります。
  2. 画面右上、または下部にある「キャッシュしたプロセスを表示」という文字をタップする。
    ※もしも見つからない場合は、メニューを開くとあります。

もちろん、私のウィジェットの名前も、終了直後に見るとこのキャッシュ一覧のなかにありました。どうやらここから復活したようです。

2-3: きっかけ

もう一度「2-1」の構造概要を見直してみると、再び動き出すまでの流れがなんとなく見えてきました。ウィジェットの終了後、アラームもサービスも停止したプロセスは、OSにキャッシュされます。この時点では「実行中」一覧から消えています。そして、そのキャッシュの「描画処理」が、何らかのきっかけで呼び出されるのが、復活のスイッチのようです。

「描画処理」が呼び出されると、描画サービスが再開され「実行中」一覧にウィジェットの名前が現れます。また、次回のアラームがセットされ、以降「描画処理」が繰り返し呼ばれ、無意味な描画処理によりCPUタイムとバッテリーを無駄に消費し続けてしまうということのようです。

復活の「きっかけ」はまだ不明ですが、終了したウィジェットが「実行中」一覧に再び現れるまでの過程がわかった気がします。
※「実行中」一覧は、具体的には「サービスが動いているアプリ」の一覧です。「キャッシュしたプロセス」ではサービスは停止しています。

PID;3 召喚

3-1: どこかに

その復活の「きっかけ」を見つけ出すため、もう一度、ソースファイルを良く見直しました。「描画処理」が呼び出されるルートがどこかにあるはずです。

しかし、なかなか見つかりません。

真っ先にアラームの動作を疑いましたが、ウィジェット終了直後には間違いなく停止しているので、これは違うようです。数日後に突然アラームが動き出すというのも考え難いです。

では、他に何が…?

3-2: 可能性

実はここまで、「まったく関係ないだろう」との判断のもと、完全に無視していた処理がありました。「描画処理」部分には、アラームの他にもう一つ、呼び出される条件があるのです。
描画処理(onReceive)
OSから「端末の時刻が変更された」という通知(ACTION_TIME_CHANGED)が来たときにも「描画処理」を実行するようにしてありました。こうしないと、セットされている次回のアラーム時刻と現在時刻が噛み合わなくなり、ウィジェットの描画がしばらく中断してしまうことがあるためです。

通常、端末の使用者が時刻の設定を変更することは滅多にありませんし、今回のテスト中も私は一切変更していません。なので、「無関係である」と最初から切り捨てて考えていました。

しかしもうここ以外に、「描画処理」が呼び出される可能性は残っていません。
「試しに時刻を変更してみよう」
そう思ってシステム設定の「日付と時刻」を開いたとき、思わず「あっ」と声をあげてしまいました。

3-3: 不定期

そこには「自動」という設定があります。ネットワークの情報から自動的に日時を修正してくれる機能です。通常、この設定は「ON」になっています。
「自動修正が働いて、時刻が変更されたときに、OSから通知が来るのでは…!?」
これなら、不定期に現象が発生する理由も説明できます。とにかくまず、端末の時刻を変更してみました。

3-4: 通知

ウィジェットを終了した後、手動で時刻を変更すると、
「…現れた! 瞬時に!」
「『実行中』一覧に私のウィジェットの名前が!」
複数の端末で繰り返し何度も検証しましたが、間違いありません。端末の時刻を変更すると100%確実に「実行中」一覧にすぐに現れました。

更に確認のため、端末の時刻を数時間ずらした状態で放置してみましたが、予想通り、時刻が自動修正された端末では復活していました。

OSにより不定期に時刻が自動修正されると、その通知は「キャッシュしたプロセス」にある私のウィジェットにも届きます。「描画処理」が呼び出され、停止していた描画サービスが再開され、「実行中」一覧に現れたのです。

復活の仕組みが、これで判明しました!

あとは対策するだけです。
※時刻が自動修正されるタイミングは、Android バージョンや 端末によって異なるようです。

PID;4 悪夢

4-1: これなら

早速、ウィジェットのソースに対策処理を入れました。

「ウィジェットが終了した」ことを記録しておき、終了後に「描画処理」が呼び出されても無視するようにしたのです。

具体的には、メンバー変数として「終了フラグ」を用意しました。ウィジェットの起動直後は「OFF」になっており、終了したときに「終了処理」でこれを「ON」にします。
「描画処理」では、「終了フラグ」が ON なら何もせずに処理を抜けるようにしました。
時計ウィジェットの構造概要
これなら、終了後にもし「描画処理」が呼び出されることがあっても、描画サービスが再び実行されることはなく、アラームもセットされずに即停止するはずです。

4-2: これで

対策したウィジェットをテスト用端末にインストールし、一度起動してから終了させます。「キャッシュしたプロセス」にはウィジェットの名前が表示され、キャッシュされていることが確認できます。この状態で、手動で端末の時刻を変更してみました。
「復活…しない!」
10台以上の端末で何度も試しましたが、「実行中」一覧に私のウィジェットの名前が現れることはありませんでした。念のため数日間、毎日「実行中」一覧を確認しましたが、やはりもう現れません。

これで、「復活」を封じることに成功したのです!

4-3: まだ

それから一週間以上が過ぎていました。「復活」現象はもう対策できたので、安心して次のアプリ開発に着手していました。

そんなとき。

開発中のアプリの動作確認のため、「実行中」一覧を見たとき、信じられないものを目にしました。
そこには、私のウィジェットの名前があったのです。
使っていないのに…。
ホーム画面に、そのウィジェットの姿はないのに…。
あわてて他の端末も確認しました。すると、一部の端末でやはり「復活」していました。

悪夢のようです。

まだ続くのか……!?

「終了フラグ」は、「復活」現象の発生頻度を大幅に減らしただけで、まだ不完全だったのです…。

 

 後編へつづく...

タグ , , | 2020/06/16 更新 |