[その2] Square: AndroidのMain Threadの仕組み

http://corner.squareup.com/2013/12/android-main-thread-2.html

1 comment | 0 points | by WazanovaNews 3年以上前


Jshiike 3年以上前 | ▲upvoteする | link

その1は、こちら

今回は、main threadとAndroidコンポーネントのライフサイクルとの関係を紹介してます。

1) Activities love orientation changes

まずは、activityのライフサイクルと、設定変更の裏にあるマジックについて紹介しましょう。

Why it matters

Square Registerで実際に起きたクラッシュを参考にこの記事を書いています。コードをシンプルにしたバージョンはこの通り。

public class MyActivity extends Activity {
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Handler handler = new Handler(Looper.getMainLooper());
    handler.post(new Runnable() {
      public void run() {
        doSomething();
      }
    });
  }
  void doSomething() {
    // Uses the activity instance
  }
}

設定の変更により、onDestroy()メソッドが呼び出されるactivityの後に、doSomething()が呼び出されることはありえる。その時点で、activityインスタンスは使うべきではない。

A refresher on orientation changes

デバイスのオリエンテーションの変更はいつでも起こりうる。Activity#setRequestedOrientation(int)を使ってactivityがつくられる間のオリエンテーションの変更をシミュレーションしてみる。画面縦向きでこのactivityが始まるときのログのアウトプットを予想できますか?

public class MyActivity extends Activity {
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d("Square", "onCreate()");
    if (savedInstanceState == null) {
      Log.d("Square", "Requesting orientation change");
      setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
    }
  }
  protected void onResume() {
    super.onResume();
    Log.d("Square", "onResume()");
  }
  protected void onPause() {
    super.onPause();
    Log.d("Square", "onPause()");
  }
  protected void onDestroy() {
    super.onDestroy();
    Log.d("Square", "onDestroy()");
  }
}

Androidのライフサイクルを理解していれば、おそらくこのようになると予想するはず。

onCreate()
Requesting orientation change
onResume()
onPause()
onDestroy()
onCreate()
onResume()

Androidのライフサイクルは通常通りで、activityがつくられ、resumeし、オリエンテーションの変更が考慮され、activityがpauseし、destroyされ、そして新しいactivityがつくられてresumeされる。

Orientation changes and the main thread

ここで重要なことを思いださなくてはいけない。オリエンテーションの変更すると、main threadのlooperのキューへメッセージをシンプルにpostすることで、activityが再度つくられる。looperのキューの内容を読み取ってみよう。

public class MainLooperSpy {
  private final Field messagesField;
  private final Field nextField;
  private final MessageQueue mainMessageQueue;
  public MainLooperSpy() {
    try {
      Field queueField = Looper.class.getDeclaredField("mQueue");
      queueField.setAccessible(true);
      messagesField = MessageQueue.class.getDeclaredField("mMessages");
      messagesField.setAccessible(true);
      nextField = Message.class.getDeclaredField("next");
      nextField.setAccessible(true);
      Looper mainLooper = Looper.getMainLooper();
      mainMessageQueue = (MessageQueue) queueField.get(mainLooper);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  public void dumpQueue() {
    try {
      Message nextMessage = (Message) messagesField.get(mainMessageQueue);
      Log.d("MainLooperSpy", "Begin dumping queue");
      dumpMessages(nextMessage);
      Log.d("MainLooperSpy", "End dumping queue");
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }
  public void dumpMessages(Message message) throws IllegalAccessException {
    if (message != null) {
      Log.d("MainLooperSpy", message.toString());
      Message next = (Message) nextField.get(message);
      dumpMessages(next);
    }
  }
}

見ての通り、メッセージキューは、単に次ぎのメッセージへのリンクのリストである。キューの内容をオリエンテーション変更の直後にログを採ってみる。

public class MyActivity extends Activity {
  private final MainLooperSpy mainLooperSpy = new MainLooperSpy();
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d("Square", "onCreate()");
    if (savedInstanceState == null) {
      Log.d("Square", "Requesting orientation change");
      setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
      mainLooperSpy.dumpQueue();
    }
  }
}

アウトプットはこのようになる。

onCreate()
Requesting orientation change
Begin dumping queue
{ what=118 when=-94ms obj={1.0 208mcc15mnc en_US ldltr sw360dp w598dp h335dp 320dpi nrml land finger -keyb/v/h -nav/h s.44?spn} }
{ what=126 when=-32ms obj=ActivityRecord{41fd2b48 token=android.os.BinderProxy@41fcce50 no component name} }
End dumping queue

ActivityThreadクラスを確認してみると、118と126のメッセージが何を意味するのかわかる。

public final class ActivityThread {
  private class H extends Handler {
    public static final int CONFIGURATION_CHANGED   = 118;
    public static final int RELAUNCH_ACTIVITY       = 126;
  }
}

オリエンテーションの変更のリクエストは、main threadのlooperのキューに、CONFIGURATION_CHANGEDとRELAUNCH_ACTIVITYメッセージを追加する。一歩下がって、何が起きているのか考えてみよう。

activityが最初にスタートしたときは、キューは空である。現在実行されているメッセージはLAUNCH_ACTIVITYで、それは、activityインスタンスをつくり、まずonCreate()、次にonResume()を呼び出す。そしてメインのlooperだけが、キューになる次のメッセージを処理する。メッセージが処理されたとき、それは、

  • 古いacitivityにあるonSaveInstanceState()、onPause()、onDestroyを呼び出し、
  • 新しいactivityインスタンスをつくり、
  • 新しいactivityインスタンスにあるonCreate()とonResume()を呼び出す。

全ては一つのメッセージの処理である。その間にpostしたメッセージは全て、onResume()が呼び出された後に処理される。

Tying it all together

オリエンテーションを変更しているときにonCreate()にあるhandlerにpostしたらどうなるか?オリエンテーション変更の直前と直後のパターンを見てみよう。

public class MyActivity extends Activity {
  private final MainLooperSpy mainLooperSpy = new MainLooperSpy();
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d("Square", "onCreate()");
    if (savedInstanceState == null) {
      Handler handler = new Handler(Looper.getMainLooper());
      handler.post(new Runnable() {
        public void run() {
          Log.d("Square", "Posted before requesting orientation change");
        }
      });
      Log.d("Square", "Requesting orientation change");
      setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
      handler.post(new Runnable() {
        public void run() {
          Log.d("Square", "Posted after requesting orientation change");
        }
      });
      mainLooperSpy.dumpQueue();
    }
  }
  protected void onResume() {
    super.onResume();
    Log.d("Square", "onResume()");
  }
  protected void onPause() {
    super.onPause();
    Log.d("Square", "onPause()");
  }
  protected void onDestroy() {
    super.onDestroy();
    Log.d("Square", "onDestroy()");
  }
}

アウトプットはこのようになる。

onCreate()
Requesting orientation change
Begin dumping queue
{ what=0 when=-129ms }
{ what=118 when=-96ms obj={1.0 208mcc15mnc en_US ldltr sw360dp w598dp h335dp 320dpi nrml land finger -keyb/v/h -nav/h s.46?spn} }
{ what=126 when=-69ms obj=ActivityRecord{41fd6b68 token=android.os.BinderProxy@41fd0ae0 no component name} }
{ what=0 when=-6ms }
End dumping queue
onResume()
Posted before requesting orientation change
onPause()
onDestroy()
onCreate()
onResume()
Posted after requesting orientation change

まとめてみる。onCreate()の終わりに、キューは4つのメッセージを保持している。最初は、オリエンテーション変更前のpost、次にオリエンテーション変更に関連した二つのメッセージ、そしてオリエンテーションの変更後のメッセージ。ログによると、それらのメッセージは順番に実行されている。

つまり、オリエンテーションの変更前にpostされたメッセージは全て、古いactivityのonPause()前に処理され、オリエンテーションの変更後にpostされたメッセージは全て、新しいactivityのonResume()の後に処理をされる。

実践的な意味付けは、メッセージをpostして、メッセージが処理される際(onCreate()もしくはonResume()からpostしたとしても)に、postしたときのactivityインスタンスがまだ実行されているとは保証されないということである。もしメッセージがviewもしくはactivityを参照していれば、そのactivityはメッセージが処理されるまではガベージコレクタで処理されないということである。

What could you do?

  • 本当に修正する: main threadにいるのであれば、handler.post()を呼び出すのを止めること。ほとんどの場合、handler.post()は順番の問題を解決するのに使われる。ランダムなhandler.post()でひどい状態にするのでなく、アーキテクチャーを修正するとよい。
  • postをする正当な理由があれば: バックグランドでのオペレーションのときのように、メッセージがactivityへの参照を保持しないようにすること。
  • activityの参照をどうしてもしなくていけなければ: onPause()のactivityにあるhandler.removeCallBacks()とメッセージのキューを削除する。
  • クビになりたければ: handler.postAtFrontOfQueue()を使って、onPause()の前にpostしたメッセージが、onPause()の前に処理されるようにすること。コードがものすごく読みづらく、わかりづらくなるので、よしなさい。
  • 直接Activity.runOnUiThread()を呼び出さずに、handlerをつくって、handler.post()を使ったのに気づいてましたか?理由は下記のコードの通り。handler.post()と違って、runOnUiThread()は、現在のスレッドがmain threadであればrunnableをpostしない。その替わり、同時にrun()を呼び出す。
public class Activity {
  public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
      mHandler.post(action);
    } else {
      action.run();
    }
  }
}

2) Services

よくある誤解を撲滅したい。serviceはbackground threadでは実行されない。

onCreate()やonStartCommand()などのserviceのライフサイクルメソッドは、main thread(あなたのactivityのファンキーなアニメーションに使われるのと同じスレッド)で実行される。serviceにいようが、activityにいようが、長いタスクは、専用のbackground threadで実行されなければいけない。このbackground threadは、activityがとうに終わっていても、アプリのプロセスが続いている限りは、生きている。しかし、Androidのシステムはいつでもアプリのプロセスをkillすることができる。serviceはシステムに、生き続けられるように、そしてプロセスをkillする前にお知らせしてくれるように頼む仕組みである。

参考: onBind()から返ってきたIBinderが別のプロセスから呼び出しを受けると、そのメソッドはbackground threadで実行される。

時間を見つけてService documentationを是非読んでみてください。

IntentServices

IntentServicesは、background threadのintentのキューを順番に処理するシンプルな仕組みです。

public class MyService extends IntentService {
  public MyService() {
    super("MyService");
  }
  protected void onHandleIntent(Intent intent) {
    // This is called on a background thread.
  }
}

内部的には、HandlerThreadのintentを処理するのにLooperを使ってます。serviceがdestroyされると、looperは現在のintentの処理を止めさせ、background threadを中断します。

3) Conclusion

ほとんどのAndroidのライフサイクルメソッドはmain threadで呼び出されます。そのコールバックをlooperのキューに送られるシンプルなメッセージだと考えてください。


[2013] ワザノバTop100アクセスランキング


#android #モバイル #モバイルアプリ #コーディング

Back