핸들러, 루퍼, 메시지 큐에 대해 알아보기

Mendel·2023년 11월 5일
0

안드로이드

목록 보기
3/7

우선, 이 주제에 대해 얘기하기 전에 안드로이드에서 왜 메인스레드를 ui스레드라고도 부르는지부터 알아야 한다.
왜 ui스레드가 곧 메인스레드일까? UI작업은 그 외 스레드에서 할 수 없는 것일까? 정답은 '맞다' 이다.
만약, ui를 조작하는 스레드가 한 개 보다 많다고 생각해보자. 우린 textView의 text를 1로 바꿔달라는 요청을 보냈는데, 다른 작업스레드가 또 덮어서 다른 글자를 쓸 수도 있다는 것이다. 그 외에도 교착상태나 경합 상태 등 여러 문제가 발생 가능하다고 함.
때문에, 안드로이드에서는 ui 작업은 메인스레드에서만 가능하게 막았다. (그 외에 안드로이드 컴포넌트의 생명주기 메소드들도 메인스레드에서 실행된다.)

그렇다면, 안드로이드에서 메인 스레드의 시작 지점은 어디일까?

프로그램의 시작인 main 함수가 있는 ActivityThread 클래스의 main 메소드이다. 여기서 ActivityThread는 액티비티나 스레드와는 연관이 없는 클래스다. 단순히 '활동'의 시작이라는 의미를 담은 클래스명이라고 한다.
image
이 ActivityThread의 main메소드 안에서 메인루퍼를 준비하고, 이 메인루퍼의 loop() 메소드를 호출해서 main함수의 종료를 막는다. 이 메인 함수가 종료되면, 프로세스가 종료되는 것이기 때문이다.
loop() 메소드 안에서는 메인 루퍼에 들어오는 UI메시지들을 처리하는 무한 반복문이 있다.

다만, API 30 이후부터는 prepareMainLooper()는 deprecated 되고 안드로이드 환경에서 MainLooper를 직접 생성해준다고 함. 뭐 내부적으로 코드가 좀 바뀐듯. 그래도 근본은 비슷할 것 같다

Looper 클래스

Looper는 일단 스레드별로 소유하고 있는 것이다. TLS라는 스레드 로컬 저장소에 각 스레드마다 대응되는 루퍼를 보관 하고 있다.
각 스레드가 가진 루퍼를 생성하고 싶다면, Looper.prepare() 를 호출하면 된다. 특이하게도 메인스레드가 가진 루퍼인 메인루퍼를 생성하고 싶다면, Looper.prepareMainLooper()를 호출해야 한다. 다만, 이 행위는 이미 ActivityThread의 main메소드에서 진행되므로 우리가 메인루퍼를 준비해줄 필요는 없다.

  • 메인 루퍼는 어디서든 얻어올 수 있다.
    Looper.getMainLooper()를 사용하면, 어떤스레드에서든 자신의 루퍼가 아닌 메인루퍼에도 접근할 수 있다.

  • 각 루퍼 별로 메시지큐를 가진다

  • Looper.loop() 메소드의 내용

    • image
    • 여기서 알 수 있듯이, 해당 스레드가 가진 루퍼를 호출하는 메소드인 myLooper()를 통해 이 스레드가 가진 루퍼를 얻는다. 그 다음, 그 루퍼에서 메시지큐를 얻고 그 메시지큐의 next() 메소드 반환값이 널이 나올 때까지 반복문을 돈다. 여기서 주의해야 할 점은 메시지가 없어서 next() 반환이 널이 나오는 것이 아니라는 것이다. Looper의 quit() 혹은 quitSafely()메소드를 호출해야 그제서야 next() 반환이 널이 나온다고 한다.
    • image
      • 여기서 알 수 있듯이, TLS라는 것으로부터 현재 스레드에 맞는 루퍼를 얻어오는 함수가 myLooper()임.

여기서 msg.target의 타입이 바로 Handler다.
즉, 메시지는 자신을 처리해줄 핸들러를 멤버변수로 갖고 있다.

MessageQueue

메시지큐는 메시지를 담는 자료구조임. LinkedBlockingQueue 구조와 가깝다고 한다. 때문에, 배열 기반의 큐가 아니라서 메시지큐에는 크기 제한이 없고, 삽입 속도가 빠르다고 함. 대신 배열구조의 큐보다 중간 인덱스에 접근하는데 시간이 오래 걸린다. 대신, 중간 삽입에 용이하다는 장점은 있음(이것 때문에 사용한다).
메시지큐에는 메시지가 실행 타임스탬프 순으로 삽입되고 연결된다고 함. 즉, 실행 시간이 빠른 것부터 꺼낸다.

Message

메시지 클래스 먼저 알아보자.
Message는 우선 Parcelable 구현체임.
또한, int what, int arg1, int arg2, Object obj, Messenger replyTo 이렇게 총 다섯 개의 퍼블릭 변수가 있다.
그 외에도 패키지 프라이빗 변수(동일 패키지에 존재하는 클래스들은 이 변수에 접근 가능하다. 참고: android.os 패키지 아래에 Looper, Message, MessageQueue, Handler가 존재한다고 함)도 존재한다. 프라이빗 변수들은 아래와 같은 다섯 개이다.

  • long when, Bundle data, Handler target, Runnable callback, Message next
    when 변수는 타임스탬프라고 함. 만약 나중에 삽입된다고 해도 이 타임스탬프가 앞서면 큐 중간에 삽입된다고 한다. 그래서 메시지큐가 링크드 기반 구조의 큐로 설계됐다고 한다.

  • 메시지 객체를 얻을 때는, Message.obtain()이나 Handler의 obtainMessage() 메소드를 호출하라고 함. 이유는 메시지 객체들을 오브젝트풀에다가 최대 50개 저장한다고 함. 즉, 불필요하게 너무 많은 메시지 객체 만들어서 쓰지 말고 계속 재활용하라는 것임. 만약 매번 메시지 객체를 생성하면, 너무 빨리 오브젝트 풀이 50개에 도달해버린다고 함. -> 메모리 낭비다.

Handler

핸들러는 메시지를 메시지 큐에 넣는 기능과 메시지큐에서 메시지를 꺼내서 처리하는 기능을 둘 다 제공한다. 이거 때문에 사람들이 핸들러를 배울 때 혼란이 오는 것 같음.

  • 핸들러의 생성자는 총 네 개이다. 이중 어떤걸 호출하더라도 결국은 매개변수가 가장 많은 Handler(Looper looper, Handler.Callback callback)이 다시 호출된다.
    image
  • 생성자를 보면 알 수 있듯이 핸들러는 루퍼를 멤버변수로 갖는다. 그리고 이는 곧 루퍼가 가진 메시지큐와 핸들러가 연결되어 있다고도 볼 수 있다.
  • Handler의 기본 생성자를 호출하면, 별도로 루퍼를 지정안해준것임. 이는 곧 현재 이 생성자를 호출한 스레드의 루퍼를 핸들러가 멤버변수로 갖겠다는 의미임.
    • 위에서 봤듯이, 메인스레드의 루퍼는 ActivityThread의 main함수에서 준비시켜놨음.
    • 하지만, 작업 스레드들의 루퍼는 우리가 준비를 해줘야함. 만약 작업 스레드에서 Handler 기본 생성자를 호출하려고 하는데, 그 스레드의 루퍼가 준비 되어있지 않다? 바로 런타임 Exception 난다. (추가 정보: prepare 메소드는 단순히 메시지큐를 생성하는 작업을 한다고 함.)

만약 작업스레드에서 호출될 수 있는 위험이 있는 메소드가 있는데 그 안에서 UI 변경 작업 코드가 들어가 있어야 한다고 치자. 이때, Handler를 사용할 수 있는데, 메인 루퍼와 연결된 핸들러를 생성해서 거기에 메시지 내용을 담아주면 된다. 그러면, 비록 이 메소드가 작업스레드에서 호출된다고 하더라도, 메인 루퍼와 연결된 핸들러 객체를 생성해서 UI 변경 작업 내용이 담긴 메시지를 보내면 된다.

핸들러의 메시지를 담아서 메시지큐에 전송하는 방법) 핸들러의 전송 함수들 알아보기

send- 시작하는 함수들은 메시지 객체를 보내는 방법임. post-로 시작하는 함수들은 Runnable 객체를 전달하는 방법임.
-Delayed 가 붙은 함수들로 보내는 함수들은 내부적으로 -AtTime이 붙은 함수를 호출하는데, 이때 -AtTime 함수들이 공통적으로 갖는 long updateMills 라는 매개변수에 넘겨줄 인자로 현재시간 + 딜레이시간의 타임스탬프를 넘겨준다고 함.

핸들러가 메시지를 처리하는 방법) Handler의 dispatchMessage 메소드 내부 뜯어보기

메시지에 담긴 target 멤버변수 핸들러를 사용해서 distpatchMessage로 처리한다.
image
메시지가 Runnable 타입의 callback 멤버변수를 소유하고 있다면, 해당 러너블 멤버변수를 실행한다.
없다면, 핸들러가 지닌 handleMesage() 메소드를 실행한다. 즉, 타깃 핸들러가 가진 handleMessage함수를 실행해서 메시지를 처리하는 것임.

  • 메시지가 post 등에 의해 메시지큐로 전달된 적이있어서 Runnable 타입의 callback 객체가 널이 아니면 그걸 실행해서 우선적으로 처리
  • 만약 메시지의 타깃 핸들러가 콜백 객체를 생성자에서 받은 핸들러라서 mCallback 멤버변수가 널이 아니면 이걸 실행해서 처리
  • 위에 두 개 모두 아니라면 자신이 가진 handleMessage 메소드를 실행한다

참고: distpatchMessage도 퍼블릭 함수라고 한다. 만약 send- 혹은 post- 이런 함수들로 메시지를 전송하는 것이 아니라 dispatchMessage로 직접 처리한다면 메시지큐를 거치지 않는다.

그래서 message를 처리하는 target 핸들러는 누구인가?

=> 여기서 주의할 것은 딱 하나, 메시지를 전송하는 역할을 맡은 핸들러가 send- 함수를 호출하느냐 아니면 post-함수를 호출하느냐이다.
만약, send- 함수를 호출해서 어디선가 obtainMessage()를 호출해서 얻어온 메시지에 내용을 담아서 전송했다면 target은 이 메시지 객체를 얻어온 핸들러로 지정된다. 아래와 같은 이유로.
image

만약 send로 메시지를 보낸 것이 아니라 postDelayed와 같은 post- 등의 함수를 사용해서 Runnable 객체를 메시지에 담아서 메시지큐에 보낸 것이였다면, 그 postDelayed 함수를 호출한 핸들러가 메시지의 타깃으로 들어간다.
image
아래 사진을 보면 Message.obtain()에서는 target 지정안해줌
image
이 Message.obtain()을 호출해서 넣는 sendMessageDelayed메소드를 타고타고 내려가면 아래 함수를 호출한다.
image

참고 링크

참고 도서

  • 안드로이드 프로그래밍 NextStep
  • 실무에 바로 적용하는 안드로이드 프로그래밍
profile
이것저것(안드로이드, 백엔드, AI, 인프라 등) 공부합니다

0개의 댓글