2014 年 11 月 22 日

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

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

前回は、スレッドがリークする様子をMAT(Memory Analyzer Tool)を使って説明しました。今回は、スレッドとともによく利用されるハンドラがどのようにリークするかを調べてみます。

ハンドラは異なるスレッドから受け取ったメッセージを処理するものです。たとえば、GUIスレッドとは別のスレッドから表示データを受け取って、そのデータを表示するといった使い方をします。

GUIスレッド以外でUIを操作することができないので、GUIスレッドとひもづいたハンドラを使って、別スレッドからUI操作を実行します。Handlerクラスと共に、活躍するのがLooperクラスとMessageクラスです。

簡単に説明すれば、以下のような動作になります。

  1. HandlerはMessageをLooperへキューイングします。
  2. LooperはキューからMessageを取り出して、HandlerのdispatchHandlerにMessageを渡して、handleMessageで処理させます。
  3. HandlerはMessageの中にタスク(Runnableを実装したもの)があれば、そのタスクを実行します。

では、このHandlerがどのようなときにリークするか調べてみます。

前回のスタティックスレッド(MyThreadクラス)のソースコードを利用して、別スレッドでカウントした数値を、GUIスレッド側でノーティフィケーションするプログラムに修正します。

ハンドラの実装

MainActivityのインナークラスとして、MainHandlerを定義します。
HandleMessageメソッドで、別スレッドから受け取ったMessageをノーティフィケーションします。

private class MainHandler extends Handler {
    
  MainHandler() {
    super();
  }
    
  @Override
  public void handleMessage(Message msg) {
      
    int notificationId = 1;  

    NotificationCompat.Builder builder = new NotificationCompat.Builder(MainActivity.this).setSmallIcon(R.drawable.ic_launcher).setContentTitle("My notification").setContentText((String)msg.obj);
    NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
      
    Intent resultIntent = new Intent(MainActivity.this, MainActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(MainActivity.this, 0, resultIntent, 0);
    builder.setContentIntent(pendingIntent);
    manager.notify(notificationId, builder.build());
  }     
}

MyThreadの修正

MyThreadクラスのコンストラクタにこのMainHandlerのインスタンスを渡します。

そして、times変数を30秒ごとにカウントアップし、MainHandlerへMessageとともに、その値を送信します。

public class MyThread extends Thread {

  private boolean mRunning = false;
  private Handler mHandler;
  
  MyThread(Handler handler) {
    super();
    mHandler = handler;
  }
  
  @Override
  public void run() {
    
    int times = 0;
    mRunning = true;
    
    while (mRunning) {
      
      Message message;
      
      SystemClock.sleep(30000);
      
      message = Message.obtain();
      message.obj = new String("wake at now. " + times);
      message.what = 0;
      mHandler.sendMessage(message);
      ++times;
    }
  }
  
  void quit() {
    mRunning = false;
  }
}

Eclipseを利用していると、MainActivityでハンドラをインナークラス(無名インナークラスも同様)で定義した時点で以下のような警告がでます。

"This Handler class should be static or leaks might occur (com.example.memoryleak.MainActivity.MainHandler)"

「MainHandlerクラスはスタティックで定義しないと、リークが発生するかもしれませんよ」という警告です。

この警告の意味はどういうことでしょうか。この警告は無視してもいい場合と、無視できない場合があります。

別スレッドと、GUIスレッドのライフサイクルがほとんど同じ時は、無視してもいいのではと考えています。ここでいうと、MainActivityのライフサイクルとMyThreadのライフサイクルがほとんど同じ場合です。

MainActivityのonCreateでMyThreadが生成されて、MainActivityのonDestroyでMyThreadが廃棄されるような場合は、ほとんど影響がないと考えます。

たとえば、このMyThreadが30秒間隔ではなく、数時間とか、数日単位で繰り返す場合や、スレッドを終了せずにいた場合を想定してみます。

MyHandlerを保持したMyThreadは、自身はスタティッククラスにも関わらず、HandlerがMyActivityを参照しているので、同様にMyActivityを参照していることになります。

そして、MyThreadが生存している間、ずっとMyHandlerも、MyActivityも回収されなくなります。ここでハンドラとアクティビティがリークすることになります。

前述の警告文は、このような事態を警告しています。

たとえハンドラがインナークラスであっても、MyThreadがすぐに終了するなら、参照は解放されるので問題はないはずです。この場合は無視できるのではと考えます。

しかし、このような警告を残しておくのは気持ちが悪いということなら、MyHandlerをスタティックに定義すればよいでしょう。

MyHandlerスタティッククラス版

  static private class MainHandler extends Handler {
    
    Context mContext;
    
    MainHandler(Context ctx) {
      super();
      mContext = ctx;
    }
    
    @Override
    public void handleMessage(Message msg) {
      
      int notificationId = 1;  

      NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext).setSmallIcon(R.drawable.ic_launcher).setContentTitle("My notification").setContentText((String)msg.obj);
      NotificationManager manager = (NotificationManager)mContext.getSystemService(Context.NOTIFICATION_SERVICE);
      
      Intent resultIntent = new Intent(mContext, MainActivity.class);
      PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, resultIntent, 0);
      builder.setContentIntent(pendingIntent);
      manager.notify(notificationId, builder.build());
    }     
  }

なぜ、ハンドラをインナークラスにするのかというと、この場合はMyActivityのインスタンスを参照したいからです。

このスタティック版では、コンストラクタにコンテキストを渡すように改修しました。そうしないと、ノーティフィケーション関連のメソッドが実行できないからです。

これで警告も消えました。しかし、これは新たなリーク問題が生じます。

ハンドラをスタティックにすることで、MyActivityのインスタンスへの暗黙な参照はなくなったのですが、今度は明示的にMyActivityインスタンスを参照してしまっています。これではリークは解決しません。

ウィーク参照を利用する

MyActivityが回収されるようにするには、参照を止めなければなりませんが、そうすると、ノーティフィケーションができなくなります。その場合は、MyActivityの強参照が解放されたことが検知できればよさそうです。

強参照が解放されたとき、もう参照先がないことを教えてくれるのが弱参照というものです。具体的には以下のように利用します。

MyHandlerウィークリファレンス版

static private class MainHandler extends Handler {
    
  private final WeakReference<Context> mContext;
    
  MainHandler(Context ctx) {
    super();
    mContext = new WeakReference<Context>(ctx);
  }
    
  @Override
  public void handleMessage(Message msg) {
      
    Context context = mContext.get();
     
    if (context == null) {
      // 強参照が解放された。
      return;
    }
    
    int notificationId = 1;  

    NotificationCompat.Builder builder = new NotificationCompat.Builder(context).setSmallIcon(R.drawable.ic_launcher).setContentTitle("My notification").setContentText((String)msg.obj);
    NotificationManager manager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
      
    Intent resultIntent = new Intent(context, MainActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, resultIntent, 0);
    builder.setContentIntent(pendingIntent);
    manager.notify(notificationId, builder.build());
  }     
}

WeakReferenceは強参照が解放された時点で、nullを返してくれます。その場合は、もうActivityが回収されているので、ノーティフィケーションすることはできません。

これでようやく安全でかつリークがないプログラムになりました。

Javaは一つのファイルに一クラスというシンプルな制約のせいもあり、インナークラスを多用する場合が多くなります。また、親インスタンスの参照を渡すのも面倒だしということもあるでしょう。

Warningだからと見逃さず、それは無視してよいのか吟味する必要があります。

MATを使わずとも、静的解析でアプリのリークや不安定さを解消できることも多くあります。また、スレッドに関わるリークや不安定さはデバッガやMATでは捕捉できないことも多いので、あらかじめ静的解析でそういう危険を取り去っておくことも重要だと思います。