Android : Looper, Handler, HandlerThread
참고자료
- Manifest Android Interview
- https://developer.android.com/reference/kotlin/android/os/Looper
- https://developer.android.com/reference/kotlin/android/os/Handler
- https://developer.android.com/reference/kotlin/android/os/HandlerThread
- https://source.android.com/docs/setup/contribute/api-guidelines
Looper
1
2
kotlin.Any
↳ android.os.Looper
1
class Looper
- 스레드의 메시지 루프를 실행하는 데 사용되는 클래스
- 스레드는 기본적으로 메시지 루프가 연결되어 있지 않다
- 루프 생성하기
- 루프를 실행할 스레드에서
prepare를 호출 - 루프가 중지될 때까지 메시지를 처리하도록
loop를 호출
- 루프를 실행할 스레드에서
메시지 루프와의 대부분 상호작용은 Handler 클래스를 통해 이루어집니다.
- Message Loop : 한 스레드에서 MessageQueue에 들어온 Message나 Runnable을 계속 꺼내서 처리(dispatch) 하는 반복 루프
다음은 prepare와 loop를 분리하여 Looper와 통신할 초기 Handler를 생성하는 Looper 스레드 구현의 전형적인 예입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare(); // Attach a Looper to the thread
// Use the Looper to create a Handler
mHandler = new Handler(Looper.myLooper()) {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop(); // Start the Looper
}
}
Looper는 message나 task queue를 순차적으로 처리하기 위해 스레드가 살아있게 유지하는 스레딩 모델의 구성요소이다. 안드로이드의 메인 스레드나 다른 worker thread의 중심적인 역할을 한다.
- 목적: 메시지 큐를 지속적으로 모니터링하여 메시지나 작업을 가져와 적절한 핸들러로 전달합니다.
- 사용법: 메시지를 처리하는 모든 스레드는 Looper가 필요합니다.
- 메인 스레드 : 자동으로 Looper를 가짐
- 워커 스레드 : 명시적으로 준비 필요
Handler
1
2
kotlin.Any
↳ android.os.Handler
1
open class Handler
Handler는 스레드의MessageQueue에 연결된Message와Runnable객체를 전송하고 처리할 수 있게 해준다.- 각
Handler인스턴스는 하나의 스레드와 그 스레드의 메시지 큐에 연관된다. - 새
Handler를 생성하면 해당Handler는 특정Looper에 바인딩됩니다. - 그러면 그
Handler는 그Looper의 메시지 큐로 Message와 Runnable을 전달하고, 해당Looper의 스레드에서 그것들을 실행합니다.
[!NOTE]
Runnable: 실행 가능한 코드 블록(함수)을 그대로 전달하는 방법,run()이 실행 코드Message: 데이터(식별자, 정수/객체/Bundle)를 담아 전달하고Handler의handleMessage()로 처리하게 하는 방식
Handler의 주요 용도
- Message나 Runnable을 향후 특정 시점에 실행되도록 예약하는 것
- 자신의 스레드가 아닌 다른 스레드에서 수행할 작업을 큐에 넣는 것입니다.
메시지 예약을 수행하는 메서드
- post 계열은 수신될 때 메시지 큐에 들어가 호출될
Runnable객체들을 큐에 넣을 수 있게 해준다.postpostAtTime(java.lang.Runnable, long)postDelayed
- sendMessage 계열은
handleMessage메서드에서 처리될 데이터 번들을 포함한Message객체를 큐에 넣을 수 있게 해줍니다(이 경우Handler의 서브클래스를 구현해야 합니다).sendEmptyMessagesendMessagesendMessageAtTimesendMessageDelayed
Handler로 포스트하거나 메시지를 보낼 때는
- 항목이 메시지 큐가 처리할 준비가 되는 즉시 처리되도록 허용할 것인지
- 처리되기 전에 지연 시간을 지정할 것인지(또는 절대 시간을 지정할 것인지)
- 이 방법을 통해 타임아웃, 틱(tick) 또는 기타 시간 기반 동작을 구현할 수 있습니다.
애플리케이션용 프로세스가 생성되면, 그 프로세스의 메인 스레드는 최상위 애플리케이션 객체들(액티비티, 브로드캐스트 리시버 등)과 이들이 생성하는 윈도우들을 관리하는 메시지 큐를 실행하는 데 전념합니다. 여러분은 자체 스레드를 생성할 수 있고, 새 스레드에서 동일한 post 또는 sendMessage 메서드를 호출하여 메인 애플리케이션 스레드와 통신할 수 있습니다. 그러면 주어진 Runnable이나 Message는 Handler의 메시지 큐에 스케줄되어 적절한 시점에 처리됩니다.
Handler 사용 시 성능 고려사항
Handler의 "has" 및 "remove" 메서드는 대기 중인 메시지 전체를 스캔하기 때문에 매우 느릴 수 있습니다. 기저에 있는 Looper의 MessageQueue에 많은 메시지가 대기 중이라면 이 작업은 비용이 클 수 있습니다.
- 대기 중인 작업을 취소하려고 메시지나 콜백을 제거하는 대신, 취소를 나타내는 플래그를 설정하고 해당 작업이 진행되기 전에 그 플래그를 확인하도록 구현하는 것을 고려하세요.
- 큐에 메시지나 콜백이 있는지를 확인해 대기 중인 작업 여부를 판단하는 대신, 그 작업이 시작될 때 플래그를 설정하도록 하여 상태를 추적하는 방법을 고려하세요.
설명
Handler는 스레드의 메시지 큐 내에서 메시지나 작업을 전송하고 처리하는 데 사용됩니다. Looper와 함께 동작합니다.
- 목적: 백그라운드 스레드에서 UI를 업데이트하는 등 한 스레드에서 다른 스레드로 작업이나 메시지를 post하기 위함.
1
2
3
4
5
6
val handler = Handler(Looper.getMainLooper()) // Runs on the main thread
handler.post {
// Code to update the UI
textView.text = "Update form background thread"
}
HandlerThread
1
2
3
kotlin.Any
↳ java.lang.Thread
↳ android.os.HandlerThread
1
open class HandlerThread : Thread
Looper를 가진 Thread입니다. 그런 다음 해당 Looper를 사용해 Handler를 생성할 수 있습니다. 일반 Thread와 마찬가지로 Thread.start()를 반드시 호출해야 합니다.
이 클래스는 오직 Handler API를 사용해야 하고, 그 처리를 기존 Looper 스레드(예: Looper.getMainLooper())가 아닌 별도의 Thread에서 수행해야 할 때만 사용하세요. 그렇지 않으면 Executor나 ExecutorService, 또는 Kotlin의 코루틴을 사용하는 것을 권장합니다.
많은 API들이 과거 SDK 버전에서는 Handler를 요구했지만, 최신 대안으로 Executor를 받는 경우가 있습니다. 가능하면 항상 최신 API를 우선 사용하세요.
HandlerThread의 대안(Alternative)
Executor는 스레딩 측면에서 더 유연합니다. Executor에 제출한 작업은 필요에 따라 다른 단일 스레드에서 실행되거나, 정적(static)/동적(dynamic) 풀의 여러 스레드 중 하나에서 실행되거나, 호출자 스레드에서 실행되도록 설정할 수 있습니다.
[!NOTE]
- static pool : 풀의 스레드 수가 고정(또는 거의 고정)인 구현. ex)
newFixedThreadPool- dynamic pool : 부하에 따라 스레드를 늘리거나 줄이며 크기가 동적으로 변하는 구현. ex)
newCachedThreadPool
Executor는 Handler에 비해 더 단순한 API를 제공합니다. ExecutorService는 Future API 같은 풍부한 기능을 제공하여 작업 상태 모니터링, 작업 취소, 예외 전파, 여러 대기 작업의 체이닝 등에 사용할 수 있습니다.
Executors는 일반적인 동시성 요구를 충족하는 다양한 Executor 인스턴스를 생성하는 팩토리입니다. 이 Executor들은 HandlerThread보다 더 나은 동시성 및 낮은 경합(reduced contention)을 제공하는 작업 큐를 사용합니다.
Kotlin에서는 코루틴을 사용해 동시성 처리를 하는 것도 좋은 방법입니다.
HandlerThread의 일반적인 성능 문제
앱이 HandlerThread를 사용하면 다음과 같은 성능 문제가 발생할 수 있습니다.
- 과도한 스레드 생성(Excessive thread creation):
HandlerThread는Thread입니다. 시스템 스레드마다 일정한 메모리 비용이 발생합니다. 특정 작업 유형별로 각기 전용HandlerThread를 많이 만들면, 수요에 따라 크기를 늘리거나 줄일 수 있는ThreadPoolExecutor같은 대안보다 비효율적으로 메모리를 낭비할 수 있습니다. - 락 경합(Lock contention):
HandlerThread는Looper를 사용하고,Looper는MessageQueue를 사용합니다.MessageQueue는 내부 큐 접근을 동기화하기 위해 단일 락을 사용합니다. 여러 스레드가 동시에 메시지를 큐에 넣으려 할 때와HandlerThread자체가 다음 메시지를 꺼내려 할 때 서로 블록될 수 있습니다. - 우선순위 반전(Priority inversion): 우선순위가 높은
HandlerThread가 낮은 우선순위 스레드에 의해 블록될 수 있습니다. 예를 들어 높은 우선순위 스레드가 메시지를 equeue하려 할 때 낮은 우선순위 스레드가 다음 메시지를 dequeue하려 하면 상호 차단이 발생할 수 있습니다.
이러한 문제를 피하려면 HandlerThread 대신 Executor나 Kotlin 코루틴을 사용하는 것이 가장 좋습니다.
설명
HandlerThread는 내장된 Looper를 가진 특수한 Thread입니다. 작업 또는 메시지 큐를 처리할 수 있는 백그라운드 스레드를 만드는 과정을 단순화합니다.
- 목적: 자체
Looper를 가진 워커 스레드를 생성하여 해당 스레드에서 작업을 순차적으로 처리할 수 있게 합니다. - 라이프사이클
HandlerThread를 시작 :start()Looper획득 :getLooper()- 자원 해제 :
quit()또는quitSafely()로Looper를 종료
1
2
3
4
5
6
7
8
9
10
11
12
13
val handlerThread = HandlerThread("WorkerThread")
handlerThread.start() // Start the thread
val workerHandler = Handler(handlerThread.looper) // Use its Looper for tasks
workerHandler.post {
// Perform background tasks
Thread.sleep(1000)
Log.d("HandlerThread", "Task completed")
}
// Stop the Thread
handlerThread.quitSafely()
관련한 Android API Guideline
콜백 디스패치를 제어하기 위한 Executor 수용
명시적인 스레딩 기대값이 없는 콜백을 등록할 때(사실상 UI 툴킷 외부의 거의 모든 곳), 콜백이 호출될 스레드를 개발자가 지정할 수 있도록 등록 시 Executor 매개변수를 포함하는 것을 강력히 권장합니다.
1
2
3
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
참고: 개발자는 유효한 Executor를 반드시 제공해야 합니다. 새로운 @CallbackExecutor 애노테이션은 개발자 문서에 일반적인 기본 옵션들을 자동으로 표시해 줍니다. 또한 Kotlin에서 관용적으로 사용하기 위해 콜백 인수가 마지막 매개변수여야 한다는 점에서 콜백 인수는 마지막에 위치해야 합니다.
일반적인 선택적 매개변수 가이드라인의 예외로서(옵션 매개변수는 마지막에), Executor를 생략한 오버로드를 제공하는 것도 허용됩니다. 단, Executor가 제공되지 않으면 콜백은 Looper.getMainLooper()를 사용하여 메인 스레드에서 호출되어야 하며, 이는 해당 오버로드된 메서드에 문서화되어야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* ...
* Note that the callback will be executed on the main thread using
* {@link Looper.getMainLooper()}. To specify the execution thread, use
* {@link registerFooCallback(Executor, FooCallback)}.
* ...
*/
public void registerFooCallback(
@NonNull FooCallback callback)
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
Executor 구현 시 주의점: 다음 구현은 유효한 executor임을 유의하세요!
1
2
3
4
5
6
public class SynchronousExecutor implements Executor {
@Override
public void execute(Runnable r) {
r.run();
}
}
이것은 이러한 형태의 API를 구현할 때, 앱 프로세스 쪽의 바인더(binder) 객체 구현부가 앱이 제공한 Executor에서 앱의 콜백을 호출하기 전에 반드시 Binder.clearCallingIdentity()를 호출해야 함을 의미합니다. 이렇게 해야 바인더 아이덴티티를 사용하는 앱 코드(예: Binder.getCallingUid())가 권한 검사 시에 호출자를 시스템 프로세스가 아닌 앱으로 올바르게 귀속시킵니다. 만약 API 사용자들이 호출자의 UID나 PID 정보를 원한다면, 이는 Executor가 어느 곳에서 실행되었는지에 따라 암묵적으로 결정되게 하지 말고 API 표면에 명시적으로 포함시켜야 합니다.
API가 Executor를 지정받도록 하는 것은 지원되어야 합니다. 성능에 민감한 경우 앱은 코드가 즉시 혹은 동기적으로 실행되어 API로부터 피드백을 받기를 원할 수 있습니다. Executor를 수락하면 이런 사용 사례가 가능해집니다. 방어적으로 추가 HandlerThread 등을 만들어 트램폴린(trampoline)으로 우회하는 것은 이러한 바람직한 사용 사례를 무너뜨립니다.
앱이 자체 프로세스 내에서 비용이 많이 드는 작업을 실행하려는 경우라면, 그들을 허용하세요. 개발자가 당신의 제한을 우회하기 위해 찾을 해결책들은 장기적으로 지원하기 훨씬 더 어려워질 것입니다.
단일 콜백의 예외: 보고되는 이벤트의 성격상 단 하나의 콜백 인스턴스만 지원해야 하는 경우에는 다음 스타일을 사용하세요:
1
2
3
4
5
public void setFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
public void clearFooCallback()
Handler 대신 Executor 사용
과거에는 콜백 실행을 특정 Looper 스레드로 리다이렉트하기 위해 Android의 Handler가 표준으로 사용되었습니다. 그러나 대부분의 앱 개발자가 자체 스레드 풀을 관리하고 있어 앱에서 사용 가능한 Looper 스레드는 메인(또는 UI) 스레드뿐인 경우가 많으므로, 이 기준은 Executor를 선호하는 방향으로 바뀌었습니다. 개발자들이 기존/선호하는 실행 컨텍스트를 재사용할 수 있도록 Executor를 사용하세요.
kotlinx.coroutines나 RxJava 같은 현대 동시성 라이브러리들은 자체 스케줄링 메커니즘을 제공하여 필요할 때 자체 디스패치를 수행합니다. 이로 인해 이중 스레드 홉(double thread hop)으로 인한 지연을 피하기 위해 직접 실행하는(ex: Runnable::run) 직접 실행기(direct executor)를 사용할 수 있도록 하는 것이 중요합니다. 예를 들어, Handler로 Looper 스레드에 포스트한 다음 앱의 동시성 프레임워크에서 다시 홉(hop)이 발생하면 추가 지연이 생깁니다.
[!NOTE]
- hop : 작업(예:
Runnable, 콜백, 코루틴 복구 등)이 한 실행 컨텍스트(스레드/루퍼/디스패처)에서 다른 실행 컨텍스트로 이동해서 실행되는 한 번의 스케줄링/전달
- 즉, 어떤 스레드 A에서 작업을 올려두고(enqueue) 다른 스레드 B가 그 작업을 꺼내 실행하면 한 번의 hop이 발생한 것이다.
- ex)
handler.post(runnable)-> runnable이 나중에 Looper 스레드에서 실행되면 한 hop- double thread hop : 두 번 연속으로 다른 컨텍스트로 옮겨 실행되는 경우, 동일한 논리 작업이 두 번 큐잉/스케줄링 되어 두 단계로 이동하는 상황
- 앱이 코루틴
Dispatchers.Default(스레드 A)에서 동작중이다- 라이브러리가
Handler.post(...)로 메시지를 보내 Looper 스레드(스레드 B)로 작업을 옮긴다. -> 첫 번째 hop (A -> B)- Looper에서 실행되는 그 작업이 다시 앱의 동시성 프레임워크(예: 코루틴
Dispatcher또는 Rx 스케줄러)에 의해 다른 스레드(스레드C)에서 실행되도록 스케줄링된다. -> 두 번째 hop (B -> C) (실행은 한 번만 일어남)
이 가이드라인의 예외는 드뭅니다. 흔한 예외 요청 사유는 다음과 같습니다:
- 이벤트를 위해
epoll이 필요한 상황이라Looper가 필요하다. 이런 경우에는Executor의 이점이 실현될 수 없으므로 예외가 허용됩니다. - 내 스레드에서 앱 코드가 블록되는 것을 원치 않는다. 앱 프로세스에서 실행되는 코드에 대해 이 예외는 일반적으로 허용되지 않습니다. 이를 잘못 처리하는 앱은 전반적인 시스템 건강에 영향을 주지 않고 자신에게만 불이익을 줍니다. 올바르게 처리하거나 공통 동시성 프레임워크를 사용하는 앱은 추가적인 지연 페널티를 받아선 안 됩니다.
- 같은 클래스 내의 다른 유사한 API들과 지역적으로
Handler가 일관적이다. 이 경우 상황에 따라 예외가 허용됩니다. 우선 순위는Executor기반의 오버로드를 추가하여Handler구현을Executor구현으로 마이그레이션하는 것입니다. (myHandler::post는 유효한Executor입니다!) 클래스의 크기, 기존Handler메서드 수, 개발자가 기존Handler기반 메서드와 새 메서드를 함께 사용해야 할 가능성 등을 고려하여Handler기반 메서드 추가에 대한 예외가 허용될 수 있습니다.