Avoiding memory leaks (超訳)

ナナメ読んだらなんか引っかかったので、えいやっと30分ほどで訳してみた。誤訳ご免。というかツッコミ歓迎。

Androidのアプリケーションは、少なくともT-MobileのG1ではヒープメモリは16MBに制限されている。電話としてはとても多くのメモリだが、同時に、やりたいことがある開発者にとってはあまりに少なすぎる。このメモリをすべて使い切るつもりがなくても、メモリの使用量はできるだけ抑えて、他のアプリケーションが強制終了されないようにしなければならない。Androidが、より多くのアプリケーションをメモリ上に持てれば、より素早くユーザがアプリケーションを切り替えられる。私の仕事上、Androiidアプリケーションのメモリリーク問題に遭遇することがあり、多くの場合、同じ過ちが原因となっている。その原因とは、Contextへの参照を長期間に渡って保持しつづけることだ。


Androidでは、Contextは多くの操作で使用されるが、大抵はリソースのロードとアクセスに使われる。なぜなら、すべてのウィジェットはコンストラクタでContextパラメータを受けとるからだ。通常のAndroidアプリケーションでは、ActivityApplicationという2種類のContextを保持するだろう。Contextを必要とするクラスやメソッドに、開発者が渡す必要があるのは、多くの場合Activityだろう。

@Override
protected void onCreate(Bundle state) {
  super.onCreate(state);
  
  TextView label = new TextView(this);
  label.setText("Leaks are bad");
  
  setContentView(label);
}

これは、ビューがアクティビティ全体への参照を持つことを意味する。結果、アクティビティが保持しているものすべてへの参照を持つことになる。言い換えると、ビューの階層全体とそのリソースすべてということになる。したがって、このContextをリークしてしまうと、大量のメモリリークになってしまう (ここで言うリークとは、参照を保持してしまった結果、GCが回収するのを妨害することを指す)。注意していないと、アクティビティ全体をリークしてしまうことは本当に簡単に起きてしまう。


デフォルトの動作では、画面の縦横が変わると、システムは現在のバンドルを保ちつつ、現在のアクティビティを破棄し、新しく作り直す。このようにして、AndroidはリソースからアプリケーションのUIをリロードする。ちょっと想像してみてほしい。巨大なビットマップを持つアプリケーションを書くときに、画面が回転する度にそのビットマップをリロードしたくないとしたら、どうするだろう。ビットマップを保持したままにして、画面回転する度にリロードする必要のないもっとも簡単な方法は、スタティックフィールドに保持することだ。

private static Drawable sBackground;

@Override
protected void onCreate(Bundle state) {
  super.onCreate(state);
  
  TextView label = new TextView(this);
  label.setText("Leaks are bad");
  
  if (sBackground == null) {
    sBackground = getDrawable(R.drawable.large_bitmap);
  }
  label.setBackgroundDrawable(sBackground);
  
  setContentView(label);
}

このコードはとても速いが、大きく間違っている。最初の1回目に画面が回転したときに生成される最初のアクティビティをリークしてしまう。


Drawableがビューにアタッチされたときに、ビューはDrawableにコールバックとして設定される。上記のコードではDrawableはTextViewへの参照を保持し、TextView自身は
アクティビティ(Contextの1つ)への参照を保持し、ひいては、ほとんどすべてへの参照を持つことになる。(もちろんコードにもよるけれど)


この例はContextをリークさせてしまうもっとも単純な例で、対処方法は、Home Screenソースコードをご覧頂くと、アクティビティが破棄されたときに、保存されているDrawableのコールバックをnullに設定することであるのがわかる。(メソッドunbindDrawables()を探して見てください)


たいへん興味深いことは、Contextリークの連鎖を作ってしまうケースがあり、そしてそれはとても悪いことだ。すぐにメモリ不足になってしまうだろう。


Context関連でメモリリークを避ける簡単な方法は2つある。自明な方は、Contextがそれ自身のスコープから外れないようにすることだ。上記の例では静的参照を挙げたが、クラスの
内部への参照や、外部クラスへの明示な参照も同様に危険だ。もう1つの解決策は、Application Contextを使用することだ。アプリケーションが生きている限り、このContextは生き続け、アクティビティのライフサイクルに依存しない。Contextを必要とするオブジェクトを長期間保持するなら、アプリケーションオブジェクトのことを思い出して欲しい。Context.getApplicationContext()あるいはActivity.getApplication()を呼び出すだけで簡単に得ることが出来る。


まとめると、Context関連のメモリリークを避けるには次のことを心に止めておくこと。

  • アクティビティContextへの参照を長期間保持しないこと(アクティビティへの参照は、アクティビティ自身と同じライフサイクルでなければならない)
  • アクティビティContextの代わりにアプリケーションContextを使用してみる
  • アクティビティの中では、静的でない内部クラスを避ける。ただし、自分でライフサイクルを制御する場合を除く。静的な内部クラスを使用し、その中でアクティビティへの弱い参照を作成する。この問題に対する解決策は、例えばViewRootとその内部クラスで行っているように、外部クラスへのWeakReferenceを持つ静的な内部クラスを使用すること。