[android] 루퍼와 핸들러의 이해 #2

개요top

이전 글에서는 기본적인 메세지큐 시스템의 의미와 원리를 알아보고 루퍼가 어떤 식으로 작동되는지에 대한 기본적인 내용을 살펴봤습니다.
이 기본적인 내용을 바탕으로 더 깊은 메세지큐의 처리로 들어가보죠.

등장인물의 상세한 관계top

이 모델에서 등장인물들의 관계를 정리해보죠. 쓰레드와 루퍼부터 살펴볼까요.

  1. 우선 쓰레드가 존재합니다. 평범한 자바쓰레드입니다.
  2. 쓰레드가 시작될때 해당 쓰레드를 루퍼로 정의합니다(Looper.prepare())
  3. 루퍼는 Loop.loop() 를 통해 해당 쓰레드를 차단적으로 지속시키면서 계속 작동합니다.

위의 과정에서 쓰레드와 루퍼와의 공생관계가 성립합니다. 루퍼는 루퍼자신이 활성화되자마자 메세지큐를 생성하게 됩니다.
이 메세지큐를 얻는 방법조차 Looper.myQueue() 이기 때문에 현재 루퍼상태에서 정적메소드를 얻어야해 굉장히 불편합니다. 메세지큐를 직접 참조해도 직접 메세지를 쓰거나 빼는건 불가능합니다. 단지 IdleHandler를 등록하여 이벤트를 수신할 수 있을 뿐입니다. 우선 이 시점에서 메세지큐에 어떻게 메세지를 추가할 수 있는가라는 문제가 발생합니다.

루퍼가 메세지큐를 소유한다는 건 그렇다치고 쓰레드를 종결시키려면 해당 루퍼를 찾아서 quit()를 하면 되는데, 문제는 위의 과정에서 루퍼의 레퍼런스가 생겨나지 않는다는 점입니다.
어떻게 해당 루퍼를 찾아서 quit()메소드를 호출할 수 있을까라는 문제도 생겨납니다.

결국 이러한 모든 해답이 해당 루퍼의 핸들러를 생성하는 것입니다. 따라서 핸들러의 역할은 다음과 같이 여러개를 갖고 있습니다.

  1. 메세지를 수신하기
  2. 메세지를 생성하는 팩토리
  3. 메세지를 메세지큐에 삽입하기
  4. 루퍼를 찾아내기

워낙 역할이 많다보니 말그대로 핸들러라는 이름 외에 더 특화된 이름을 부여하기가 어렵습니다. 구체적으로 파악하자면 루퍼와 루퍼의 메세지큐에 대한 핸들러이자 메세지수신자라는 의미입니다.
따라서 루퍼는 생성과 설정부분을 공부하면 거의 끝이고, 이번 포스팅에서는 거의 핸들러를 공부하게 되겠죠.

핸들러 생성하기top

그럼 우선 핸들러를 만들어야합니다. 핸들러는 반드시 루퍼와 연결되어야합니다. 그럼 생성시 직간접적으로 특정 루퍼를 참조해야할 것입니다. 우선 암묵적인 핸들러의 생성자를 살펴보죠.

  1. 암묵적 생성자는 현재 루퍼와 바인딩됩니다.
  2. 현재 루퍼란 현재 쓰레드와 연결된 루퍼입니다.
  3. 이 현재쓰레드와 연결된 루퍼를 얻어내는 변태같은 방법은 Looper.myLooper() 입니다. 내부에서 이를 수행했다고 봐야겠죠.
new Handler();
new Handler(Handler.Callback);

핸들러의 메세지 수신자는 별도의 인터페이스로 정의되어있어 핸들러 본체에 정의하지 않고 별도의 구상체를 만들어 전달해도 됩니다.

이에 반해 명시적인 핸들러 생성자에서는 루퍼를 직접 전달합니다.

new Handler(Looper);
new Handler(Looper, Handler.Callback);

이런 점에서 암묵적 생성자는 언제나 명시적 생성자로 대체될 수 있습니다.

Handler h1 = new Handler();
Handler h2 = new Handler(Looper.myLooper());

h1와 h2는 모두 동일한 루퍼를 참조로 생성됩니다.

하나의 루퍼에 다수의 핸들러top

핸들러가 루퍼를 안고 태어나야하는 것은 의존성 관계로 보면 핸들러가 루퍼에 의존성이 있다는 것입니다. 반대로 루퍼는 핸들러에 의존성은 없습니다. 단지 내부적인 관계로 메세지큐를 핸들러가 직접 참조할 수 있는 루트를 열어줄 뿐이죠.

그러한 이유로 루퍼와 핸들러 사이에는 1:N관계가 성립합니다. 그럼 하나의 루퍼에 핸들러A와 핸들러B가 바인딩된 경우를 상정해보죠.

  1. 핸들러A에서 작성된 메세지는 메세지 내부에 메세지의 핸들러가 핸들러A라는 마킹이 들어가있습니다.
  2. 마찬가지로 핸들러B에서 작성된 메세지도 핸들러B마킹이 들어가 있죠.
  3. 루퍼는 메세지큐에서 메세지를 꺼내서 리스너에게 전달할때 각 메세지의 핸들러 마킹을 확인하고 해당 핸들러에 지정된 리스너에게 보내줍니다.

즉 루퍼 하나에 여러개의 핸들러를 쓰는 이유는 리스너를 채널별로 관리하기 위해서입니다. 메세지를 그룹핑하자고 루퍼를 여러개 만들 필요는 없는거죠. 단지 루퍼하나에 메세지 채널별로 핸들러를 추가해가면 됩니다.
약간은 바보같지만(핸들러에 여러개의 리스너를 붙이면 될 일을..=.=) 일단 루퍼를 참조하는 다수의 핸들러를 만들 수 있다는 것과 그 의미를 기억해두는 정도로 정리합니다.

메세지top

우선 메세지큐에 들어갈 메세지를 생성해야 합니다. 메세지는 기본적으로 그냥 생성할 수 있습니다.

Message m = new Message();

이렇게 생성한 인스턴스는 재활용되는 풀링 등을 직접 구현하고 관리해야할 것입니다. 하지만 안드로이드는 이를 이미 해결해두었습니다. 전역레벨에서 메세지풀링이 내장되어있는데 이를 활용하려면 Message의 정적메소드를 사용하거나 핸들러의 메소드를 사용하여 메세지를 생성하면 됩니다.

  1. 결국 핸들러는 루퍼와 연결되어있고
  2. 메세지의 라이프사이클과 메세지풀링에 관여해야할 직접적인 객체는 루퍼이기 때문에
  3. 메세지 생성시 연결시켜주는 것이죠.

전역 메세지 풀링에서 중요하게 알아야 할 사항은 메세지가 재활용된다는 점입니다. 생성된 메세지는 메세지큐에 들어갔다가 리스너에게 전달된 후 다시 풀로 돌아와 재활용됩니다. 따라서 리스너에서 함부로 메세지를 참조로 잡아서 재활용하거나 별도의 메세지 풀링을 구축하면 안됩니다.

실제 생성방법을 보죠. 우선 핸들러로부터 메세지를 생성하는 경우입니다. 머 아무래도 인자하나를 생략할 수 있으니 가장 많이 사용하는 형태이기도 합니다.

Message m = handler.obtainMessage()
Message m = handler.obtainMessage(int what)
Message m = handler.obtainMessage(int what, int arg1, int arg2)
Message m = handler.obtainMessage(int what, int arg1, int arg2, Object obj)

굳이 Message로부터 생성하고 싶다면 다음과 같이 할 수 있습니다.

Message m = Message.obtain(Handler h);
Message m = Message.obtain(Handler h, int what);
Message m = Message.obtain(Handler h, int what, Object obj);
Message m = Message.obtain(Handler h, int what, int arg1, int arg2);
Message m = Message.obtain(Handler h, int what, int arg1, int arg2, Object obj);

헌데 좀 특이한 것도 있습니다.

Message m = Message.obtain(Handler h, Runnable callback);
Message m = Message.obtain(Message orig);
m.copyFrom(Message o);
  1. Runnable을 주는 경우는 메세지가 데이터타입이 아니라 태스크 타입이라는 뜻입니다. 태스크타입은 리스너에게 전달되지 않고 루퍼가 큐에서 꺼내는 순간 곧장 실행해버립니다.
    이 태스크 타입의 메세지는 핸들러에서는 작성할 수 없습니다.
  2. 다른 메세지를 인자로 메세지를 생성하는 경우는 해당 메세지의 복사본을 생성하게 됩니다.
  3. 마지막으로 이미 만들어진 메세지에 copyFrom을 이용하면 다른 메세지의 내용을 복사하게 됩니다.

메세지에 관련된 handler를 넣어줄 수는 있지만 필수적이지 않습니다.

  1. 루퍼는 메세지가 삽입되는 순간 어떤 핸들러가 삽입했는지를 마킹하는거지,
  2. 메세지에 들어있는 핸들러를 기반으로 리스너를 고르지는 않습니다.

실제 다음의 코드도 문제없이 각각의 리스너를 찾아갑니다.

public class MainActivity extends AppCompatActivity {

    class LooperThead extends Thread{ //루퍼클래스
        Handler h1, h2;//두개의 핸들러생성
        @Override
        public void run(){
            Looper.prepare();
            //각각의 리스너를 갖게 정의함
            h1 = new Handler(){public void handleMessage(Message m){Log.i("bs","test1");}};
            h2 = new Handler(){public void handleMessage(Message m){Log.i("bs","test2");}};
            Looper.loop();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //루퍼시작
        LooperThead l = new LooperThead();
        l.start(); 

        //잠시대기!  
        try{Thread.sleep(10);}catch(InterruptedException e){}

        //핸들러가 바인딩되지 않은 메세지 생성
        Message m = new Message();
        //강제로 두번째 핸들러를 지정
        m.setTarget(l.h2);

        //첫번째 핸들러에게 보냄
        l.h1.sendMessage(m); //"test1"

        //잠시대기!  
        try{Thread.sleep(10);}catch(InterruptedException e){}

        //두번째 리스너에게보냄 - m을 재활용하는건 원래 나쁜짓!
        l.h2.sendMessage(m); //"test2"
    }
}

즉 메세지에 연결된 핸들러는 메세지 리스너를 결정할 때는 아무런 기능도 하지 않는다는 사실을 알 수 있습니다. 심지어 해당 메세지를 재활용하는 것도 가능하지만, 전역풀링을 이용하는 경우라면 메세지 내용이 손상되므로 하면 안되는 짓인거죠.

메세지큐에 삽입하기top

메세지 생성에 대한 전반적인 내용을 다뤘으므로 이를 메세지큐에 넣어야합니다. 메세지큐에 넣는 유일한 방법은 루퍼에 연결된 핸들러의 메소드를 이용하는 것입니다. 많은 메소드가 제공됩니다만 우선 알아야할 것은 post계열과 send계열입니다.

  1. postXXX메소드는 태스크메세지를 처리합니다. 즉 Runnable을 큐에 넣고 루퍼는 때가 되면 실행하게 됩니다.
  2. sendXXX메소드는 정상적인 데이터메세지를 메세지큐에 넣고 루퍼는 이를 꺼내 리스너에게 전달합니다.

또다른 구분점으로는 접미어의 의미입니다. 보통 메세지는 큐에 들어가는 시점에 타임스탬프가 찍히고 메세지큐는 타임스탬프기준으로 정렬을 하여 루퍼는 해당 메세지를 적절한 우선순위와 시간에 맞춰 꺼내 처리하는 식입니다. 이 시간이나 정렬순서에 관여할 수 있는 방법이 각 메소드의 접미어가 갖는 의미입니다.

  1. xxxAtFrontOfQueue – 큐의 가장 앞에 메세지를 삽입합니다.
  2. xxxAtTime – 지정한 시간으로 설정하여 큐에 삽입합니다.
  3. xxxDelayed – 현재시간으로부터 지정한 시간만큼 뒤로 설정하여 큐에 삽입합니다.

이러한 개념을 염두하고 핸들러의 전송메소드를 살펴보면 이해하기 편리합니다.
워낙 많은 메소드를 제공하고 있으므로 소개는 링크로 대신합니다.

Handler

각 post와 send에는 직접 Message를 생성하지 않고도 간단히 처리해주는 편리한 메소드가 많이 포함되어있습니다. 예를들어 post계열만 살펴보죠.

  1. handler.post(Runnable r); – 큐의 마지막에 태스크를 삽입한다(메세지를 따로 작성하지 않아도 알아서 내부에서 처리해줌)
  2. handler.postAtFrontOfQueue(Runnable r); – 1과 동일하지만 큐의 젤 앞에 삽입한다.
  3. handler.postAtTime(Runnable r, long uptimeMillis); – 지정한 시간으로 큐에 삽입한다.
  4. handler.postAtTime(Runnable r, Object token, long uptimeMillis); – 3과 동일하지만 token을 같이 넘긴다
  5. handler.postDelayed(Runnable r, long delayMillis); – 지정한 시간만큼 딜레이된 시간으로 지정한뒤 큐에 삽입한다.

다시 한 번 짚어보자면 메세지큐에 메세지를 삽입하는건 순서가 중요하지 않습니다. 각 메세지에 설정된 시간이 중요합니다. 왜냐면 삽입될 때마다 시간순으로 메세지를 정렬하고 루퍼는 시간에 맞춰 메세지를 꺼내가려하기 때문입니다. 이를 확인해볼까요. 앞에 제작한 LooperThread를 그대로 활용하니 생략하고 onCreate쪽만 살펴보겠습니다.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    LooperThead l = new LooperThead();
    l.start();
    try {
        Thread.sleep(10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //우선 현재시간을 찍어보고
    Log.i("bs", Long.toString(SystemClock.uptimeMillis()));
    l.h1.post(new Runnable() {
        @Override
        public void run() {
            //루퍼에 의해 실행된 타임을 찍어보자.
            Log.i("bs", Long.toString(SystemClock.uptimeMillis()));
        }
    });
}

결과는 보통

11-06 02:36:00.447 24085-24085/com.bsidesoft.test I/bs: 1506016
11-06 02:36:00.447 24085-24105/com.bsidesoft.test I/bs: 1506018

이런 식으로 2ms이하로 나올 것입니다. 루퍼가 즉시 처리한거라고 봐야할 정도의 오차입니다. 이제 atTime을 이용해보죠.

Log.i("bs", Long.toString(SystemClock.uptimeMillis()));
l.h1.postAtTime(new Runnable() {
    @Override
    public void run() {
        Log.i("bs", Long.toString(SystemClock.uptimeMillis()));
    }
}, SystemClock.uptimeMillis() + 100);

100ms의 차이를 갖게 만들었습니다. 결과는

11-06 02:38:39.257 26454-26454/com.bsidesoft.test I/bs: 1664828
11-06 02:38:39.367 26454-26474/com.bsidesoft.test I/bs: 1664930

100ms에 가까운 차이로 실행되는 것을 볼 수 있습니다. Delayed등도 대동소이하므로 생략합니다.

결론top

저 번 글에 이어 보다 구체적으로 메세지와 메세지큐, 이들과 관련된 핸들러를 살펴봤습니다. 마지막으로 다음 글에서는 메세지큐의 관찰, 쓰레드 우선순위조정, 메인루퍼와의 관계 등을 정리해봅니다.