[안드로이드] 쓰레드와 핸들러

|

액티비티 ANR

개념

ANR(Application Not Responding) 은 특정 로직을 처리하는데 상당한 시간이 걸려 사용자 이벤트에 5초 이내에 반응하지 못할 경우 시스템에서 액티비티를 강제로 종료하는 현상을 말한다. 보통 네트워킹 때문에 발생하는 문제로 서버로부터 정보를 받거나 비동기적으로 처리해주어야 하는 작업에 대해 ANR은 반드시 신경써줘야 하는 문제이다.

해결법

안드로이드에선 2가지 종류의 쓰레드(Thread)가 존재하는데 UI쓰레드(메인쓰레드) 라고 불리는 시스템에서 발생시킨 쓰레드와 개발자가 자체적으로 만든 쓰레드인 개발자 쓰레드 가 있다.

개발자 쓰레드를 생성하여 작업이 오래 걸리는 로직을 처리하게 되면 UI쓰레드에서 사용자 이벤트에 바로 반응하는 액티비티를 만들 수 있다. 쓰레드를 통해 작업을 분리시키는 개념으로 자바의 Thread API를 그대로 사용한다.

Thread

Thread 클래스 상속

기본적인 자바 개념으로 쓰레드를 만들기 위해선 Thread 클래스를 상속하는 방법이 있다.

class MyThread extends Thread {
    public void run() {
        // 작업수행
    }
}

MyThread thread = new MyThread();
thread.start();

run() 메소드 안의 작업들이 하나의 실행흐름(쓰레드)으로 이루어지며 해당 작업이 끝나면 쓰레드도 종료된다.

Runnable 인터페이스 구현

Runnable 인터페이스를 구현하여 Thread 객체의 매개변수로 주는 방법도 있다.

class MyThread implements Runnable {
    public void run() {
        // 작업수행
    }
}

MyThread runnable = new MyThread();
Thread thread = new Thread(runnable);
thread.start();

이렇게 시작된 쓰레드를 제어하는 메소드로는 sleep(), wait(), notify() 등이 있지만 구체적으로 다루진 않겠다.

Handler

개념

개발자 쓰레드에서 작업로직이 수행되는데 이런 작업을 수행하면서 화면에 표시를 하고 싶을 때가 대부분이다. 이런 경우에 어떻게 처리를 할 수 있을까? ANR은 쓰레드로 해결이 되지만 개발자 쓰레드에서 UI쓰레드의 뷰 객체를 건드리게 되면 런타임 에러가 발생한다. 따라서 핸들러라는 클래스에 의뢰를 하여 UI쓰레드에 표시를 하는 방법을 사용해야 한다.

작업 의뢰(post)

개발자 쓰레드에서 핸들러에게 뷰에 대한 작업을 의뢰하는 방법이다. 먼저 Runnable 인터페이스를 구현하는 클래스를 정의해야 한다. 이 때 해당 클래스의 run() 메소드에 뷰에 대한 작업이 포함되어 있다.

class UIUpdate implements Runnable {
    @Override
    public void run() {
        textView.setText("sum: " + sum);
    }
}

이제 핸들러를 사용하기 위해 객체를 생성하자.

Handler handler = new Handler();

마지막으로 개발자 쓰레드를 정의하며 UI쓰레드에 속한 UIUpdate의 객체를 핸들러의 매개변수로 넘겨주는 작업을 해주어야 한다.

class MyThread implements Runnable {
    public void run() {
        for(int i=0; i<100; i++){
            sum += i;
            handler.post(new UIUpdate());
            try{
                Thread.sleep(1000);
            } catch(InterruptedException e){
                
            }
        }
    }
}

위와같이 post() 메소드를 통해서 UI쓰레드에게 뷰에 대한 작업을 의뢰할 수 있으며 의뢰를 한 순간에 post() 메소드의 매개변수로 넘겨준 Runnable 객체의 run() 메소드를 자동 호출하게 된다.

작업 의뢰(sendMessage)

post() 메소드 이외에도 sendMessage() 메소드를 통해 작업을 처리할 수 있으며 Message 객체를 매개변수로 넘겨 UI쓰레드에 개발자 쓰레드의 데이터를 전달할 수 있다. Message 객체에선 4가지 변수를 정의할 수 있는데 다음과 같다.

  • what : int형 변수로 요청을 구분하기 위해 사용한다.
  • obj : UI쓰레드에 넘길 데이터로 Object 타입의 변수이다.
  • arg1, arg2 : UI쓰레드에 넘길 데이터로 int형 변수이며 간단한 숫자일 경우 사용한다.

sendMessage()로 개발자 쓰레드의 요청을 처리하는 것은 post() 보다는 살짝 복잡하다. 먼저 어떻게 동작하는지 살펴보자.

class MyThread extends Thread {
    public void run() {
        try {
            int count = 10;
            while(loopFlag) {
                Thread.sleep(1000);
                if(isRun) {
                    count--;
                    Message message = new Message();
                    message.what = 1;
                    message.arg1 = count;
                    handler.sendMessage(message);
                    if(count==0) {
                        message = new Message();
                        message.what = 2;
                        message.obj = "Finish!!";
                        handler.sendMessage(message);
                        loopFlag = false;
                    }
                }
            } 
        } catch(Exception e){}
    }
}

위 코드를 보게 되면 Message 객체를 생성하면서 what 값을 1,2로 지정한 것을 볼 수 있다. 첫번째 sendMessage()에선 count값을 넘기기 위해 arg1을 사용하였고 두번째에선 문자열을 넘기기 위해 obj 값을 사용하였다.

이렇게 sendMessage()의 요청을 처리하기 위해선 Handler 클래스의 서브 클래스를 정의해야 한다.

Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        if(msg.what == 1) {
            textView.setText(String.valueOf(msg.arg1));
        } else if(msg.what == 2) {
            textView.setText((String)msg.obj);
        }
    }
}

위처럼 서브클래스를 정의한 뒤 그 안에 handleMessage() 메소드를 오버라이딩하여 UI쓰레드에서의 동작을 정의하게 되면 개발자 쓰레드에서 sendMessage()를 호출하는 순간 UI쓰레드가 자동으로 handleMessage()를 호출하게 되며 매개변수도 함께 전달된다. 코드를 보면 알 수 있듯이 what 값을 통해 어떤 요청인지 구분하여 UI쓰레드에서의 동작을 설정하고 있다.

Looper

개념

자 이제 쓰레드와 핸들러를 배웠으니 루퍼까지 포함해서 어떻게 동작하는지 살펴보자. 개발자 쓰레드에서 핸들러를 통해 sendMessage()를 호출하면 message queue에 해당 메시지를 담게 된다. 즉, 핸들러는 메시지를 담는 역할을 하는 것이다. 이렇게 담은 메시지에 대해서 핸들링을 하기 위해선 handleMessage()를 호출해야 하는데 이 역할을 바로 루퍼가 한다. 루퍼는 message queue에 메시지가 담기는 것을 감지하며 해당 메시지를 추출하여 handleMessage()를 호출하는 것이다. 이렇게 개발자 쓰레드와 UI 쓰레드가 상호작용 할 수 있는 것은 내부적으로 루퍼가 있기 때문에 가능하다. 그러나 UI 쓰레드가 아닌 개발자 쓰레드간의 상호작용을 하려면 어떻게 해야될까? 원래 UI 쓰레드와의 상호작용에선 시스템이 기본적으로 루퍼를 제공해주지만 개발자 쓰레드간의 상호작용은 루퍼를 개발자가 정의해야 한다. 이제 그 방법을 알아보도록 하자.

개발자 쓰레드 간 상호작용

개발자 쓰레드인 OneThreadTwoThread가 있다고 하자. 이 때 TwoThread는 특정 업무를 수행하고 수행결과를 핸들러의 sendMessage()에 전달하여 OneThreadhandleMessage()가 호출되게 된다. TwoThread에선 랜덤한 숫자를 발생시켜 짝수/홀수에 따라 처리를 하는 간단한 로직이다.

class TwoThread extends Thread {
    @Override
    public void run() {
        Random random = new Random();
        for(int i=0; i<10; i++) {
            int data = random.nextInt(10);
            Message message = new Message();
            if(data % 2 == 0) {
                message.what = 0;
            } else {
                message.what = 1;
            }
            message.arg1 = data;
            oneThread.oneHandler.sendMessage(message);
        }
    }
}

위와 같이 작성된 코드를 통해 oneThreadhandleMessage()를 호출하면 아래와 같이 처리한다.

class OneThread extends Thread {
    @Override
    public void run() {
        Looper.prepare();
        oneHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                int data = msg.arg1;
                if(msg.what == 0) {
                    Log.d("kkang", "even data : " + data);
                } else if(msg.what == 1) {
                    Log.d("kkang", "odd data : " + data);
                }
            }
        };
        Looper.loop();
    }
}

Looper.prepare()를 통해서 message queue를 준비하며 메시지가 담기는 것을 감지하며 처리하는 것은 Looper.loop()를 통해 하게 된다. 주의할 점은 이 메소드가 무한루프를 도는 방식이기 때문에 message queue에 담기는 것을 감지할 필요가 없는 상황이라면 반드시 종료해주어야 한다. onDestroy()에서 종료한다면 아래와 같이 종료할 수 있다.

@Override
protected void onDestroy() {
    super.onDestroy();
    oneThread.oneHandler.getLooper().quit();
}

출처

  • 깡샘의 안드로이드 프로그래밍 16장 : 쓰레드와 핸들러
  • https://academy.realm.io/kr/posts/android-thread-looper-handler/