이 포스트는 Android Main Method에서도 필수적으로 쓰이고 있는 핸들러 클래스에 대한 이야기를 자세하게 살펴보는 포스트입니다.
Handler는 Message
를 MessageQueue
에 넣는 기능과 MessageQueue
에서 꺼내 처리하는 기능을 함께 제공합니다.
Handler가 Looper, MessageQueue와 어떤 관계가 있는지 살펴보고 Handler의 사용 방법에 대해서 알아봅시다.
Handler를 사용하려면 먼저 생성자를 이해해야 합니다. Handler에는 기본 생성자 외에도 Handler.Callback
이 전달되는 생성자도 있고, Looper
가 전달되는 생성자도 있습니다.
저 위 첫번째 항목부터 3번째 항목의 생성자는 파라미터 개수가 많은 4번째 생성자를 다시 호출합니다. Handler는 Lopper(결국 MessageQueue)와 연결되어 있습니다.
기본 생성자는 바로 생성자를 호출하는 스레드의 Looper를 사용하겠다는 의미입니다.
(여기서 잠깐, Looper는 스레드 로컬 스토리지(TLS)에 저장되고 꺼내어집니다.)
따라서 메인 스레드에서 Handler 기본 생성자
는 앱 프로세스가 시작할 때 ActivityThread에서 생성한 메인 Looper를 사용합니다. Handler()
는 UI 작업할 때 많이 사용됩니다.
그럼 여기서 백그라운드 스레드에서 Handler 기본 생성자를 사용해도 될지에 대한 궁금증이 생기는데요. 백그라운드 스레드에서 Handler() 생성자를 사용할 때, Looper가 준비되어 있지 않다면 RuntimeException
이 발생합니다.
RuntimeException
의 "Can't create handler inside thread that has not called Looper.prepare"라는 메세지에 따라 문제를 해결하려면, 먼저 Looper.prepare()를 실행해서 해당 스레드에서 사용할 Looper를 준비해야 합니다.
내부적으로 prepare()
메서드는 MessageQueue
를 생성하는 것 외에 별다른 동작을 하지 않습니다.
참조) Looper API 공식 문서
class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler(Looper.myLooper()) {
public void handleMessage(Message msg) { - (1)
// process incoming messages here
}
};
Looper.loop();
}
}
위 코드는 백그라운드 스레드에서 Handler를 사용하는 샘플 코드입니다.
LooperThread에서 스레드를 시작하면 Looper.loop()
에 무한 반복문이 있기 때문에 해당 스레드는 종료되지 않습니다.
그리고 mHandler
에서 sendXxx(), postXxx() 메서드를 사용하면 스레드 내에서 (1) handleMessage(Message msg)
을 실행합니다.
개발 중에 Looper
가 준비되지 않아서 RuntimeException
을 만나는 경우가 있습니다. 바로 백그라운드 스레드에서 Handler 기본 생성자를 쓴 경우입니다.
메서드 호출 스택이 깊어지면 호출 위치가 메인 스레드인지 백그라운드 스레드인지 알기 쉽지 않습니다. 예를 들어, 여러 곳에서 사용하는 메서드라면 메인 스레드뿐만 아니라 백그라운드 스레드에서도 호출할 가능성이 있습니다. 기본 생성자로 Handler를 생성하고 post() 메서드를 통해서 TextView를 변경하려 하는데, Handler를 메인 스레드에서 생성하는지 백그라운드 스레드에서 생성하는지 모호해집니다.
메인 스레드에서는 메인 Looper
가 이미 있어서 문제가 되지 않지만, 백그라운드 스레드에서는 대응하는 Looper
가 없다면 RuntimeException을 만나게됩니다.
Handler는 일반적으로 UI 갱신을 위해 사용됩니다.
백그라운드 스레드에서 네트워크 DB 작업 등을 하는 도중에 UI를 업데이트합니다. 지금은 deprecated되서 잘 쓰지 않는 AsyncTask에서도 내부적으로 Handler를 이용해서 onPostExecute() 메서드를 실행해서 UI를 업데이트합니다.
UI 작업 중에 다음 UI 갱신 작업을 MessageQueue
에 넣어 예약합니다. 작업 예약이 필요한 경우가 있는데, 예를 들어 Activity onCreate()
메서드에서 하지 못하는 일들이 있습니다.
소프트 키보드를 띄우는 것이나, ListView의 setSelection() 메서드를 호출하는 작업은 onCreate() 메서드에서는 잘 동작하지 않습니다.
이 때 Handelr
에 Message
를 보내면 현재 작업이 끝난 이후의 다음 타이밍에 Message를 처리합니다.
반복해서 UI를 갱신하는 패턴은 다음과 같습니다.
private static final int DELAY_TIME = 2000;
private Runnable updateTimeTask = new Runnable() {
@Override
public void run() {
systemInfo.setText(monitorService.getSystemInfo());
handler.postDelayed(this, DELAY_TIME);
}
}
public void onClickButton(View view) {
handler.post(updateTiemTask);
}
UI 갱신이 끝나고 handler.postDelayed(this, DELAY_TIME);
에서 postDelayed()에 Runnalbe 자체를 전달해서 계속 반복합니다.
시간을 제한할 때도 Handler를 사용합니다. 안드로이드에서 내부적으로 ANR을 판단할 때도 사용하는 방법이기도 합니다.
앱에서 많이 사용하는 방식으로는, 몇 초 내에 back 키(=뒤로가기)를 반복해서 누를 때에만 앱이 종료되도록 할 때 쓰이는 방식입니다.
example code
private boolean isBackPressedOnce = false;
@Override
public void onBackPressed() {
if (isBackPressedOnce) {
super.onBackPressed();
} else {
Toast.makeText(this, R.string.backpressed_message, Toast.LENGTH_SHORT).show();
isBackPressedOnce = true;
timerHandler.postDelayed(timerTask, 5000);
}
}
private final Runnalbe timerTask = new Runnable() {
@Override
public void run() {
isBackPressedOnce = false;
}
}
안드로이드 프레임워크 내부에서 쓰이는 Handler
안드로이드 프레임워크에서도 내부적으로 Handler를 많이 사용합니다. 메인 스레드에서 실행해야 하는 작업들이, Handler 사용해서 메인 Looper의 MessageQueue를 거쳐서 순차적으로 실행됩니다.
- ActivityThread의 내부 클래스인
H
는 Handler를 상속- ViewRootImpl 클래스에서 Handler를 이용해서
touch(터치)
,invalidate(그리기)
등 이벤트 처리- Activity는 멤버 변수에 Handler가 있고
runOnUiThread()
메서드에서만 사용- View에는 ViewRootImpl에서 전달된 ViewRootHandler를
post()와 postDelayed()
메서드에서 사용
개발하다 보면 타이밍 이슈를 접하게 되는데, Handler의 타이밍 이슈도 마찬가지입니다.
원하는 동작 시점과 실제 동작 시점에서 차이가 생기지만, 이런 타이밍 이슈는 메인 스레드와 Handler를 이해하고 나면 단순하게 해결할 수 있는 문제기도 합니다.
먼저 메인 스레드는 한 번에 하나의 작업밖에 하지 못하고, 여러 작업이 서로 엉키지 않기 위해서 메인 Looper의 MessageQueue에서 Message를 하나씩 꺼내서 처리합니다.
또한, MessageQueue에서 Message를 하나 꺼내오면 Activity의 onCreate()에서 onResume()까지 쭉 실행됩니다.
onCreate()
에서 Handler의 post()에 전달한 Runnable은 onResume()
이후에 실행됩니다. 이미 MessageQeueu에서 꺼내 실행 중이기 때문에 onCreate()
실행 도중에 Handler의 postAtFrontOfQueue() 메서드를 실행해도 마찬가지로 onResume() 이후에 실행됩니다.
Handler에서 -Delayed()
나 -AtTime()
메서드에 전달된 지연 Message는 지연 시간(delay time)을 정확하게 보장하지는 않는다. MessageQueue에서 먼저 꺼낸 Message 처리가 오래 걸린다면 실행이 당연히 늦어집니다.
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.d(TAG, "200 delay");
}
}, 200);
handler.post(new Runnable() {
@Override
public void run() {
Log.d(TAG, "just");
SystemClock.sleep(500);
}
});
위 코드 속 postDelayed()
와 post()
에서 각각 Runnable Message를 보내고 있습니다. postDelayed()
에서 먼저 첫 번째 Message를 보내지만 200ms 이후에 실행되는 작업이고 post()
에서 보낸 두 번째 Message는 즉시 실행되지만 sleep
시간을 포함하여 500ms가 걸리는 작업입니다.
단일 스레드 규칙 때문에 앞의 작업을 다 끝내야만, 뒤의 것을 처리할 수 있는데요. 따라서 200 delay
라는 로그는 200ms가 아닌 최소 500ms 이후에 남게 됩니다. 그러므로 지정한 지연 시간 후에 정확히 Message가 처리된다고 가정하면 안 됩니다.
또한, 200ms 간격을 두었다면 그 사이에 다른 Message가 MessageQueue에 쌓일 가능성이 있습니다.
하나의 예를 든 것이지만 메인 스레드에서 다른 Message를 처리느라고 당장 하려는 작업이 지연되는 케이스는 실제로도 흔합니다.
Handler가 쓰는 함수에 대한 내용도 있지만, 너무 루즈하고 늘어질까봐 이 글에서는 생략했습니다. 이번 포스트를 정리하면서 핸들러에 대해 다시 한 번 알수 있었던 기회였고 역시 기본 클래스에 대해 정확하게 알아야 할 필요성을 또 느끼게 됐습니다.
안드로이드 프로그래밍 Next Step - ch 2 중
사소하지만 velog는 코드 작성이 너무 불편하다. 단순한 markdown으로 쓰는.. 에디터에 대한 mvp 기능만 있을 뿐이지 완전 좋진 않다.. 하물며 Notion이나 깃헙 readme처럼 코드를 잘 보여지는 것도 아니라 가독성에 문제가 있다... 쓰는 사람도 불편하다..