본문 바로가기
Android/안드로이드 프로그래밍 Next Step

안드로이드 프로그래밍 Next Step - 2.1 메인 스레드와 Handler

by 몽슬몽슬 2019. 3. 24.

메인 스레드와 Handler


1. UI 작업을 메인 스레드에서만 하는 이유

안드로이드에서의 UI 작업을 담당하는 메인 스레드(UI 스레드)는 싱글 스레드 모델을 원칙으로한다. 뷰나 뷰그룹이 여러 스레드에 의해 업데이트될 수 있다면 교착 상태(Deadlock)나 경합상태(Race Condition) 등 여러문제가 발생할 수 있다. 


교착 상태(데드락, Deadlock)

두 스레드 A와 B를 실행하는데 리소스 a와 b가 모두 필요하다고 가정하자. 

- A는 현재 리소스 a를 사용하고 있고, 다음 작업에서 리소스 b가 필요하다.

- B는 현재 리소스 b를 사용하고 있고, 다음 작업에서 리소스 a가 필요하다.

위와 같은 상황이라면, A는 B가 종료될 때까지 기다리고 B는 A가 종료될 때까지 기다려야한다.

즉, A와 B 모두 영원히 끝나지 않을 대기 상태에 빠지는 것이다. 이를 교착 상태(Deadlock)라고 한다.


경합 상태(Race Condition)

두 스레드 A와 B가 TextView에 표시되는 리소스 a를 읽거나 쓰는 작업을 병행으로 한다면, 두 스레드의 작업 순서에 따라 실행 결과가 달라질 수 있다. 이를 경합 상태(Race Condition)라고 한다. 



2. Looper와 Message Queue

스레드는 내부적으로 Looper를 가지며 그 안에는 Message Queue가 포함된다. 


Looper

- Looper는 TLS(Thread Local Storage)에서 관리된다. 

- Looper.prepare()에서 스레드별로 Looper를 생성하며 ThreadLocal<Looper>의 set() 메서드를 통해 새로운 Looper를 추가할 수 있고 get() 메서드를 통해 Looper를 가져올 때 스레드마다 다른 Looper가 반환된다. 


Message

- 스레드에서 처리할 일을 담고 있는 클래스

- 실행 타임 스탬프, 어떤 일을 할지, 어디로 응답을 보낼지, 다음 Message의 링크 등을 가진다.

- Message 객체 생성 시 Message.obtain() 또는 Handler의 obtainMessage() 메서드를 통해 오브젝트 풀에서 가져온다.


Message Queue

- Message를 담는 자료구조이며, 실행 시간 순으로 저장되고 꺼내어진다.

- 나중에 호출되더라도 실행 타임 스탬프가 빠르면 중간에 삽입이 되어야하기 때문에 ArrayBlockingQueue 보다는 LinkedBlockingQueue에 가까운 구조로 구현되어있다.


3. Handler

Message를 MessageQueue에 넣거나, MessageQueue에서 꺼낸 Message를 처리하는 역할을 한다. 

생성자는 아래와 같이 4개의 형태로 존재한다. 

  • Handler()
  • Handler(Handler.Callback callback)
  • Handler(Looper looper)
  • Handler(Looper looper, Handler.Callback callback)


Handler 기본 생성자

기본 생성자는 Handler를 생성한 스레드의 Looper를 사용한다. 보통 UI 작업을 처리할 때 메인 스레드에서 Handler를 기본 생성자를 통해 생성하고 ActivityThread에서 생성된 메인 Looper를 사용한다.


백그라운드 스레드에서의 Handler 기본 생성자

백그라운드 스레드에서 Handler 기본 생성자를 호출할 때는 Looper가 준비되어있는지 확인해야하며, 해당 스레드의 Looper가 준비되어있지 않은 경우 RuntimeException이 발생한다. 

class LooperThread extends Thread {

public Handler handler;

public void run() {

Looper.prepare(); // 해당 스레드의 Looper 준비 (MessageQueue 생성)

handler = new Handler() {

public void handleMessage(Message msg) {

// TODO. Message 처리

}

};

Looper.loop(); // 해당 스레드 종료되지 않도록 무한 루프 돌리기

}

}

LooperThread의 handler에서 sendXxx(), postXxx() 메서드를 사용하면 해당 스레드에서 handleMessage 작업이 이루어진다. 

run() 메서드에서 Looper.loop()를 통해 Message를 기다리며, 해당 액티비티의 onDestroy에서 handler.getLooper().quit()을 호출해야만 루퍼가 돌고있는 스레드가 정상적으로 종료될 수 있다. 


Handler를 생성하는 스레드가 불분명한 경우

같은 메서드가 여러 곳에서 쓰일 때 메서드를 호출하는 스레드가 메인 스레드일 수도 있지만, 백그라운드 스레드일 수도 있다. 이 경우 Handler를 기본 생성자를 통해 생성 후 UI를 업데이트 한다면, 백그라운드 스레드에서 UI 업데이트 작업이 요청될 수 있고 그 경우 당연히 CalledFromWrongThreadException이 발생한다. 

이때 메인 Looper와 연결시켜주기 위해 Handler의 세 번째 생성자를 사용하면 된다.

new Handler(Looper.getMainLooper()).post(new Runnable() {

public void run() {

// UI 업데이트 작업 수행

}

});

AsyncTask에서도 내부적으로 Handler를 이용하여 onPostExecute() 메서드를 통해 UI를 업데이트한다.



4. Handler의 활용


1. 백그라운드 스레드에서 UI 갱신

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

예를들어 Activity의 onCreate() 메서드에서 소프트 키보드를 띄우거나 ListView의 setSelection() 메서드를 호출하는 작업은 잘 동작하지 않기 때문에 MessageQueue에 넣어 현재 작업이 끝난 이후 처리해야하는데 이때 Handler가 사용될 수 있다.

3. 반복적인 UI 갱신

현재시간을 갱신해야하는 Clock 위젯에 사용될 수 있다.

4. 시간 제한, 타이머

안드로이드 내부적으로 ANR을 처리하는 로직에도 사용된다.

아래 코드는 백 키를 두 번 연속 눌렀을 때만 액티비티를 종료시키는 로직이다.

boolean isBackPressed = false;


@Override

public void onBackPressed() {

if(isBackPressed) { 

// 백 키가 5초 이내에 한 번 눌린 적이 있는 경우 

super.onBackPressed(); // 액티비티 종료

} else { 

// 백 키가 처음 눌린 경우

makeToast(R.string.backpressed_message); // "종료하려면 뒤로가기 키를 한번 더 누르세요"

isBackPressed = true;

timerHandler.postDelayed(timerTask, 5000); // 5초 후 isBackPressed 다시 false로 되돌리기

}

}


private final Runnable timerTask = new Runnable() {

@Override

public void run() {

isPackPressed = false;

}

}

5. 안드로이드 프레임워크 내부에서 쓰이는 Handler

  • 컴포넌트 생명주기 Message는 모두 ActivityThread가 내부 클래스로 가지는 Handler를 거친다.
  • Activity의 runOnUiThread() 메서드에서 Handler 멤버 변수가 사용된다.
  • ViewRootImpl에서 화면을 그릴 때 Message를 Choreographer에 위임하는데, 여기서도 내부적으로 Handler를 상속한 FrameHandler가 사용된다.





댓글