Looper, Handler

GDSC Android·2024년 5월 8일
post-thumbnail

본격적으로 looper와 handler에 대해 알아보기전에, 이해를 돕기 위해 안드로이드의 스레드에 대해서 먼저 알아보고 넘어가자.

Thread

안드로이드는 UI 작업을 담당하는 메인 스레드 (UI 스레드)로 단일 스레드 모델을 원칙으로 한다. 공식문서에 따르면, 안드로이드의 단일 스레드 모델에서는 지켜야할 규칙이 두 가지가 있다.

  • 메인 스레드를 블로킹해서는 안된다
  • UI 관련 동작은 오로지 메인스레드에서만 접근해야 한다

애플리케이션이 시작되면 시스템은 기본 스레드라고 하는 애플리케이션 실행 스레드를 만듭니다. 이 스레드는 그리기 이벤트를 포함하여 적절한 사용자 인터페이스 위젯에 이벤트를 전달하는 역할을 하므로 매우 중요합니다. 또한 거의 항상 애플리케이션이 Android UI 도구 키트의 android.widget 및 android.view 패키지에 있는 구성요소와 상호작용하는 스레드입니다. 이러한 이유로 기본 스레드를 UI 스레드라고도 합니다. (Android Developers Guide)


근데 왜 단일 스레드여야할까 ?

한마디로 정리하면 UI 업데이트에 있어서 교착 상태, 경합 상태를 방지하고자이다. 가령 간단한 하나의 위젯, 뷰의 업데이트에 여러 스레드가 관련된다고 하면 같은 자원을 여러 스레드가 참조하기 때문에 해당 위젯이 그려지는 순서, 업데이트 되는 순서를 정확히 보장하기가 어렵다.

교착 상태 (deadlock) : 서로의 작업을 기다려주다가 모든 스레드가 무한 대기 상태에 빠지는 현상이다

경합 상태 (race condition) : 공유 자원에 동시 접근할 때 실행 순서에 따라서 그 결과가 달라질 수 있는 현상이다

그렇다면, 네트워크 요청이나 쿼리 작업같이 긴 시간이 필요한 작업까지 메인 스레드에서 실행될까 ? 아니다. 보통 시간이 오래 걸리는 작업은 백그라운드 스레드 (worker thread)에서 실행된다.

그렇다면 결국 어쨌든 백그라운드 스레드에서 작업을 처리하고 이 결과를 UI에 업데이트해야하는 상황이라면, 스레드에서 스레드로 결과를 전달해야할 것이다. 즉, 스레드 간의 통신을 해야 하는데 이때 등장하고 활용되는 개념이 Handler와 Looper이다.

Looper

Class used to run a message loop for a thread

하나의 스레드는 하나의 looper를 가진다. 이 looper안에는 messageQueue가 존재하는데, 이 큐 안에 looper가 담당한 스레드가 처리해야할 동작들이 메시지 형태로 선입선출 방식으로 쌓이게 된다. looper는 이렇게 쌓인 메시지를 꺼내 handler가 처리할 수 있도록 전달한다.

앞서 말했듯이 looper는 각자의 한 스레드에 종속되고, 해당 스레드의 Thread Local Stroage(TLS)에 저장된다. 그렇다면 looper의 생성은 어떤식으로 이루어질까 ?


looper의 생성

메인 스레드의 looper는 안드로이드 환경에서 직접 생성해주고, 나머지 일반 looper들은 prepare() 를 통해 초기화 호출되고, loop()를 통해 동작되고 실행된다.

Looper의 내부 코드

loopOnce의 내부코드

위의 내부 코드에서 알 수 있듯이, looper는 messageQueue의 내용물이 null이 될때까지 pop하며 무한 반복되고, quit() 또는 quitSafely()가 호출될때 종료된다.

이때 quit()는 즉시 종료, quitSafely()는 남아있는 작업을 마친 후 종료된다는 차이가 있다.


Message

그럼 또 여기서 메시지가 뭔지 모르겠다. 메시지에 대해서 알아보자

메시지는 수행해야할 작업의 작은 단위이고, messageQueue는 이를 담을 자료구조

While the constructor of Message is public, the best way to get one of these is to call Message.obtain() or one of the Handler.obtainMessage() methods, which will pull them from a pool of recycled objects.

메시지는 사용한 메시지 객체를 다시 초기화하여 재사용하는 방식인 오브젝트 풀 방식을 사용한다. 따라서 이를 위해 obtain(), Handler.obtainMessage()를 통해 얻고 관리되어야 한다.

이러한 메시지들은 msg.recycleUnChecked()를 통해 looper에서 재사용할 수 있게 초기화된다.

메시지들을 가지고 있는 것이 MessageQueue이며 이때 객체를 직접 참조하여 메시지를 전달하거나, 메시지를 가져와서 처리하지는 않는다.

다시 한번 정리하면, 메시지 전달은 MessageQueue에 연결된 handler를 통해서이고, MessageQueue로부터 메시지를 꺼내고 처리하는 역할은 looper가 한다.


Handler

handler는 그 이름처럼 looper에 쌓인 메시지를 다루는 역할을 한다. 어떤 메시지를 looper의 messageQueue에 넣거나, looper가 messageQueue에서 메시지를 꺼내 전달하면 이를 처리한다.

즉, 메시지를 looper로 전달하거나 전달받는다.

A Handler allows you to send and process Message and Runnable objects associated with a thread's MessageQueue.

근데 여기서 handler가 보낼 수 있는 객체가 한 가지 더 등장한다. 바로 Runnable이다.

메시지는 public 클래스 변수들(int, object 등)을 통해 스레드 간에 주고 받을 내용을 전달해 코드가 실행되게한다. 이때 그냥 코드 자체를 객체화해서 보내버리면 특정 상황에서는 더 간단하게 활용될 수 있을 것이다. 이때 필요한 것이 Runnable이다.


Runnable


구현된 run()에 있는 명령어들, 즉 실행될 동작 그 자체를 객체로 저장하고 있는 형태가 Runnable이다.

The Runnable interface should be implemented by any class whose instances are intended to be executed by a thread. The class must define a method of no arguments called run.
In addition, Runnable provides the means for a class to be active while not subclassing Thread.

따라서 메시지와 runnable를 다른 스레드로 전달하는 과정은 다음과 같다

이렇게 스레드와 looper, handler를 알아보았다면 실제로 이를 이용해 어떻게 스레드끼리 데이터를 주고 받는지 직접 구현해보자. 다음은 간단하게 메인 스레드에서 작업자 스레드로 메시지를 보내고, 이를 확인하기 위해 로그를 찍은 모습이다

class MainActivity : AppCompatActivity() {

    private lateinit var workerThread: WorkerThread

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

        workerThread = WorkerThread("workerThread")
        workerThread.start()

        // 작업 스레드의 Handler가 준비될 때까지 기다린다
        workerThread.readyLatch.await()

        // 메인 스레드에서 작업 스레드로 메시지 보내기
        workerThread.handler?.post {
            val message = Message.obtain()
            message.obj = "Hello from Main Thread"
            workerThread.handler?.sendMessage(message)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        workerThread.quit()
    }

    inner class WorkerThread(name: String) : Thread(name) {
        var handler: Handler? = null
        val readyLatch = CountDownLatch(1)

        override fun run() {
            // 해당 스레드 looper 초기화
            Looper.prepare()

            handler = object : Handler(Looper.myLooper()!!) {
                override fun handleMessage(msg: Message) {
                    val messageData = msg.obj as String
                    Log.d("Worker", "Received in Worker Thread: $messageData")

                    // 메인 스레드의 main Looper에 msg 보내기
                    Handler(Looper.getMainLooper()).post {
                        Log.d("Worker", "Sending message to Main Thread")
                    }
                }
            }

            // Handler 준비완료
            readyLatch.countDown()

            // 해당 스레드의 messageQueue안의 message들을 실행한다
            Looper.loop()
        }

        fun quit() {
            handler?.looper?.quitSafely()
        }
    }
}

실행하면 위와 같이 전달받은 메시지가 로그로 잘 찍힌다

이렇게 안드로이드에서 handler와 looper를 활용하면 스레드 간에 runnable, message를 주고 받으며 작업자 스레드에서도 handler를 통해 UI의 변경에 간접적으로 관여할 수 있다.


handler, looper의 실사용

이러한 handler, looper는 대표적으로 다음과 같이 쓰인다.

  • 반복 UI 갱신

  • 백그라운드 스레드에서 UI 업데이트


여기서 그치지 않고 함께 알아보면 좋은 것들

  • ANR (UI의 교착, 단일 스레드 모델 관련해 이어서)
  • Activity.runOnUiThread (handler에서 제공한다)
  • Runnable과 thread의 차이


참고자료
https://developer.android.com/guide/components/processes-and-threads?hl=ko

https://blog.msg-team.com/android-looper%EC%99%80-handler%EB%A5%BC-%ED%86%B5%ED%95%9C-thread%EA%B0%84%EC%9D%98-%ED%86%B5%EC%8B%A0-7a7c51f7a9f6

profile
건국대학교 GDSC 안드로이드 파트의 블로그입니다

0개의 댓글