2018年2月1日木曜日

Android Studioでメモリリークを解決しちゃおう

Androidにおけるメモリリーク

Javaはメモリリークしない。オブジェクトが相互に参照しあっていても Mark and Swipe によってメモリからクリアされるからだ。
よって、Java、そしてAndroidにおけるメモリリークとは、ライフサイクルの不一致のことを指す。

一般的に、リークするとまずいのは画像などであるが、Androidの場合はActivity(or Fragment)という「画面」も該当する。なぜなら、たいていの画面はたくさんの画像を保持しているからだ。
Androidでは気をつけないとActivityがリークする。今回はActivityのリークを調べる方法をご説明しよう。

Android Studioを使ってActivityのメモリリークを調べる

Android Studio の Run > Profile… をクリックすると、apkがdeviceにインストールされ、Android Profiler が立ち上がる。

Android Profiler に3つのグラフが表示される。これらはリアルタイムで更新される。MEMORY のグラフをクリックして、メモリ使用量のグラフのみを表示する。

Activity間の遷移を行ったり、Activityでの操作を行ったりしたあと、Backキーによってアプリを閉じる。

Android Profiler の左上にあるゴミ箱アイコンを何回か時間を置いてクリックする。この操作によって、参照がないオブジェクトはGCによってメモリから消える。メモリ使用量のグラフががくっと下がるのがわかるはずだ。ゴミ箱を押してもグラフに変動がなくなったら、次のステップに進もう。

ゴミ箱の隣の Dump Java Heap をクリックし、処理が終わるまでしばらく待とう。グラフの下にオブジェクトが列挙される。これらが、メモリに残っているものだ。

Activityは全て閉じた状態でGCを走らせた。そのため、Activityがメモリに残っている場合はリークしている。Activityをアプリのパッケージ名配下 com.example.ui に置いているならば、Arrange by class となっているプルダウンメニューを箇所を Arrange by package に変更しよう。ここで、com > example > ui とたどっていって、Activityクラスの名前があればリークしている。みつけたらクリックしよう。

Android Studio を使ってActivityのメモリリークを解決する

みつけたActivityクラスをクリックすると、 Instance View が開く。ここには、そのクラスのメモリ上にあるインスタンスが列挙される。3つあれば、そのActivityが3つリークしているということだ。そのうちの1つをクリックしよう。

References が開く。ここには、そのインスタンスへの参照が列挙される。何かがActivityへの参照を保持しているために、GCによってもメモリから削除されずにリークしている。解決するためには、この参照を切る必要がある。
しかし、Activityへの参照はたくさんある。なぜなら、Activity内で表示するViewは、ActivityをmContext のようなメンバ変数で保持しているためだ。これは大抵の場合は問題ではない。なぜならActivity-View間の相互参照であり、JavaのMark and Swipe方式ではメモリからクリアされるためだ。問題となるのは、ROOTにつながる参照である。
ROOTにつながる参照を探すには、ReferencesDepth カラムに着目する。Depthが小さくなるほどROOTに近い。そのためDepthの昇順でソートして(デフォルト)、上から順に展開していき、Depth 0 までの参照をたどる。

Depth 0 までの参照がわかれば、その参照を切ればよい。なぜそのような参照が残ってしまうのかは、一般的なバグ解決の手段である(がんばれ)。これでメモリリークは解決だ。

注意

Activityが全て閉じられても、プロセスが死ぬとは限らない。もっとも、Profilerで見れている以上はそのプロセスは生きている。プロセスが生きていれば、staticな変数などはメモリ上に残っていて正常だ。

WeakReference のインスタンスからの参照は、メモリリークにつながらない。原因は別の参照経路にある。なぜなら WeakReference は他からの参照がある場合のみ参照を保持し続けるという「弱参照」をするためのクラスであるからだ。

this$0 から参照されている場合、実際のコードを探してもそのような定義文は存在しない。これは、staticでないインナークラスが、アウタークラスへの参照を暗黙のうちに保持しているためだ。Android開発におけるあるあるメモリリークパターンである。