➊ はじめに
Androidで、スレッド間通信を実現するには、どうすれば良いのか調べました。
➋ やること
スレッド間通信の仕組みをお勉強するために、以下のような簡単なアプリを作ることとしました。
(1) アプリイメージ
なんやこれ?スレッド間通信言うてるから来たのに、クリックに合わせて、textView1と2に数字出しとるだけやん!ワシは帰るぞ😠
お待ち下さい。見えませんが、裏でスレッド間通信をやっております。
(2) シーケンス
- 画面(textView1)をタッチすると、UIスレッドでランダム値(1~100)を生成します。
- 生成したランダム値を、UIスレッドからSUBスレッドへ送信します。
- SUBスレッドでランダム値を受け取り、計算処理(ランダム値に100を足すだけ)を行います。
- SUBスレッドで受け取ったランダム値と計算処理した結果を、SUBスレッドからUIスレッドへ送信します。
- UIスレッドで、受け取ったランダム値をtextView1に、計算結果をtextView2に表示します。
➌ お勉強
Androidでのスレッド間通信の方法はいくつかあるみたいですが、今回は、LooperとHandlerを使用した方法で実施したいと思います。Looper、Handlerの役割、スレッド間通信のイメージとしては、以下のような感じです。
##(1) Threadとは
Threadとは、プログラム処理の実行単位です。プログラム処理の実行単位は、プログラマの必要に応じて増減できます。
■ わざわざThreadを追加する理由
わざわざThreadを追加する理由のひとつとしては、Androidは、シングルスレッドモデルにより、アプリのUIの応答性のためにも、UIスレッドをブロックしないことが不可欠です。即座に実行する必要のない操作の場合は、別のスレッドで実行するようにする必要があります。例えば、画像処理に約10秒かかるとして、これをUIスレッドで実施してしまうと、ユーザからは約10秒間応答が無いように感じられてしまいます。時間がかかる処理がある場合、この様なことを回避するために、別スレッドを作る必要があります。
■ UIスレッドでなければ、UIの更新はできない
注意としては、基本的に、UIスレッド以外のスレッドからUIを更新することはできない仕様となっているので、UIスレッド以外での処理結果によりUIを更新したい場合は、UIスレッドへメッセージ通知などを行い、それをトリガとしてUIスレッド上からUIを更新する必要があります。
(2) Looperとは
Looperとは、メッセージQueueのことです。メッセージQueueとは先入先出法(FIFO:First In First Out)で管理するメッセージバッファのことです。
※ちなみに、UIスレッドは、Looperを既に持っています(Handlerは持っていません)ので、新規に作る必要がありませんが、スレッドを新規に生成した場合は、Looperは生成されないので、新たに生成する必要があります。
(3) Handlerとは
Handlerとは、LooperへQueuingしたり、LooperからQueuingされたメッセージを取り出す役割を持っています。メッセージ送信や、LooperへのQueuing、LooperからのQueue取り出しを簡単にしてくれる便利なモノと考えると良いと思います。Looperとbindして(結びつけて)使用します。
(4) サンプルプログラム
snippet1が、SUBスレッドのクラス定義、snippet2が、SUBスレッドを生成するUIスレッド側のサンプルプログラムです。SUBスレッド開始処理(runメソッド)の中で、LooperとHandlerの生成及びバインド、Looper監視開始を行っています。handleMessageをoverrideしておくと、Looperがメッセージ受信した際に、ここが呼び出されるようになります。
class SubThread: Thread() { // SUBスレッドClass定義
companion object {
const val MSG_NUMBER_1 = 1 // MessageNumber1
}
var mHandler: Handler? = null // mHandlerはメッセージ送信のためにPublic設定
override fun run() { // subThread.start()で実行される
Looper.prepare() // Looper生成
mHandler = object : Handler(Looper.myLooper()!!) { // Handler生成(LooperとBind)
override fun handleMessage(msg: Message) {
// ここにメッセージ受信処理を追加します
// ...
}
}
Looper.loop() // Looper監視開始
}
}
val subThread = SubThread() // SUBスレッド生成
subThread.start() // SUBスレッド開始
subThread.mHandler?.sendMessage(SubThreadHandler.MSG_NUMBER_1) // SUBスレッドへメッセージ送信
➍ 実装
さて、いよいよ実装です。以下、2点の仕組みを理解しておけば、なんとなくできちゃいます。(なんとなくかいっ!😅)
- メッセージ受信側にLooperとHandlerを生成してbindする(UIスレッドはLooper生成済み)。
- 送信先Handlerのインスタンスが分かれば、どこからでもメッセージ送信できる。
(1) activity_main.kt
package com.poodlemaster.app11
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
import android.widget.TextView
import kotlin.random.Random
import kotlin.random.nextUInt
// ------------------------------------------------------------------------------------------------
class MainActivity : AppCompatActivity() {
companion object {
const val LOGCAT_TAG = "sample.MainActivity"
}
lateinit var textView1 : TextView
lateinit var textView2 : TextView
private lateinit var mainHandler : Handler
private lateinit var subThreadHandler : Handler
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
SysInfo.logThread(LOGCAT_TAG)
textView1 = findViewById(R.id.textView1)
textView2 = findViewById(R.id.textView2)
// MainLooperからのメッセージハンドラをバインド
mainHandler = MainHandler(this)
// Thread作成&開始
val subThread = SubThread(mainHandler)
subThread.start()
// textView1クリックイベント待ち
textView1.setOnClickListener {
Log.d(LOGCAT_TAG, "textView1 Click")
val rnd : Int = (Random.nextUInt(100u)+1u).toInt() // rnd = 1~100
Log.d("$LOGCAT_TAG/onCreate/Random(rnd)", rnd.toString())
subThreadHandler = subThread.getHandler()!!
sendMessage(subThreadHandler, rnd)
}
}
// ハンドラへメッセージ送信
private fun sendMessage(distHandler : Handler, sendData : Int) {
val msg = distHandler.obtainMessage(SubThreadHandler.MSG_SUB_THREAD_HANDLER1)
msg.obj = MessageData(sendData, 0)
Log.d("$LOGCAT_TAG/sendMessage", distHandler.toString() + "|" + msg.obj.toString())
distHandler.sendMessage(msg)
}
}
// ------------------------------------------------------------------------------------------------
class MainHandler(private val context : Context) : Handler(Looper.getMainLooper()) {
companion object {
const val LOGCAT_TAG = "sample.MainHandler"
const val MSG_MAIN_HANDLER1 = 1
}
override fun handleMessage(msg: Message) {
var text1 = "textView1 : "
var text2 = "textView2 : "
SysInfo.logThread("$LOGCAT_TAG/handleMessage")
when (msg.what) {
MSG_MAIN_HANDLER1 -> {
Log.d("$LOGCAT_TAG/handleMessage", "msgId=${msg.what} Receive, obj=${msg.obj}")
text1 += (msg.obj as MessageData).intData.toString()
text2 += (msg.obj as MessageData).editIntData.toString()
}
}
(context as MainActivity?)!!.textView1.text = text1
(context as MainActivity?)!!.textView2.text = text2
}
}
// ------------------------------------------------------------------------------------------------
class SubThread(private val mainHandler: Handler) : Thread() {
companion object {
const val LOGCAT_TAG = "sample.SubThread"
}
private var subHandler : SubThreadHandler? = null
override fun run() {
SysInfo.logThread("$LOGCAT_TAG/run")
try{
Looper.prepare() // Looper生成
subHandler = SubThreadHandler(mainHandler) // 実行中ThreadのLooperにHandlerを設定
Looper.loop() // MessageQueue監視
}
catch (e : InterruptedException) {
currentThread().interrupt()
Log.d("$LOGCAT_TAG/run", "InterruptedException")
}
}
fun getHandler() : Handler? {
return(subHandler) // 送信先ハンドラを教えるために使用
}
}
// ------------------------------------------------------------------------------------------------
class SubThreadHandler(private val mainHandler : Handler) : Handler(Looper.myLooper()!!) {
companion object {
const val LOGCAT_TAG = "sample.SubThreadHandler"
const val MSG_SUB_THREAD_HANDLER1 = 1
}
override fun handleMessage(msg: Message) {
SysInfo.logThread("$LOGCAT_TAG/handleMessage")
when (msg.what) {
MSG_SUB_THREAD_HANDLER1 -> {
Log.d("$LOGCAT_TAG/handleMessage", "msgId=${msg.what} Receive, obj=${msg.obj}")
val intData : Int = (msg.obj as MessageData).intData
val editIntData : Int = (msg.obj as MessageData).intData + 100
sendMessage(intData, editIntData)
}
}
}
private fun sendMessage(IntData : Int, EditIntData : Int) {
val msg = mainHandler.obtainMessage(MainHandler.MSG_MAIN_HANDLER1)
msg.obj = MessageData(IntData, EditIntData)
Log.d("$LOGCAT_TAG/sendMessage", mainHandler.toString() + "|" + msg.obj.toString())
mainHandler.sendMessage(msg)
}
}
// ------------------------------------------------------------------------------------------------
data class MessageData(val intData : Int, val editIntData : Int)
// ------------------------------------------------------------------------------------------------
class SysInfo {
companion object Utility {
fun logThread(tag : String) : Long {
val threadName = Thread.currentThread().name
val threadId = Thread.currentThread().id
Log.d(tag, "ThreadName(ThreadId) = %s(%d)".format(threadName, threadId))
return(threadId)
}
}
}
(2) activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView1"
android:layout_width="371dp"
android:layout_height="49dp"
android:text="textView1 : Click Me!!"
android:textSize="34sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.4" />
<TextView
android:id="@+id/textView2"
android:layout_width="371dp"
android:layout_height="49dp"
android:text="textView2 : Hello World!!"
android:textSize="34sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="20dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="20dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
➎ 概略図
➏ 実行ログ
分かりやすく加工した実行ログを載せておきます。
MainActivity: ThreadName(ThreadId) = main(2) # UIスレッド(スレッド名:main、スレッドID:2)
SubThread/run: ThreadName(ThreadId) = Thread-2(271) # SUBスレッド(スレッド名:Thread-2、スレッドID:271)
MainActivity: textView1 Click # UIスレッドでtextView1のクリックを検知
MainActivity/onCreate/Random(rnd): 93 # UIスレッドで生成したランダム値
# UIスレッドからSUBスレッドへメッセージ送信
MainActivity/sendMessage: Handler (com.poodlemaster.app11.SubThreadHandler) {ad160fd}|MessageData(intData=93, editIntData=0)
SubThreadHandler/handleMessage: ThreadName(ThreadId) = Thread-2(271) # SUBスレッドでメッセージ受信
SubThreadHandler/handleMessage: msgId=1 Receive, obj=MessageData(intData=93, editIntData=0)
# SUBスレッドからUIスレッドへメッセージ送信
SubThreadHandler/sendMessage: Handler (com.poodlemaster.app11.MainHandler) {76191f2}|MessageData(intData=93, editIntData=193)
MainHandler/handleMessage: ThreadName(ThreadId) = main(2) # UIスレッドでメッセージ受信
MainHandler/handleMessage: msgId=1 Receive, obj=MessageData(intData=93, editIntData=193)
➐ 以上
とりあえず、スレッド間通信を通して、簡単なマルチスレッドの勉強ができました。
「Android Studio」でkotlin言語を使って簡単なアプリを作れたら楽しいだろうなと思って少し使い始めました。まずは、機能の基礎勉強中です。同士の役に立てれば本望です😉