Androidアプリの間違った作り方
とある外注さんが書いてきたコードがひどかった。
- バックグラウンドでのオーディオ再生を、Activityから作成したワーカースレッドで行う。キャンセル処理は全く考慮していない。
- UIスレッド上でばんばんHTTPリクエストを投げる。応答を読み終わるまでループを回す。
アプリケーションの起動中にずっと保持する必要のある情報をIntentに書く。それを読むのはonStart()ではなくonCreate()。Activityの初期化は全部onCreate()に書く。onStart()もonStop()も全く使ってない。- アプリケーションの起動中にずっと保持する必要のある情報をstaticメンバに持たせる。
どう悪いか説明しておこう。
Activityから起動されるワーカースレッドの寿命
たとえば画面上で表示する情報をHTTPリクエストで取得する等の処理はワーカスレッドを使うと応答性を失わずに欠くことができる。
onPause(),onStop()のイベントまでに適切に止められるなら、Activityはワーカスレッドを使ってもいい。
それより長い寿命でワーカスレッドを扱おうとしても、そのスレッドへの参照を保持しきれないので扱うことができない。なぜなら、activityのインスタンスの寿命はシステムが管理していて、画面が表示されなくなった時点でいつ破棄されてもおかしくないからだ。
UI部品を扱えるのはUIスレッドだけなので、ワーカスレッドからUI部品を扱うにはHandlerやLooperについて少しは知っておかないといけない。また、ワーカスレッド上で動作するバックグラウンド処理は、画面側の都合でcancelされることを想定しておかないといけない。
このあたりの事情を理解した上で、頻出する記述を軽減するために android.os.AsyncTask を使うとラクができる。ただし、onPause()やonStop()の際にAsyncTask.cancel()でスレッドを止める必要があるのはワーカスレッドを自分で管理した場合と同様だ。
画面の寿命と同期しないバックグラウンド処理を扱うにはServiceを使うことになる。たとえば通知領域にアイコンを出してオーディオ再生を行う等がそれにあたるだろう。
作成したインスタンスは複数回表示されることがあるが、onCreate()が呼ばれるのは最初の一回だけ
例えばブラウザのようなActivityを作成する場合、そのActivityは異なるURIを含む複数のIntentを使って、何度か表示されることになるだろう。しかし onCreate()が呼ばれるのは、そのうち最初の1回だけになるかもしれない。
The Developer's Guide では onCreate()は「Activity の静的な初期化に使う」と書かれている。UIの初期化、UI部品へのイベントハンドラの登録、画面上で使用するリソースの取得などがそうだ。それ以外のもの、たとえばIntentに含まれるURIを使って云々といった処理はonCreate()ではなくonStart()を使うべきだろう。
onCreate()の際にIntentの詳細を読むのは多くの場合は間違いだ。
launchModeが"standard"のActivityは、処理するIntentは1つだけであるらしい。
クラスのstaticメンバはプロセスの終了で破棄されるが、その後もアプリケーションは継続する。
クラスのstaticメンバ変数にデータを置いただけで「これでアプリケーション起動中はずっとこのデータは保持されるに違いない」と信じるのは間違いで、他のアプリがリソースを要求した際に失われる危険がある。
Androidではアプリケーションの寿命とそのプロセスの寿命は異なる。
起動されたアプリケーションは activity stack を持つ task として表現される。(このstackには複数種類のパッケージのactivityが並ぶこともある。)
activity stack上のactivityは常時保持されるわけではなくて、システムがリソースを必要とした時にプロセス単位で終了が可能かどうかチェックされ、可能なら終了処理が行われる。stack上にはActivityの再現に必要なIntentとsavedInstanceStateだけが残る。
savedInstanceStateはActivity単位の情報を次のインスタンスに持ち越すためのもので、ユーザは onSaveInstanceState()をオーバライドすることでこれを更新できる。
で、アプリケーションの起動中にずっと保持したい情報はどう扱えばいいの?
複数の画面で構成されたアプリケーションで、アプリケーション全体で扱う情報を保持する場所はWindowsやLinuxではプロセス上のメモリで良かった。複雑なオブジェクトをそのまま保持できた。
androidの場合、生のオブジェクトをそのまま保持できるような方法は存在しない。
プロセスを殺せないようしてリソースを浪費させるか、ストレージを使って永続化するか、Sticky Broadcast を使うしかない。どれも暗号化などの観点からは弱いので、対策が必要なら自前で暗号化を行う事になる。(そしてDRM同様、この手の暗号化は常に負ける運命にある。)
永続化
SharedPreferences や SQLiteOpenHelper などの ストレージを使う。詳しくは http://developer.android.com/guide/topics/data/data-storage.html で。
Sticky Broadcast
Serviceを起動して、sendStickyBroadcast() や removeStickyBroadcast() を使う。受信側は Context.registerReceiver() を receiver引数をnullにして使えば、貼られたIntentをいつでも参照できる。
Root Activity と savedInstanceState
Root Activityはタスクに常に残るので、savedInstanceStateで情報を適切に保存すればプロセスが再起動してもデータを保持できる。後はSticky broadcast等で状態を通知すれば他のActivityから参照できる。
まとめ
より正確な記述が必要な人は、AndroidのThe Developer's Guideの最初のトピックにApplication Fundamentalsがあって、Activities and Tasks や Component Lifecycles 等のトピックが説明されているから、そちらを見てほしい。
色々書いたけど、これ全部は公式の開発ガイドの最初のトピック1ページ読めば分かることなんですよ。それすらもしないで常駐系のアプリを書いた人の火消しで週末が消えたりとか、もうね。