Androidアプリ開発では、例えば 1秒(1000ミリ秒)後に処理を実行したい場合、次のような方法が紹介されます。
Handler(Looper.getMainLooper()).postDelayed({
Log.d("hogehuga", "1秒後に実行されました")
}, 1000)
このコードがどのように動作しているのかは、Javaでの記述を見るとより理解しやすいと思います。
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
Log.d("hogehuga", "1秒後に実行されました");
}
}, 1000);
他の言語の経験がある人にとっては、1秒待機するだけでなぜこれだけ複雑な記述が必要なのか? Handler とは?Looper.getMainLooper()
とは?という気持ちになるかもしれません。これは、Android のスレッドやメッセージループの仕組みを踏まえて考えると、より腑に落ちると思います。
そこで今回は、Androidの内部でどのような流れになっているのかを解説していきます。Androidを支える技術〈I〉 をかなり参考させてもらっているため、一部古い箇所があるかもしれません。
(近年では Handler を直接使うよりも、Kotlin Coroutines を用いて待機処理を書く方法が推奨されており、より安全とされています。)
UIスレッドとは?
Android において、例えば「TextView
のテキストを変更する」「Toastを表示する」といったGUI関連の処理を行う時は、UIスレッド(メインスレッドとも呼ばれる)で実行する必要があります。
ここで、スレッドはプログラム内の処理を実行する1つの場所のようなもので、UIスレッドは基本的に同時に1つの処理しか実行できません。そのため、以下のようなコードを書くと、1秒間待機するどころか1秒間アプリが何も操作できない状態(= フリーズ)してしまいます。
runBlocking {
println("A")
delay(1000)
println("B")
}
そのため、時間のかかる処理を実行する時は、別のスレッドに分けた上で実行することが重要になります。
例1)「APIサーバーとの通信を待ちたい」「重い処理を実行したい」
- I/Oは
Dispatchers.IO
、CPUの重い処理はDispatchers.Default
のスレッドで実行する
例2)「1秒待機してから何かしらの処理を実行したい」
- Handlerを使用して待機する ←今回はこっちを深掘りします
- または、Kotlin Coroutinesを使用して
lifecycleScope.launch { ... }
の中でノンブロッキングにdelay
を呼び出す
メッセージループ
Android のUIスレッドは、Looper
と呼ばれる仕組みを使ってメッセージループを回しています。
メッセージループとは、キューに溜まった処理(Runnable
)をひたすら順番に取り出して実行していく仕組みのことです。while
文のようなループをイメージしてもらえれば OK です。
Android ではLooper
がこのループを回していて、アプリが動作している間、UIスレッド上で延々に繰り返されています。
そこに Handler を使って「○○ミリ秒(ms)
後にこの処理を実行して」と命令すると、その処理がMessageQueue
にタイムスタンプ付きで登録され、順番が来たら実行される という流れになります。
Handler
まず最初に、Handler のpostDelayed
を呼ぶときには、Runnable
オブジェクトを引数に渡します。Kotlinの場合は、ここをラムダ式で書くことができます。
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
Log.d("hogehuga", "1秒後に実行されました");
}
}, 1000);
その後、Handler はRunnable
オブジェクトをMessage
に詰め込み、「何ミリ秒後に実行するか」を示すタイムスタンプを付与します。
今回の例では、1000ミリ秒に実行すべきメッセージとして、このRunnable
を保持したメッセージがMessageQueue
に登録されます。
Looper
Looper
はAndroidにおけるメッセージループを提供するクラスです。
MessageQueue
はその名の通りキュー(Queue)の構造を持ち、メッセージを順番に管理する役割を担っています。
MessageQueue
のnext()
を呼び出すと、今回の例ではMessage1
が先に積まれていても、発火するまでの時間の短いMessage2
が優先的に取り出されます。
Looper自身はスレッドを生成せず、呼ばれたスレッドでメッセージループを回します。
AndroidのUIスレッドにおけるLooper
は、AOSP (Android Open Source Project) のコードを見るとActivityThread
の中で以下のように呼ばれています。1
public static void main(String[] args) {
// ~略~
Looper.prepareMainLooper();
// ~略~
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
// ~略~
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
ここで登場する主なメソッドはLooper.prepareMainLooper()
とLooper.loop()
の2つです。
Looper.prepareMainLooper()
を呼び出した後、同じスレッド内で Handler のインスタンスを生成すると、その Handler はメインのLooper
として紐づけられ、メッセージキューを利用できるようになります。
実際に、thread.getHandler()
を見ると、mH
の中に Handler のインスタンスが生成されて、格納されているように見えます。
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
final H mH = new H();
// ~略~
public Handler getHandler() {
return mH;
}
Looper.loop()
では終了メッセージが届くまで無限ループを続けます。
MessageQueue
にメッセージが投入されると、loop()
を呼び出したスレッドと同じスレッド内で処理が実行されます。
おわりに
今回の記事では、UIスレッドとメッセージループの仕組みについて、その中心となる Handler にフォーカスして解説しました。Handler や Android 内部のより詳しい仕組みについては、Androidを支える技術〈I〉に分かりやすく解説されています。興味を持った方はぜひ手に取って読んでみてください!
もし間違っているところがありましたら、コメントにて教えていただけると大変助かります。
おまけ: 現代の開発では Handler よりも Kotlin Coroutines で書くべき理由
ここまで解説してきた Handler には、1つ大きな問題があります。それは「Android のライフサイクルを認識できない」という点です。
例えば、次のように「5秒後にTextView
のテキストを"Done"
に変更する」コードを考えてみます。
val handler = Handler(Looper.getMainLooper())
val button = view.findViewById<Button>(R.id.button)
button.setOnClickListener {
handler.postDelayed({
// View破棄後に呼ばれると requireView() がクラッシュ
val textView = requireView().findViewById<TextView>(R.id.message)
textView.text = "Done"
}, 5000)
}
このとき、5秒の待機中にユーザーの操作によって、バックグラウンドに入った、画面が回転した ...等で Activity が非表示・破棄されても、Runnable
は依然としてキューに残り続けます。
その結果、IllegalStateException
が起きてしまったり、Activity がガーベジコレクションで解放されるまで参照され続けて、メモリリークの原因となる可能性があります。
そのため、今までは 自力でスレッドの管理をする といった回避策が必要でした。
Kotlin Coroutines はこの問題の解決策となります。特定のスレッドに縛られることなく処理を記述でき、UIスレッドをブロックしません。また、lifecycleScope
を利用することで、Activity のライフサイクルに応じて Coroutine を自動キャンセルできるため、画面回転や Back 操作後に古い処理が残ってしまうリスクを防げます。
val button = view.findViewById<Button>(R.id.button)
val textView = view.findViewById<TextView>(R.id.message)
button.setOnClickListener {
lifecycleScope.launch {
delay(5000)
textView.text = "Done"
}
}
バックグラウンドに入った場合も含めて処理を制御したい場合は、repeatOnLifecycle
を併用することで解決できます。
以下のコードでは、アプリが表に出ている(フォアグラウンド)時だけ、5秒待機が動作します。onStop
が呼ばれてバックグラウンドに移動した時点で自動的にキャンセルされ、再びonStart
に入ってフォアグラウンドに戻ると再起動されます。
button.setOnClickListener {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
delay(5000)
textView.text = "Done"
}
}
}
参考文献
-
if (false) { ... }
ってなんだ...... ↩