이번 포스팅에는 안드로이드의 핸들러와 루퍼에 대해서 알아보도록 하겠습니다.

안드로이드 시스템은 기본적으로 하나의 Main Thread(=UI Thread)만을 가지고 있고 보통 이 스레드 안에서 작업을 하는데, UI 관련 작업은 반드시 Main Thread(=UI Thread)에서 처리를 해야하고 시간이 오래 걸릴 수 있는 네트워크 작업이나 데이터베이스 작업들은 Main Thread(=UI Thread)가 아닌 별도의 스레드에서만 작업을 해야합니다.

이러다가 보니 다운로드업로드와 같이 네트워크 작업별도의 스레드에서 하고 해당 스레드에서 작업한 결과를 UI에 보여줄때는 Main Thread(=UI Thread)에 표시를 해야하는 경우가 자주 발생합니다.

이렇게 되면 자연스럽게 멀티 스레드 환경에서 작업을 해야하는데, 이 때 서로다른 스레드간 통신을 하기 위해 고려해야할 부분이 많아집니다. 이때 안드로이드에서 스레드간 통신을 도와주는 도구가 바로 핸들러와 루퍼(=Handler & Looper)입니다.


핸들러를 통한 스레드간 통신

핸들러를 통한 스레드간 통신은 다음과 같습니다.

  1. 핸들러에 있는 sendMessage()를 통해서 Message(=작업)를 전달합니다.
  2. 그럼 핸들러는 Message Queue에 차례대로 넣습니다.
  3. 그럼 LooperMessage Queue로부터 하나씩 Message를 뽑아서 핸들러로 전달합니다.
  4. Looper로부터 전달받은 메시지는 handleMessage()를 통해서 작업을 하게 됩니다.

핸들러(Handler)

안드로이드에서 사용할 수 있는 스레드 통신 방법은 여러 가지가 있는데, 대표적인 방법 중 하나가 Hadler를 통해 Message를 전달하는 방법입니다.

Handler를 생성하면 호출한 스레드의 Message QueueLooper에 자동으로 연결됩니다.

메서드 리턴타입 설명
obtainMessage() Message 핸들러 자신으로 지정된 메시지 객체 리턴
sendMessage(msg: Message) boolean 메시지 큐에 message를 만들어서 전달을 합니다.
post(runnable: Runnable) void 메시지 큐에 runnable 전달
sendEmptyMessage(what: Int) boolean 메시지 큐에 메시지를 만들어서 전달을 합니다.
removeMessages(what: Int) void 전달한 what 코드 메시지를 메시지 큐에서 삭제합니다.
handleMessage(msg: Message) void 루퍼를 통해 메시지 큐에서 꺼낸 MessageRunnable 처리
handler = object : Handler() {
    override fun handleMessage(msg: Message) {
        super.handleMessage(msg)

        if (msg.what === START_CODE) {
            thread.start()
        } else if (msg.what === PROGRESS_CODE) {
            textView.text = "Count : ${msg.arg1}"
        }
    }
}

메시지(Message)

스레드 통신에서 핸들러에 데이터를 보내기 위한 클래스로 데이터를 Message에 담아서 핸들러로 보내면 해당 객체는 핸들러를 통해 Message Queue에 쌓이게 됩니다.

변수 설명
what 메시지 종류를 식별하기 위한 메시지 코드
arg1 메시지를 통해 전달되는 정수 값1
arg2 메시지를 통해 전달되는 정수 값2
obj 메시지를 통해 전달되는 객체
val message = Message()
message.what = PROGRESS_CODE
message.arg1 = count

handler.sendMessage(message)

메시지 큐(Message Queue)

Message Queue는 이름 그대로 Message 객체Queue 형태로 관리하는 자료 구조를 말합니다. Queue 구조는 기본적으로 들어온 순서대로 차례대로 처리하게 됩니다.

호출 순서

sendMessage() -> handleMessage()

루퍼(Looper)

루퍼는 스레드당 하나씩 밖에 가질 수 없고, 루퍼는 Message Queue가 비어 있는 동안 아무 행동도 하지 않고, 메시지가 들어오면 해당 메시지를 꺼내 적절한 Handler로 전달합니다. 기본적으로 새로 생성한 스레드는 루퍼를 가지지 않고 Looper.prepare() 메서드를 호출해야지 Looper가 생성이 됩니다.

루퍼는 무한히 실행되는 메시지 루프를 통해 큐에 메시지가 들어오는지 감시하며 들어온 메시지를 처리할 핸들러를 찾아서 handleMessage()를 호출합니다.

메서드 리턴타입 설명
prepare() void 루퍼를 생성
loop() void 무한히 루프를 돌며 Message Queue에 쌓인 Message나 Runnable객체를 핸들러에 전달
quit() void 루프 종료

예제 (전체소스)

[실행화면]

package com.byjw.handlerthread

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView

class MainActivity : AppCompatActivity() {

    private val countTextView by lazy {
        findViewById<TextView>(R.id.txt)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val myHandler = MyHandler(countTextView)
        val workerThread = WorkerThread(myHandler)
        workerThread.start()


    }
}

[MainActivity.kt]

package com.byjw.handlerthread

import android.os.Handler
import android.os.Message
import android.widget.TextView
import com.byjw.handlerthread.Contract.Companion.COUNT_DATA_KEY
import com.byjw.handlerthread.Contract.Companion.MSG_CODE

class MyHandler(private val countTextView: TextView) : Handler() {
    override fun handleMessage(msg: Message) {
        when(msg.what) {
            MSG_CODE -> countTextView.text = msg.data.getInt(COUNT_DATA_KEY).toString()
        }
    }
}

[MyHandler.kt]

package com.byjw.handlerthread

interface Contract {

    companion object {
        const val MSG_CODE = 100
        const val COUNT_DATA_KEY = "count"
    }
    
}

[Contract.kt]

package com.byjw.handlerthread

import android.os.Bundle
import android.os.Handler
import android.os.Message
import com.byjw.handlerthread.Contract.Companion.COUNT_DATA_KEY
import com.byjw.handlerthread.Contract.Companion.MSG_CODE

class WorkerThread(private val handler: Handler) : Thread() {

    override fun run() {
        for (i in 0..100) {
            val bundle = Bundle()
            bundle.putInt(COUNT_DATA_KEY, i)

            val message = Message()
            message.what = MSG_CODE
            message.data = bundle

            handler.sendMessage(message)
            sleep(100)
        }
    }
}

[WorkerThread.kt]

<?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/txt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

[activity_main.xml]


핸들러 스레드(Handler Thread)

메인 Thread는 기본적으로 Looper가 생성되어 있지만, 일반적으로 새로 생성된 Thread는 Looper를 가지고 있지 않아 메시지를 받을 수 없습니다. 이때 메시지를 받기 위해서는 Looper.prepare()메서드와 Looper.loop()에서드를 통해 Looper를 생성하고 메시지를 받을 수 있습니다. 이를 쉽게 이용하기 위한 HandlerThreadThread를 상속하고 자동으로 Looper.prepare()Looper.loop()를 실행하여 개별 Looper를 가지는 내부에서 무한루프를 도는 백그라운드 스레드입니다.

백그라운드에서 어떤 작업을 처리해야하는데, 순차적으로 처리를 해야한다면 이용할 수 있는 클래스입니다.

간단하게는 아래와 같이 HandlerThread 객체를 만들어서 start() 하고 만들어진 HandlerThreadLooper를 이용하여 Handler로 사용할 수 있습니다. quit()함수를 호출하기 전까지는 계속 루프를 돌고 quit()함수가 호출이 되면 Looper가 종료가 됩니다. 액티비티 LifeCycle()에 따라서 Destroy()때 호출하는 식으로 처리할 수 있습니다.

// 별도의 스레드에 Looper와 MessageQueue가 자동으로 생성됨
val handlerThread = HandlerThread("Jungwoon")
handlerThread.start()

// 위에서 만든 HandlerThread의 Looper를 가져와서 Handler를 만듬
Handler(handlerThread.looper).post {
    // 새로 만들어진 스레드에서 해야할 작업 명시
}

아니면 직접 HandlerThread를 상속받아 처리할 수 있습니다.

package com.byjw.handlerthread

import android.os.Handler
import android.os.HandlerThread
import android.os.Message
import android.util.Log

class MyHandlerThread : HandlerThread("MyHandlerThread") {
    // HandlerThread안에서 사용할 Handler 호출
    private lateinit var handler: Handler
    private val ACTION_1 = 1
    private val ACTION_2 = 2
    private val ACTION_3 = 3

    // Looper가 생성이 될때 호출되는 메소드
    override fun onLooperPrepared() {
        super.onLooperPrepared()

        handler = object : Handler(looper) {
            override fun handleMessage(msg: Message) {
                super.handleMessage(msg)

                when (msg.what) {
                    ACTION_1 -> doAction1()
                    ACTION_2 -> doAction2()
                    ACTION_3 -> doAction3()
                }

            }
        }
    }

    fun action1() {
        val message = getMessage(ACTION_1)
        handler.sendMessage(message)
    }

    fun action2() {
        val message = getMessage(ACTION_2)
        handler.sendMessage(message)
    }

    fun action3() {
        val message = getMessage(ACTION_3)
        handler.sendMessage(message)
    }
    
    private fun getMessage(what: Int): Message {
        val message = Message()
        message.what = what
        return message
    }

    private fun doAction1() {
        // todo something
    }

    private fun doAction2() {
        // todo something
    }

    private fun doAction3() {
        // todo something
    }
}