[Android] Thread, Handler, Looper, Message Queue 다루기

WonseokOh·2022년 5월 1일
0

Android

목록 보기
3/16
post-thumbnail

Asynchronous Programming

  요즘 대부분의 프로그램들은 비동기적으로 실행됩니다. 안드로이드에서도 마찬가지로 다양한 방법을 이용하여 비동기 프로그래밍을 구현할 수 있습니다. 별도의 Thread를 생성하여 동시적으로 작업을 실행하게 하는 방법부터 AsyncTask, RxJava, RxKotlin, Coroutine 등 편리하게 구현할 수 있도록 많은 라이브러리들이 나타나고 있습니다. 하지만 안드로이드에서 제공해주는 Thread, Handler, Looper, Message Queue를 제대로 알고 있어야 기본 원리를 깨닫게 되고 다른 라이브러리들의 편리함을 알 수 있을 것 같아 공부를 하게 되었습니다.


Thread

  안드로이드 애플리케이션을 실행하면 프로세스가 메모리에 올라와 실행되고 자동으로 메인 스레드(Main Thread) 까지 생성합니다. Main Thread에서는 UI 관련된 작업들을 처리하게 되는데 Main Thread 이외의 일반 Thread에서는 UI 작업을 처리할 수 없도록 하였습니다. 왜냐하면 EditText의 값을 Thread1, Thead2에서 동시에 값을 변경 후 내용을 읽으면 기대한 결과가 나타날 수 없고 경쟁상태(Race condition)가 되기 때문에 안드로이드 시스템에서는 Main Thread에서만 UI를 변경할 수 있도록 하였습니다.

Main Thread

  • 주로 UI 작업을 처리하는 스레드로 UI 스레드라고도 불림
  • Main Thread에서는 UI 작업이 아닌 시간이 오래 걸리는 작업을 하게 되면 ANR이 발생
  • 안드로이드 컴포넌트의 생명주기 메소드와 그 내부 호출은 모두 Main Thread에서 처리
  • Activity 이외에도 BroadcastReceiver, Service, Application에서 UI와 관련이 없더라도 Main Thread에서 작업 처리

Worker Thread

애플리케이션의 성능을 향상시키기 위해 Thread를 생성하여 Network 작업, DB 쿼리와 같은 시간이 오래 걸리는 작업들을 주로 처리하게 됩니다. Worker Thread는 백그라운드에서 처리하는 작업들이 많아 백그라운드 Thread라고도 부릅니다.


ANR(Application Not Response)

  안드로이드 애플리케이션 개발을 하면서 ANR은 모두가 한 번쯤은 경험해봤을 것이라고 생각합니다. ANR은 말 그대로 애플리케이션이 더 이상 반응하지 않는 상태로 Main Thread에서 너무 오래 걸리는 작업을 수행할 경우에 발생하게 됩니다. 애플리케이션이 포그라운드 상태에서 작업 진행 중에 ANR이 발생하면 팝업이 나타나고 해당 팝업으로 애플리케이션을 강제종료 시킬 수 있습니다. 안드로이드 컴포넌트 실행은 Main Thread에서 처리하기에 어디서든 ANR이 발생할 수 있지만, 대표적으로 자주 발생하는 경우가 있습니다.

  • Main Thread에서 Network 작업과 같은 시간이 오래 소요되는 작업 시
  • BroadcastReceiver가 5초 이내로 반응하지 않을 시
  • Service에서도 오랜 시간 동안 리턴하지 않을 경우

  위의 설명과 같이 UI를 변경하는 작업은 Main Thread에서 처리하고 시간이 오래 소요되는 작업은 Worker Thread를 생성해서 처리하는 것을 보았을 때 안드로이드 애플리케이션 상에서 여러 Thread가 존재할 가능성이 높습니다. 그래서 Thread간 통신하는 경우도 존재하고 통신하는 방법에 대해서 알 필요가 있습니다. 이를 위한 Looper, Handler, Message Queue 등 다양한 클래스가 존재하고 해당 클래스의 역할과 과정을 잘 이해해야 합니다.


Looper

  Looper 클래스는 Message Queue를 생성하고 관리하는 역할로 Message나 Runnable 객체를 하나씩 꺼내서 Handler에 전달합니다. Looper 클래스와 Message Queue로 인해 UI 경쟁상태(Race Condition)을 방지할 수 있습니다. Looper.loop() 메소드 내부에서 무한루프를 돌면서 Message Queue를 확인하여 하나씩 처리하게 됩니다.

Looper.loop()

  public static void loop() {
          final Looper me = myLooper();
          if (me == null) {
              throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
          }
          final MessageQueue queue = me.mQueue;

          // Make sure the identity of this thread is that of the local process,
          // and keep track of what that identity token actually is.
          Binder.clearCallingIdentity();
          final long ident = Binder.clearCallingIdentity();

          // Allow overriding a threshold with a system prop. e.g.
          // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
          final int thresholdOverride =
                  SystemProperties.getInt("log.looper."
                          + Process.myUid() + "."
                          + Thread.currentThread().getName()
                          + ".slow", 0);

          boolean slowDeliveryDetected = false;

          for (;;) {
              Message msg = queue.next(); // might block
              if (msg == null) {
                  // No message indicates that the message queue is quitting.
                  return;
              }

              ...

              try {
                  msg.target.dispatchMessage(msg);
                  if (observer != null) {
                      observer.messageDispatched(token, msg);
                  }
                  dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
              }
          }
          ...
   }

  메소드를 좀 더 자세히 살펴보면 자신의 Looper를 가져오고, Looper 클래스의 멤버변수로 MessageQeueu가 있어서 직접 가져올 수 있습니다. MessageQueue에서 하나씩 살펴보면 msg가 null이 아니라면 dispatchMessage 함수를 통해 Message를 처리하게 됩니다. 여기서 Looper가 종료될 때 Message가 null이 됩니다.

  Main Thread에는 기본적으로 실행 시에 Looper를 생성하여 Looper.getMainLooper() 메소드를 통해 가져올 수 있습니다. 하지만 일반 Thread에서는 Looper가 없기 때문에 run 메소드를 오버라이딩하여 작성한 코드만 실행할 뿐 메세지를 받거나 처리할 수 없습니다. 일반 Thread에서도 다른 Thread에서 보내는 메세지를 받거나 처리하기 위해서는 아래와 같은 일련의 작업이 필요합니다.

  • 일반 스레드에서도 메세지를 받기 위한 Looper를 생성합니다.
  • Looper.prepare() 메소드를 통해 스레드별로 Looper를 생성합니다.
  • 이후 Looper.loop() 메소드에서 MessageQueue를 하나씩 확인하여 Message를 Handler에게 전달합니다.
  • Handler에서는 해당 Message를 처리할 수 있습니다.
  • Looper를 종료하기 위해서 quit(), quitSafely() 함수를 사용할 수 있습니다. quit함수는 즉시 종료하고, quitSafely는 안전하게 MessageQueue에 쌓인 메세지까지 처리한 후 종료하게 됩니다.

Meesage Queue

  MessageQueue는 Looper 클래스를 설명하면서 많이 설명하였지만 다시 설명하자면 Message를 담는 Queue 형태의 자료구조입니다. MessageQueue는 Message를 처리할 때 Queue에서 삭제하고, Meesage를 추가할 때는 Queue에 추가하기 때문에 추가, 삭제가 용이한 구조여야 합니다. MessageQueue는 LinkedBlockingQueue로 구현되어 있으며 링크드 리스트와 같은 구조로 다음 노드에 대한 링크를 가지는 방식으로 하여 Message 추가, 삭제를 빠르게 할 수 있습니다. MessageQueue에는 해당 Message가 실행되는 순서대로 추가가 되며, 중간에 먼저 처리가 되어야 하는 Message가 들어오면 중간에 삽입이 될 수도 있습니다.


Handler

  Handler는 Looper로부터 처리되어야 할 Message들을 받아서 처리하는 역할과 다시 MessageQueue에 Message를 전달하는 역할을 합니다. 또한, Handler를 통해 다른 Thread에서 요청하는 메세지를 처리하고 다시 해당 Thread에 메세지를 전달할 수도 있기 때문에 Thread간의 통신을 담당한다고 할 수 있습니다.

Handler Constructor

  원래 Handler의 기본 생성자는 4개로 정의되어 있었지만, 작업들을 처리하지 못하고 잃거나 충돌로 인한 버그들로 인해 2개는 deprecated 되었습니다. 이전에는 매개변수가 없는 생성자로 Handler 생성 시에 자동으로 실행되는 스레드와 루퍼에 연결하였지만, 이제는 명시적으로 Looper를 대입하는 생성자를 사용하여야 합니다.


class LooperThread extends Thread{
    public Handler mHandler;

    @Override
    public void run() {
        Looper.prepare();
        mHandler = new Handler(Looper.myLooper()){
            @Override
            public void handleMessage(@NonNull Message msg) {
                // 메세지 처리
                super.handleMessage(msg);
            }
        };
        Looper.loop();
    }
}

  다음 코드에서 Looper.prepare()과 Looper.loop() 메소드가 없으면 NPE(NullPointerException)이 발생하게 됩니다. Handler의 생성자에는 Looper가 들어가야 하며 myLooper를 통해서 TLS(Thread Local Storage)에 저장된 Looper를 가져올 수 있습니다. 하지만 prepare 함수를 호출하지 않으면 Looper가 생성되지 않고 MessageQueue도 없기 때문에 null이 되어 NPE가 발생하게 됩니다. 따라서 일반 스레드에서 Handler를 사용하려면 Looper를 무조건 생성해주어야 합니다.


Handler 주요 용도

1.일반 스레드에서 UI 업데이트

일반 스레드에서 네트워크 통신이나 DB 작업 이후 UI 업데이트가 필요할 경우 Main Thread에 연결된 Handler를 통해 업데이트 작업을 요청할 수 있습니다.

2. 메인 스레드에서 다음 작업 예약

메인 스레드에서 UI 작업을 바로 하지 못하는 경우도 있습니다. 이 때 API 중 Delayed가 붙은 메소드를 통해 특정 시간 이후에 실행할 수 있도록 Handler를 사용할 수 있습니다.

3. 반복 갱신

반복해서 UI를 갱신할 때 Handler에 Runnable 객체를 대입하고 Runnable 객체에서는 해당 Handler를 가지고 다시 Runnable을 호출하는 재귀적인 방식으로 코드를 작성합니다. 처음 Handler 호출은 post 메소드를 사용하고 이후 Runnable 에서는 postDelayed를 호출하여 반복적으로 주기마다 Runnable 객체를 실행합니다.

4. 시간 제한

안드로이드에서 시간 제한할 때도 Handler를 주로 사용합니다. 뒤로 가기 버튼을 더블 클릭 시 특정 이벤트가 발생한다고 했을 때 더블 클릭에 대한 시간 제한을 두어 해당 시간안에 클릭이벤트가 한 번 더 입력이 되면 특정 이벤트를 발생하도록 합니다.

ex) 2초 안에 뒤로가기 더블 클릭 시 액티티비 종료

  • 처음 클릭 시 finish 변수 true 변경
  • 한 번 더 클릭 시 finish가 true면 종료
  • 하지만 2초 안에 더블 클릭이 발생하지 않으면 Handler가 finish를 false로 변경

참고

profile
"Effort never betrays"

0개의 댓글