LoginSignup
7
8

More than 1 year has passed since last update.

【Android】スレッド間通信 in Kotlin

Posted at

➊ はじめに

Androidで、スレッド間通信を実現するには、どうすれば良いのか調べました。

➋ やること

スレッド間通信の仕組みをお勉強するために、以下のような簡単なアプリを作ることとしました。

(1) アプリイメージ

なんやこれ?スレッド間通信言うてるから来たのに、クリックに合わせて、textView1と2に数字出しとるだけやん!ワシは帰るぞ😠

お待ち下さい。見えませんが、でスレッド間通信をやっております。

(2) シーケンス

  1. 画面(textView1)をタッチすると、UIスレッドでランダム値(1~100)を生成します。
  2. 生成したランダム値を、UIスレッドからSUBスレッドへ送信します。
  3. SUBスレッドでランダム値を受け取り、計算処理(ランダム値に100を足すだけ)を行います。
  4. SUBスレッドで受け取ったランダム値と計算処理した結果を、SUBスレッドからUIスレッドへ送信します。
  5. UIスレッドで、受け取ったランダム値をtextView1に、計算結果をtextView2に表示します。

以下シーケンスになります。
image.png

➌ お勉強

Androidでのスレッド間通信の方法はいくつかあるみたいですが、今回は、LooperHandlerを使用した方法で実施したいと思います。LooperHandlerの役割、スレッド間通信のイメージとしては、以下のような感じです。
image.png

(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がメッセージ受信した際に、ここが呼び出されるようになります。

snippet1
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監視開始
    }
}
snippet2
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

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

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>

➎ 概略図

実装の概略図を載せておきます。イメージ伝わるかな?
image.png

➏ 実行ログ

分かりやすく加工した実行ログを載せておきます。

logcat(分かりやすく加工済み)
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言語を使って簡単なアプリを作れたら楽しいだろうなと思って少し使い始めました。まずは、機能の基礎勉強中です。同士の役に立てれば本望です😉

7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8