프로그래밍/Android

[Android] Android Thread, Looper, Handler

돌및쓰고 2021. 10. 18. 23:33

Android MainThread

Android 에서 App process가 시작되면 MainThread가 시작되는데 MainThread가 UI를 유일하게 수정할수 있는 Thread이기 때문에 MainThread라 하면 일반적으로 UI Thread라고 한다. (예외적으로 UI를 가지지 않는 Service, BroadcastReciver, application 경우에는 UI Thread라고 부르는것이 적절하지 않다.)

일반적으로 어플리케이션에서는 성능을 위해 Multi Thread를 사용합니다. 그러나 UI를 업데이트 하는데는 Single Thread를 주로 사용하는데 이는 UI를 업데이트하며 교착상태, 데드락 등을 방지해야 하기 때문이다.

 

Java 어플리케이션 에서는 main() 메소드로 실행되는 것이 MainThread라고 할수 있다. 그렇다면 안드로이드의 MainThread는 무엇일까?

 

안드로이드에서는 ActivityThread에 있는 main() 메소드가 앱의 시작점이다

public static void main(String[] args) {
    ... 중략
    Looper.prepareMainLooper(); // Looper를 준비

    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

    ... 중략

    // End of event ActivityThreadMain.
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    Looper.loop(); // Lopper 실행

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

ActivityThread에서 가장 중요한 부분은 Lopper라고 할수 있는데

Looper.prepareMainLooper()를 통해 Lopper를 준비하고 Looper.loop()를 통해 UI 관련 처리를 실행한다. 그리고 Lopper.loop()에는 무한 반복문이 있어 main() 메서드는 프로세스가 종료되지 않게 해준다.

Looper

 

위 사진은 Android의 Thread간의 통신 방법을 보여준다 MainThread는 Looper를 가지고 Message Queue에 있는 Message를 처리하고 Handler를 통해 Message를 Queue에 넣나 처리한 결과를 알려준다.

여기서 Looper는 안드로이드에서 중요한 통신수단이라고 할수 있다.

Lopper 는 TSL(thread local storage)에 저장되고 꺼낼수 있다

@UnsupportedAppUsage
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
...
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

TSL은 Thread의 범위로 선언하는 변수로 prepare() 할때 현재 Thread에 Looper가 없다면 새롭게 생성하고 TSL에 저장해 두어 Thread마다 하나의 Looper를 가지게 되어있다.

Looper는 각각의 Message Queue를 가지는데 이를 통해 Ui작업에서 경합 상태를 해결한다.

public static void loop() {
    final Looper me = myLooper();
    final MessageQueue queue = me.mQueue;

    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;
        } catch (Exception exception) ...
        msg.recycleUnchecked();
    }
}

Looper가 실행되는 loop()를 살펴보면 messageQueue에서 하나씩 message를 꺼내고 msg가 null일때 loop()를 종료하는 것을 볼 수 있다. 그리고 dispatchMessage를 실행하는 target은 Handler로 Message를 처리함을 알려준다

Handler

Handler는 Message를 MessageQueue에 넣는 기능과 MessageQueue에서 Message를 꺼내 처리하는 기능을 제공한다. 그리고 이를 통해 우리가 Background Thread에서 MainThread에 Ui를 업데이트를 요청 할 수있도록 도와준다.

Handler의 기본생성자는 Deprecated되었는데 그 이유는 Handller에 looper를 지정하지 않으면 자동으로 Looper를 선택하게 되는데 그때 현재 Thread에 Looper가 없을 수도 있고, 의도와 다른 Looper를 선택해 의도하지 않은 동작을 발생시 킬 수 있어 Deprecated되었다고 한다. 그래서 우리는 보통 MainLooper를 가져와 일반적으로 Handler를 생성한다.

그래서 최종적으로 Looper가 호출하는 dispatchMessage를 Handler는 수신받고 우리가 callback을 통해 Message객체를 수신 받게되는 것이다.

@Deprecated
public Handler() {
    this(null, false);
}

//Looper에서 호출하는 dispatchMessage
public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

// Handler 생성
Handler(Looper.getMainLooper()) {
    //handleMessage
    return@Handler true
}

-Delayed, -AtTime Handler의 처리 시점

앞서 Looper는 Message Queue를 통해 Message를 처리 한다고 했다. 그렇다면 Message의 처리가 지연 된다면 뒤에 Message도 지연될까?

val handler = Handler(Looper.getMainLooper())
handler.postDelayed({
    Log("200ms delay")
}, 200)
handler.post({
    delay(500)
})

Handler의 Delayed, AtTime은 실행 시점을 보장하지 않는다 앞에 Message의 처리가 지연된다면 뒤에 Message들도 자연스럽게 딜레이 된다.

그래서 위에 "200ms delay"는 최소한 500ms 뒤에 실행되게 된다

Background에서 UI수정하기

// 1. Handler
Handler handler = new Handler(Looper.getMainLooper())
handler.post({
    //UI UPdate
})

// 2. runOnUiThread
public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}

// 3. View.post
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
}

Background에서 UI를 업데이트 하려고 에러 메시지와 같이 종료 될텐데

이를 방지하고 update하는 방법은 일반적으로 3가지가 있다

  • 첫번째는 Handler를 통해 update하는 것이다 Handler에서 처리하는 내용은 MainThread에서 실행되므로 Background에서 처리를 마친뒤 Handler로 데이터만 전달해 Ui를 update하는 것이다.
  • 두번째는 Activity의 runOnUiThread로 첫번째 방법을 메소드로 만들어 둔 것 인데 현재 Thread가 MainThread라면 그냥 실행하고 다른 Thread라면 Handler를 통해 처리한다
  • 세번째는 View에 있는 post기능이다 View를 통해 MainThread의 Handler에 접근해 Ui를 update 할 수 있다. 그러나 이방법은 View가 Attached상태일 때만 동작한다

세가지 방법 모두 결국엔 Handler를 통해 동작함을 볼수 있다. 이렇게 Android 는 Thread간 통신의 Handler를 다양하게 사용 하는 것을 볼수 있다.

최근에는 LiveData나 코루틴, Rxjava같이 새로운 기술들이 많이나와 Handler를 직접 사용하는 경우가 적어졌지만 이런 기술들이 Handler를 바탕으로 동작함을 알아야 한다.