2012年8月7日火曜日

handleleakがうざったい。

静的解析ツールであるandroid lintは、大量の警告がぐわーっと出てきて大変うざい(特にバージョンアップで仕様変わった後とか…)んですけど、コーディングのミスなどを指摘してくれて、ものによってはクイックフィックスで自動的に解決してくれたり、Explain Issueを選ぶとなぜこの警告を出したのか、どうすれば解決できるのか教えてくれます。…英語ですけど。

iOSが静的解析でメモリリークを撲滅して、ガベージコレクタなしでのメモリ自動管理(なぜか古参開発者は自分で管理するのを好んで使わないARC)を実現しているので、androidおっくれってるー!感はなきにしもあらずですけど、まあ便利です。

---

androidにLintが本格的に組み込まれるようになってから見るようになったこの警告。

This Handler class should be static or leaks might occur

「ハンドラクラスはstaticにせよ、さもないとリークが起きるであろう」…Explain Issueを読むと、「ハンドラはstaticなinnerクラスで、かつouterクラスを弱参照で持つように修正しろ」とあります。

具体的なコードはStackOverflowに載っています。

This Handler class should be static or leaks might occur: IncomingHandler

このLint警告は気にし過ぎなくてもいい気がするんですけどねえ。

以下なんで出るかの解説です。

---

内部クラスには四種類あります。非static内部クラスと、static内部クラス、そして匿名内部クラスです。

これ意外と頭に入らないんですよね。匿名内部クラスはリスナー書いてれば自然と覚えられるとして、デザインパターンが頭に入っていけば、static内部クラスは外部クラスのインスタンスがなくても作れる(=外部クラスのインスタンスのビルダーとして使われる)、非staticは外部クラスのメンバへのアクセス手段を持つ(=アダプタやイテレータとして使われる)として自然に覚えられると思います。

ローカルクラス?覚えなくていいです。

---

非staticな内部クラスは、Effective Javaでは非推奨だったり、一部のコーディングスタイルだと使うな!とか書かれていたりします。(もちろんIteratorなど正当な使用方法の場合は別でしょうけど)

その理由としてJavaの仮想マシンの仕様があります。Java言語仕様上内部クラスと呼ばれているものは、仮想マシン上ではただのクラスとして認識しているのです。

staticを宣言した内部クラスは、外部クラスのインスタンスがなくとも独立して生成できます。なので位置づけとしては特殊なスコープの、普通のクラスです。

一方、非staticの内部クラスは必ず外部クラスのインスタンス(=エンクロージングインスタンス)と紐付けられます。エンクロージングインスタンスへの暗黙の参照(OuterClass.thisってやつです)を持つばかりか、エンクロージングインスタンスのprivateフィールドへも自由にアクセスできます。

匿名クラスはfinalなローカル変数にアクセスできること、名前がないためコンストラクタで生成することができない(例えば空のコンストラクタが絶対必要なフラグメントを匿名クラスで生成すると、再生成できないから画面を傾けるとアプリが死ぬ)という2点を除いて、非static内部クラスと同じです。

――仮想マシンからはただのクラスとしか認識されていならば、外部クラスと内部クラスは別のクラスなのに、どうやってprivateフィールドにアクセスしているのか。

その答えは単純で、コンパイラが外部クラスのprivateフィールドへアクセスするメソッドを、非static内部クラスのために自動的に生成しているのです。

暗黙の参照の存在を理解していないと、リソースリークに繋がりかねませんし、バイトコードに変換されたときに不本意にカプセル化が破られている問題があったり、まあいろいろ嫌われているわけです。

---

Handlerはandroidのキモの一つですね。LayoutInflatorとHandlerが説明できるようになれば中級者、なイメージです。自分的には。

Handlerに関する説明は世の中いくらでも分かりやすいのが転がっているんで、知らなかったら探して読んで欲しいんですけど、アプリケーションのメインループにタスクを渡すためのオブジェクトです。メインループのタスクキューに詰まれたHandlerは、タスクが実行するまで保持され続けます。

----

以上より次の事実が導けます。

  • 非static内部クラス・匿名内部クラスはエンクロージングインスタンスの暗黙の参照を持つ
  • Looperに渡されたHandlerはタスクが消化されるまで保持され続ける

さて、1時間後にタスクが起動するようなHandlerを生成した場合どうなるでしょうか?

そのころにはとっくにアプリを終了させていると思うのですが、Looperに詰まれたHandlerはそんなことを知りませんから、エンクロージングインスタンスへの暗黙の強参照を持ち続け、ガベージコレクタの回収を妨害してしまうのです。

Handlerを内部クラスとして定義したいときは、別スレッドからメインスレッド(UIスレッド)を操作したいというケースなので、エンクロージングインスタンスはメインスレッドを持つアクティビティなのが一般的です。

下手なHandlerの使い方をすると、アクティビティがメモリリークし続けるのです。

----

Android Lintが提案する解決法が理解できたと思います。

staticインナークラスにすることで、外部クラス(おそらくはアクティビティ)への暗黙の参照を持たないようにしつつ、弱参照で保持するように改修する。

そうすれば、エンクロージングインスタンスが不要になった(アクティビティが終了した)時点で、きちんとガベージコレクタの回収対象としてくれるので、Handlerによるリークは起こりえなくなるのです。

しかし、例えば画面の再描画タスクのような、エンクロージングインスタンスと内部クラスとの生存時間がほぼ等しくなるような状況で、いちいち気にして余計なコードを書く必要性はないと思うのですけど…。

0 件のコメント:

コメントを投稿