2014 年 11 月 16 日

アンドロイドアプリのThreadがリークするとき

アンドロイドアプリのThreadがリークするとき

スマホアプリは、ユーザーの操作反応をよくするために非同期の処理が多く必要です。アンドロイドでもメインスレッドで時間が掛かる処理をシリアルに行うことは避けなければいけません。

アンドロイドOSには、非同期処理を扱うクラスやメソッドが多く存在しますし、Javaのスレッドを扱うなら、Executorsフレームワーク(SE5以上)の利用を検討すべきでしょう。

どちらにしろ、気をつけたいのは非同期処理に関わるメモリリークです。今回はJavaのThreadクラスを使って、どのようにスレッドがリークするのか調べます。

短時間の非同期処理であれば、処理が終了した時点で、アクティビティのライフサイクルで終了できる非同期処理は、アクティビティのonDestoryで必ずスレッドを終了しておく必要があります。

この記事では、アンドロイドのスレッドのリークを発見するためにEclipseのMAT(Memory Analyzer Tool)プラグインを利用します。

MATプラグインのインストール方法(OptionalなChartsも共にインストールしています)は省きますが、スレッドのリークの発見方法とあわせて、簡単なMATの使い方を紹介します。

環境

  • Eclipse Kepler Service Release 2
  • Memory Analyzer 1.4.0 Release
  • AndroidOS 4.1.2

簡単なMATの使い方

アプリを実行したら、DDMSを開きます。

MATの使い方(1)

Devicesタブから該当のアプリのプロセスを選択し、一度Update Heapを実行しておきます(そうしないといけないわけではありません)。こうしておくと、右ペインのHeapタブからGCを実行することができます。

MATの使い方(2)

次に左ペインのDump HPROF Fileアイコンをクリックします。しばらくするとMATのウィザードが現れます。必要なければキャンセルしてもかまいません。

MATの使い方(3)
MATの使い方(4)

次に真ん中のペインからDominator Treeを選択して、任意のキーワードでオブジェクトを絞り込みます。
Dominator Treeは占有量の大きいオブジェクトを探すためにあります。Histgramからも同等のリストが表示できますので、見比べてみるとよいと思います。

MATの使い方(5)

アクティビティとスレッドのリーク

参考記事とほぼ同様なプログラムでスレッドがリークするところをMATのDominator Treeで見ていきます。このプログラムは、1秒間スリープを永遠に繰り返す単純なスレッドをアクティビティの生成とともに起動します。参考記事ではアンドロイドのSystemClockクラスでスリープしていますが、単にthrows,try-catch句を省略できるからだろうと思います。

このスレッドは終了することがないので、このスレッドが参照しているアクティビティも回収されることがありません。たとえば、画面の向きを変えてやると、アクティビティの再生成が発生しますが、向きを変えるごとに古いアクティビティが残っていきます。

public class MainActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    createLocalThread();
  }

  private void createLocalThread() {
    new Thread() {
      @Override
      public void run() {
        while (true) {
          SystemClock.sleep(1000);
        }
      }
    }.start();
  }
}

MATでこの状態がどのように表示されるか示します。

初回起動後

DDMSにてHPROFファイルをダンプして、Dominator Treeから”^com.example.memoryleak.*”で表示内容を絞りんだ状態です。一つのアクティビティと一つのスレッドが存在することがわかります。

アクティビティとスレッドのリーク(1)

スレッドがUnknown,Threadになっています。スレッドはGCルートのthreadsから参照できるから、Threadと表示されているのだと考えますが、なぜ、Unknownとしても表示されるのかわかっていません。private staticにしているためかと考えて、publicなクラスにしても同じ結果です。

GCルートから参照できないということなら、回収対象になるかと思うのですが、GCルートのどこかから参照があるが、単にHPROFファイルからの情報にはルート情報がないため、unknownとなっているのかもしれません。

参考記事では、スレッドに関して以下のように述べられています。

Threads in Java are GC roots; 
that is, the Dalvik Virtual Machine (DVM) keeps hard references to all
active threads in the runtime system, and as a result, threads that
are left running will never be eligible for garbage collection.

またElipseのドキュメントのGCルートに関する説明は以下のとおりです。

Elicpse Documentationから引用

Thread
A started, but not stopped, thread.

Unknown
An object of unknown root type. Some dumps, such as IBM Portable
Heap Dump files, do not have root information. 
For these dumps the MAT parser marks objects which are have no
inbound references or are unreachable from any other root as roots
of this type. This ensures that MAT retains all the objects in the dump.

オリエンテーションローテーションを4回繰り返す

このようにそれぞれ5つのオブジェクトが残っています。もちろん、アクティビティのonDestroyは4回発生しています。

アクティビティとスレッドのリーク(2)

しかし、このときGCを発生させるといくつかのアクティビティが回収されます。また、すばやくオリエンテーションローテーションを繰り返した場合も、いくつかのアクティビティが回収されているように見えます。

正直、この現象が理解できていませんが、アクティビティがリークするのは間違いなさそうです。

スレッドのリーク

少しプログラムを修正します。Threadを継承したスタティッククラスを宣言し、今度はスタティッククラスのスレッドを起動するように変更します。

public class MainActivity extends Activity {

  private StaticThread mThread;
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    createStaticThread();
  }

  private void createStaticThread() {
    mThread = new StaticThread();
    mThread.start();
  }
  
  private static class StaticThread extends Thread {
    
    private boolean mRunning = true;
    
    @Override
    public void run() {
      while (mRunning) {
        SystemClock.sleep(1000);
      }
    }
    
  }
  
  @Override
  public void onDestroy() {
    super.onDestroy();
  }
}

初回起動後

HPROFファイルをダンプした状態です。一つのアクティビティと一つのスレッドが存在することがわかります。

スレッドのリーク(1)

オリエンテーションローテーションを4回繰り返す

今度は、アクティビティは一つのみでスレッドが5つ残っています。

スレッドのリーク(2)

これは、スレッドをスタティッククラスにすることで、スレッドオブジェクトが親のアクティビティオブジェクトを参照しなくなり、アクティビティが回収されるようになったためです。

最初の例では、スレッドオブジェクトがアクティビティオブジェクトを参照しているため、スレッドが生き続ける間、アクティビティも回収されません。

アクティビティ、スレッドともリークさせない

スレッドも正しく終了することで、必要ないスレッドオブジェクトも回収されるところを示します。

スレッドプログラムを終了するメソッドを追加します。スレッドには終了というメソッドはなく、単にrunメソッドを終了すればいいだけなので、単純にループを抜ける仕組みを加えます。アクティビティのonDestoryで、スレッドを終了させます。

public class MainActivity extends Activity {

  private StaticThread mThread;
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    createStaticThread();
  }

  private void createStaticThread() {
    mThread = new StaticThread();
    mThread.start();
  }
  
  private static class StaticThread extends Thread {
    
    private boolean mRunning = true;
    
    @Override
    public void run() {
      while (mRunning) {
        SystemClock.sleep(1000);
      }
    }
    
    void quit() {
      mRunning = false;
    }

  }
  
  @Override
  public void onDestroy() {
    super.onDestroy();
    mThread.quit(); // スレッドを終了させる
  }
}

初回起動後

同様にHPROFファイルをダンプした状態です。一つのアクティビティと一つのスレッドが存在することがわかります。

アクティビティ、スレッドともリークさせない(1)

オリエンテーションローテーションを4回繰り返す

アクティビティはひとつだけですが、スレッドは5つ残っています。しかし、よく見ると、そのうち4つはunknownの後のthreadが消えています。

アクティビティ、スレッドともリークさせない(2)

これは、スレッドが停止することでGCルートのThreadsからは参照されていないこと示していると理解していますが、しばらくunknownとして残ります。他のアプリを操作している間に全てきれいになくなります。

簡単にスレッドとアクティビティのリークを調べてみました。メモリリークを探す手がかりになれば幸いです。

実際のアプリケーションはもっと複雑で、こんな単純な方法でリークを見つけることは難しいかもしれません。

スレッドは最適な時点で終了しないと、リークしてしまうということを理解してコーディングすれば、不注意なリークが減少するのではと考えています。

また、本当にここはスレッドで実装すべきかという視点を持つことも重要ではないでしょうか。

参考記事

Activitys, Threads, & Memory Leaks