About HandlerThread 클래스

woga·2022년 3월 12일
0

Android 공부

목록 보기
20/49
post-thumbnail

백그라운드 스레드를 활용하면 앱의 성능을 향상하는 데 많은 도움이 된다. 그 중 구조가 단순한 HandlerThread를 살펴보자.

HandlerThread 클래스

HandlerThread는 Thread를 상속하고, 내부에서 Looper.prepare()Looper.loop()를 실행하는 Looper 스레드이다. 클래스명 때문에 Handler를 가진 스레드라고 생각할 수 있지만 그렇지 않다.
HandlerThread는 Looper를 가진 스레드이면서 Handler에서 사용하기 위한 스레드라고 보는게 맞다.


Handler를 Looper에 연결하는 방식에는 2가지가 있는데, 이 둘에는 미묘한 차이가 있다.
Handler를 스레드 안에 두고 사용하는 방식이 있고, 스레드에서 Looper를 시작하고 스레드 외부에서 Handler를 생성하는 방식이 있다.

두 번째 방식을 미리 만든 것이 HandlerThread이다. HandlerThread는 내부적으로 prepare(), loop()를 실행하는 것 외에 별다른 내용이 없다.

  • 기본 샘플
private HandlerThread handlerThread;

public Processor() {
	handlerThread = new HandlerThread("Message Thread");
    handlerThread.start();
}

public void process() {
	...
    new Handler(handlerThread.getLooper()).post(new Runnable() {
    	
        @Ovveride
        pubilc void run() {
        	...
        }
    });
}

new Handler(handlerThread.getLooper())를 보면 Handler 생성자에 HandlerThread의 Looper를 전달한다. 이후에는 이 핸들러에서 보낸 Message가 HandlerThread에서 생성한 스레드에서 처리된다.

HandlerThread 프레임워크 소스

  • public class HandlerThread extends Thread
Looper mLooper;

	@Override
    public void run() {
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
    }
    
    public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        
        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }
    
    public boolean quit() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quit();
            return true;
        }
        return false;
    }

run() 메서드에서 Looper.prepare()와 Looper.loop()를 실행하면 될 것 같지만, 추가적인 작업이 더 있다.
mLooper = Looper.myLooper();에서 mLooper에 Looper.myLooper()를 대입하는 것이다.
그런데 getLooper() 메서드나 quit() 메서드에서 mLooper를 직접적으로 사용하지 않는다. 다시 말해서 getLooper()에서 바로 return mLooper와 같이 단 한 줄로 끝나지 않는다. quit()에서도 mLooper는 사용되지 않고 getLooper() 메서드를 거친다.

getLooper() 메서드는 이렇게 내부에서도 사용되기도 하지만, public 메서드로 외부에서도 사용된다. 위 기본 샘플에서 new Handler(handlerthread.getLooper())로 핸들러 생성자에 전달돼서 핸들러에 핸들러스레드의 Looper를 연결시킨다.

if(!isAlive())를 보면 Thread를 상속한 HandlerThread에서 start()를 호출했는지 체크한다.
isAlive()는 스레드가 start() 메서드로 시작되고 아직 종료되지 않았을 때 true를 리턴한다. HandlerThread를 사용할 때 start()를 빠뜨리는 실수를 하기 쉬운데 getLooper를 호출하기 전에 반드시 start()를 호출해야 한다.

while (isAlive() && mLooper == null)를 보면 mLooper가 null인지를 체크한다.

그 이유는 위 기본샘플을 보면,
handlerThread.start()로 start를 호출하고서 스레드에서 run() 메서드가 실행되는 시점은 정확히 알 수 없다. 따라서 new Handler(handlerthread.getLooper())에서 getLooper()는 run 메서드 내에서 mLooper를 대입하는 시점까지 대기하기 위해서 while 문에서 mLooper가 null인지를 계속해서 체크한다.

mLooper 멤버 변수는 대입되고 나서 getLooper() 메서드 외에는 다른 곳에 쓰이지 않는 것을 볼 수 있다.

그리고나서 wait() 메서드로 대기한다. HandleThread 프레임워크 속 run 함수 내에 mLooper 대입 이후에notifyAll();를 실행해서 대기하는 스레드를 깨운다.

순차 작업에 HandlerThread 적용

HandlerThread가 필요한 곳은 어디일까? 바로 UI와 관련없지만 단일 스레드에서 순차적인 작업이 필요할 때이다.

순차적인 작업은 허니콤 이후부터 디폴트로 SERIAL_EXECUTOR를 사용해서 순차적인 스레드 작업을 지원한다.

안드로이드 프레임워크에서는 IntentService와 HandlerThread를 내부적으로 사용한다.

예를 들면, 아래 캡쳐 사진 속 항목의 즐겨찾기가 있고 선택과 해제 여부를 실시간으로 DB에 반영하는 요구사항이 있다고 보자.

UI를 블로킹하지 않도록 별도 스레드에서 DB에 반영하기로 한다. 사용자가 선택/해제를 마구 바꾸기도 하기 때문에 이에 대비해 세심하게 처리할 필요가 있다.

Q. 만일 체크 상태가 바뀔 때마다 스레드를 생성하거나 스레드가 스레드를 가져다가 DB에 반영하면 어떤 일이 벌어질까?

스레드가 start()를 실행한 순서대로 실행되지 않기 때문에 선택->해제->선택을 했지만, DB에 반영할 때는 선택->선택->해제순으로 반영하여 최종 결과가 잘못될 가능성이 있다.

=> 즉 실행순서를 순차적으로 맞추어야 한다. 바로 HandlerThread가 필요한 지점이다.
HandlerThread를 사용하면 구조보다 Message에 더 집중할 수 있다.

private Handler favoriteHandler;
    private HandlerThread handlerThread;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        handlerThread = new HandlerThread("Favorite Processing Thread");
        handlerThread.start(); // 1
        favoriteHandler = new Handler(handlerThread.getLooper()) { // 2
            @Override
            public void handleMessage(Message msg) { // 이 함수 전체 내용이 3
                MessageFavorite messageFavorite = (MessageFavorite) msg.obj;
                FavoriteDao.updateMessageFavorite(messageFavorite.id, messageFavorite.favorite);
            }
        };
    }
    
    private class MessageAdapter extends ArrayAdapter<Message> {
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            holder.favorite.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    boolean checked = ((CheckBox) view).isChecked();
                    Message message = favoriteHandler.obtainMessage();
                    message.obj = new MessageFavorite(item.id, checked);
                    favoriteHandler.sendEmptyMessage(message); // 4
                }
            });
        }
    }
    
    @Override
    protected void onDestroy() {
        handlerThread.quit(); // 5
        super.onDestroy();
    }

1) HandlerThread를 시작한다. HandlerThread는 Thread를 상속한 것이라는 점을 기억하자.
2) HandlerThread에서 만든 Looper를 Handler 생성자에 전달한다.
4) 체크박스를 선택/해제할 떄마다 Handler에 Message를 보낸다.
3) Message를 받아서 DB에 반영한다.
5) HandlerThread의 quit() 메서드는 내부에서 Looper.quit()을 실행해서 Looper를 종료한다.

profile
와니와니와니와니 당근당근

0개의 댓글