Androidのスレッドを解説! (どこよりも丁寧に)

Androidのスレッド構造についてまとめる。

 

 私たちが開発したアプリをAndroid(というOS)が起動するとき、Androidはスレッドを1つ新たに生成し、アプリの上位のClassのインスタンスを生成し呼び出す。

 普通のJavaアプリの場合、1つのJavaVMの上で1つのアプリが起動し、アプリが終了するとJavaVMも終了する。ところがAndroidの場合、1つのJavaVMの上で複数のアプリを起動する。そこだけ見るとWindowsLinuxっぽい。Androidからしてみれば、アプリを起動するためにごく普通の「スレッド」を起動したに過ぎないが、起動される私たちのアプリ側からしてみれば、これを「メインスレッド」と呼ぶ。複数のアプリが起動するため、「メインスレッド」と呼ばれるスレッドも複数あることになる。ただ、現状のAndroidは、複数のアプリを真に同時に起動できているかというと、搭載メモリ量、バッテリー容量、CPU性能などの制約からできていない。特にバッテリー容量の制約が大きいと思われる。次のアプリを起動したときには、それまで実行したアプリを一時停止させている。

 メインスレッドは、またの名をUIスレッドとも言い、Button、TextView、RadioButtonなどのUI(User Interface)部品の操作を許された唯一のスレッドである。

 メインスレッドの管理下でアプリの上位のClassのインスタンスの実行が始まる。上位クラスは、アプリを動作させるための基本的な環境準備が整うと、CallBackルーチンの中の onCreate()を呼び出してくれる。私たちはonCreate()の中で自身のアプリの初期化処理を書くことができる。

 また、別のアプリが起動されたので、私たちが作成したアプリを一旦停止させる必要が生じる場合もあるだろう。この場合上位Classは、停止に先立ちonPause()を呼び出してくれる。onPause()の中で私たちは、リソースの解放などの処理を書くことができる。

 このように、上位Classは、状況に応じて私たちが作成したアプリのCallBackルーチンを呼び出してくれる。アプリは、CallBackルーチンの集合体だ。

f:id:ittokoton:20190223073918p:plain


 アプリは、必要に応じてRunnableを実装したクラスのインスタンスを作成しstart()メソッドを呼び出すことで、メインスレッド以外のスレッド(ここではサブスレッドと呼ぶことにする)を作ることができる。例えば、あるホームページの内容をダウンロードする処理を書く際に、ダウンロードには時間がかかることが予想されたため、サブスレッドを作成してこの処理を並列に実行させ、その間メインスレッドは別の作業を行うとする。

 サブスレッドは、ダウンロードし終えたデータをTextViewなどのUI部品にセットしようとする。しかしセットした途端、Exceptionが飛び失敗してしまう。Androidでは、サブスレッドがUI操作を行うことは許されていないのだ。これを解決するには、サブスレッドは「何らかの方法」でメインスレッドにUIを操作したい旨を伝える必要がある。

 その「何らかの方法」が、メッセージキュー(Message Queue)だ。Windowsのプログラム構造をご存知の方であれば、Windowsメッセージの仕組みと類似しているといえば伝わるだろうか。メインスレッドは、Message Queueを持ち、Looperという機能がこのQueueに入ってくるメッセージを無限ループしながら監視している。メインスレッドの仕事の大半はこのQueue監視だ。CallBackを呼び出し、CallBackからリターンしてくると、すぐまたQueue監視の仕事に戻る。いつどこからMessageが飛んできてQueueに入るかもわからないので休んではいられない。メインスレッドは忙しいのだ。わかってあげて欲しい。彼は、ミリ秒、マイクロ秒の世界で生きている。CallBackの中で、ホームページからデータをダウンロードするような何秒もかかる処理を書くのは、もってのほかである。メインスレッドが怒り出すであろう。なので、時間のかかる処理はサブスレッドにやらせる必要があるのだ。

 では、ダウンロードが終わったとき、サブスレッドはどうすれば良いのか。そう、メッセージにダウンロードデータを添えて、メインスレッドが持つMessage Queueに投げるのだ。メインスレッドは、サブスレッドからメッセージが届くのを今か今かと無限ループしながら待ってくれているのだから遠慮なく投げよう。

 メインスレッドのQueueにメッセージを入れるには、Handlerを使う。メインスレッドの管理下で、new Handler() としてHandlerを作成すると、メインスレッドのQueueに結びついたHandlerが生成される(これ、結構重要)。このHandlerをサブスレッドに渡す必要がある。サブスレッドは、渡されたHandlerを使ってメッセージを投げる。

ここまでを図にしてみよう。ひとつのメッセージを黒丸ひとつ(●)で表している。

 

f:id:ittokoton:20190222002013p:plain

 

Handler経由でMessage Queueに投げ入れられるものとしては、下記の2種類がある。好きな方を使って良い。

  • Message
    Handler.sendMessage(Message msg)を使用する。要求種別番号(Message.what)を指定できる。何番が何の要求なのかは開発者が決める。
    任意でオブジェクトを添付できる。(Message.obj)
  • Runnable
    Handler.post(Runnable)を使用する。Runnable・・・つまり依頼したい処理を、コードのまま渡す。なんと大胆な! なお、post()メソッドは、結局のところAndroid内部でsendMessage()に変わる。Message Queueに入れられるのはMessageだけだからね。

 

 LooperがMessage Queueに格納されたものをFIFO(先入れ先出し)方式で取り出す。このとき、取り出されたものが、Messageだった場合とRunnableだった場合で、その後の処理が変わってくる。

 取り出したものがMessageだった場合、そのメッセージをHandlerインスタンスhandleMessage(Message msg)メソッドに渡してくれる。よって、handleMessage()メソッドを実装した独自のHandlerクラス(class MyHandler extends Handler)を作成(new MyHandler)すればよい。そして、handleMessage(msg)メソッドに好きな処理を書けばよい。メッセージの要求内容は、msg.what で参照することができ、msg.what==1なら、こういう処理をする・・・とか、msg.what==2なら、こうするとか・・・を記述していく。

 一方、取り出したものがRunnableだった場合、メインスレッドはそのRunnableが持つrun()メソッドを「実行」する。最初、私は「メインスレッドのLooperが、QueueからRunnableを取り出し、start()を呼び出してスレッドを生成するのか?」

             new Thread(Runnnable r).start()

とも考えたが、それだと結局サブスレッドになってしまい、やはりUI操作時にExceptionになってしまうし・・・。で、結論は、Runnableのrun()メソッドを直にCallする! だった・・・。うっそーん。そんなのあり!?  runnable.start() じゃなく、runnable.run() !?でも、処理自体を引数として丸ごと渡せるJavaって、ある意味便利w。これならメインスレッド管理下で動くので、run()にUI操作処理が書いてあってもエラーにならないねw

 念のため、サンプル1のあちこちに下記のような感じでログ出力処理を入れて検証した。

Log.d("■","onCreate(): ThreadName=" +Thread.currentThread().getName());

その結果は期待どおりだった。post()したRunnableのrun()はメインスレッドで実行されていた。

onCreate() : ThreadName=main ・・・onCreate()に入ってすぐ
onClick() : ThreadName=main ・・・onClick()に入ってすぐ
onClickRun(): ThreadName=Thread-4・・・onClick()のあとサブスレッド                    を起動し、run()が動作した直後
onPostRun() : ThreadName=main ・・・post()したRunnableのrun()に                   入ってすぐ

 

 一応、Androidのソースも追ってみよう。

android.os.Handler.java

private static void handleCallback(Message message) {
message.callback.run();
}

  handlerCallback()からrun()が呼ばれている。やっぱり、run()を直接呼び出している。さらに遡ってみよう。

 

android.os.Handler.java

public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

  Messageにcallbackがあれば、run()を呼び出している。なければhandleMessage()を呼んでるね。handleMessage()をオーバーライドしていれば、それが呼ばれるね。

 

ところでMessage.callbackは、いつセットされるかというと、

Handlerにpostしたときだね。m.callbackにrunnable をセットしてる。

android.os.Handler.java

private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}

 

以下が、メインのループだ。

android.os.Looper.java

public static void loop() {
(途中省略)
  final Looper me = myLooper();
 (途中省略)
final
MessageQueue queue = me.mQueue;
  for (;;) {
Message msg = queue.next(); // might block
(途中省略)
msg.target.dispatchMessage(msg);

 やはり、Looperで無限ループしているときにQueueからMessageを 取り出してDispatchするんだね。

 

 

 

 

さて、スレッドの重要な点をまとめよう。

  • メインスレッド(UIスレッド)のみが、UI部品を操作できる
  • メインスレッドでは時間のかかる処理はせず、サブスレッドにやらせる
  • サブスレッドでUIを操作したくなったらメインスレッドに依頼する
  • 依頼方法は、Message、またはRunnableを投げる
  • 投げる際には、メインスレッドで作成したHandlerを使用する
  • このHandlerをサブスレッドに渡す必要がある

 

長くなってきたので 次のページ で、サンプルコードをいくつか記載する。

  

---------------------------------------------------

ITとことん の目次
┗■Androidの調査結果(目次)
 ┗■本ページ