안녕하세요 :) 안드로이드의 Thread와 Handler의 동작구조에 대해 이해하기위해, Thread, Looper, MessageQueue를 살펴보려고 합니다. 이 글에선 각 요소들의 Android Framework 소스를 확인하고, 그들의 동작방식을 설명합니다. 기본으로 강조되는 내용이므로, 깊은 이해를 도모하시길 바랍니다. 앗 그리고 분량이 많아 쪼개버린, Handler와 Message, Runnable을 다루는 다음 포스팅도 참고해주세요 ! 그럼 오늘도 화이팅 입니다 🌿
안드로이드의 메인스레드는 컴포넌트 생명주기 메서드와 그 안의 메서드 호출을 기본적으로 담당하고 있습니다. 메인 스레드에 대한 더 자세한 내용은 이 게시물을 참고해보세요 !
이 작업들의 경합 상태, 교착 상태를 방지하고자, 메인스레드엔 단일 스레드 모델이 적용됩니다.
단일 스레드 모델은 자원 접근에 대한 동기화를 신경쓰지 않아도 되고, 작업전환(context switching) 비용을 요구하지 않으므로, 경합 상태와 교착 상태를 방지할 수 있다.
안드로이드에서의 단일 스레드 모델이란 안드로이드 화면을 구성하는 뷰나 뷰그룹을 하나의 스레드에서만 담당하는 원칙을 말합니다. 단일 스레드 모델은 아래 두 가지 규칙을 갖습니다.
첫째, 메인 스레드(UI 스레드)를 블럭하지 말 것
둘째, 안드로이드 UI 툴킷은 오직 UI 스레드에서만 접근할 수 있도록 할 것
단일 스레드에서의 긴 작업은 어플리케이션의 반응성을 낮추거나, ANR의 원인이 될 수 있습니다. 따라서 메인 스레드에선 정해진 최소한의 일만 담당하고, 특히 긴 작업은 다른 스레드가 담당하게 해야합니다.
따라서 이 메인 스레드와 다른 스레드가 협업하기위해, 스레드간 통신이 필요하게 되었습니다.
안드로이드에선 Looper와 Handler를 사용하여, 다른 스레드와 메인 스레드간의 통신을 할 수 있습니다.
위 내용을 정리하면, 안드로이드에서의 Thread-Looper-Handler 구조는, 메인 스레드에 단일 스레드 모델이 적용되면서 요구되는 스레드간의 통신 방법을 지원하는 구조라고 이해하면 좋습니다.
Thread별로 Looper를 생성합니다. Thread의 TLS에 Looper가 저장되고, 꺼내어져 사용됩니다. Looper ∈ Thread
Looper별로 MessageQueue를 가집니다. 즉, Looper 자신의 loop 대상이 담긴 곳(MessageQueue)이 지정되어있다고 이해하면 좋습니다. Looper# - MessageQueue#
MessageQueue는, 이름에서도 알 수 있듯이, Queue 자료구조입니다. Message, Runnable 객체가 FIFO 방식에 따라 들어가고 나오게 됩니다. Message, Runnable ∈ MessageQueue
Handler는 어디서 생성되는지, 어떤 Looper를 사용하는 지에 따라 통신 대상 스레드가 달라집니다. 메인스레드, 다른 일반스레드는 물론 자기 자신도 통신대상이 될 수 있습니다. Handler에 대한 자세한 내용은 다음 포스팅인 안드로이드 Thread와 Handler 2을 참고해보세요 !
공식문서에 의하면, java.lang.Thread는 프로그램에서 실행된는 스레드를 의미하며, JVM에서 지원하는 바와 같이 여러개의 스레드를 동시에 실행할 수 있는 프로세스의 구성요소 입니다.
A thread is a thread of execution in a program. The Java Virtual Machine allows an application to have multiple threads of execution running concurrently.
스레드는 프로세스의 작업 단위로 설명되기도 합니다. 이와 관련하여 프로그램, 프로세스, 스레드, TLS 를 참고해보세요 !
(위 그림 출처) 여러개의 Thread 클래스의 생성자 중, 추후에 나올 Runnable에 대한 설명을 돕기 위해, Runnable 매개변수의 유무로 생성자를 2갈래로 구분지어 설명하려 합니다.
첫 번째 방식은 기본 생성자인 Thread() 생성자로 스레드를 생성하는 방식입니다. 내부적으로 run()을 Override하여 사용합니다.
두 번째 방식은 Thread(Runnable runnable) 생성자로 스레드를 생성하는 방식입니다. 따로 Runnable 인터페이스를 구현한 객체를 생성하여 전달하여야합니다.
공식문서에 따르면, Runnable 인터페이스를 구현한 클래스는 run()으로 정의 되어야한다고 합니다. 즉, Runnable 객체는 run()에 대한 내용을 가지고 있는 객체라 이해하시면 좋습니다.
The
Runnable
interface should be implemented by any class whose instances are intended to be executed by a thread. The class must define a method of no arguments calledrun
.
매개변수로 전달된 runnable 객체는 스레드가 실행될때 사용됩니다. 즉 스레드의 start()가 호출되면, runnable의 run()이 호출됩니다.
Thread에서 run()을 Override한 경우 (첫 번째 방식)
start()함수가 실행되면, JVM에 의해, 해당 스레드의 run()이 호출됩니다.
Causes this thread to begin execution; the Java Virtual Machine calls the
run
method of this thread.
new thread().start();
결과적으로는 동시에 2개의 스레드가 실행됩니다.
Runnable 사용하는 경우 (두 번째 방식)
Thread의 run()은 내부적으로 Runnable의 run()을 호출합니다.
new Thread(Runnable).start();
If this thread was constructed using a separate
Runnable
run object, then thatRunnable
object'srun
method is called; otherwise, this method does nothing and returns.
Runnable
: the object whoserun
method is invoked when this thread is started. Ifnull
, this classesrun
method does nothing.
스레드를 직접 정의하여 사용하는 방법으로는, 어떤 것이 부모가 되는지에 따라 두가지 방법으로 나뉘게 됩니다.
Thread 클래스를 상속받아 정의하는 방법
class MyThread extends Thread {}
Runnable 인터페이스를 구현하여 정의하는 방법
class PrimeRun implements Runnable {}
A class that implements
Runnable
can run without subclassingThread
by instantiating aThread
instance and passing itself in as the target.
Looper란 Handler가 처리할 작업을 계속해서 pop 해주는 구조를 의미합니다. 즉 MesseageQueue에 있는 message, runnable을 계속해서 FIFO 방식으로 하나씩 꺼내줍니다.
Looper란 명칭에서 알 수 있듯이, 이렇게 Looper는 반복 작업을 맡고 있습니다.
Looper는 Handler와 가장 많이 소통합니다.
Most interaction with a message loop is through the
Handler
class.
Looper의 반복 작업은, 스레드 종류에 상관없이 (메인스레드, 일반스레드에 상관없이) loop()함수의 무한 loop을 통해 이뤄집니다.
오픈소스인 안드로이드 프레임워크단의 소스를 통해 loop() 함수의 구현을 확인해봅시다. 여기를 통해 소스 전체를 확인하실 수 있습니다.
loop() 함수안은 pop한 내용물이 null일때까지 반복하는 무한 loop로 구성되어있습니다. 이 무한 loop안에서, msg.target.dispatchMessage(msg); 를 통해, Handler인 target이 작업을 처리하게 만들어줍니다.
여기서 dispatch란 ready status를 running status로 만들어주는 작업을 의미합니다.
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
public static void loop() {
final Looper me = myLooper();
// 생략
final MessageQueue queue = me.mQueue;
// 생략
for (;;) { // 무한 루프 시작 ‼️
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// 생략
try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
// 생략
msg.recycleUnchecked();
} // 무한 루프 끝 ‼️
}
기본적으로 스레드 상관없이, 각 Looper는 자신을 생성한 스레드의 Thread Local Storage (TLS)에 저장됩니다.
메인스레드의 Looper인 MainLooper는, API 30 이전엔, ActivityThread.java의 main 함수에서 prepareMainLooper() 호출을 통해 MainLooper가 생성되고 loop() 호출에 의해 MainLooper가 동작되었습니다.
// ActivityThread.java
public static void main(String[] args) {
// 생략
Looper.prepareMainLooper();
// 생략
Looper.loop();
// 생략
}
하지만 API 30 이후, prepareMainLooper()가 deprecated 되었는데, 안드로이드 환경에서 MainLooper를 직접 생성해주는 방식으로 바뀌었기 때문입니다. 따라서 개발자들은 (원래도 그랬지만) MainLooper의 생성에 관여하지 않고, getMainLooper()를 통해 가져다 쓰기만 하면 됩니다.
* @deprecated The main looper for your application is created by the Android environment, * so you should never need to call this function yourself.
반면, 일반스레드는 기본적으로 Looper를 가지고 있지 않아, 각 스레드의 Handler를 사용하기 위해선 직접 Looper를 만들어주어야 합니다.
Threads by default do not have a message loop associated with them.
일반스레드의 Looper를 생성하는 방법은 prepare() 함수를 사용하는 것 입니다. 또한 메인스레드와 마찬가지로, loop() 호출을 통해 동작하게 됩니다. 정리하면, 개발자는 일반스레드를 사용하는 곳에서 prepare() 호출을 통해 Looper를 생성하고 loop() 호출에 의해 Looper를 동작하게 합니다.
// Android Developers : Looper
class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler(Looper.myLooper()) {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop();
}
}
-delayed()가 Queueing에 미치는 영향에 대해 이야기해보겠습니다. 만약 현재 시각이 10이고, handler.postDelayed(runnable, 200); 이 호출되었다면, 해당 runnable이 MessageQueue에 삽입되고 실행되는 시각은 언제일까요 ?
enqueue > 먼저 삽입되고 200 delay될까요 ? 200 delay후 삽입될까요 ?
dispatch > dispatch되는 순서는 어떻게 될까요 ?
정답은 바로 -delayed가 호출된 10에 enqueue되며, 현재시간(uptimeMillies, 10) + delayMilillis(200) 의 시간 이후로, 즉 200에 (또는 200 이후에) 해당 작업이 dispatch 되도록 합니다.
내부적으론, MessageQueue의 msg, runnable의 정렬과 딥 슬립을 통해 특정 시간(uptimeMillies, 타임스탬프) 이후로, 해당 작업이 dispatch 되도록합니다.
MessageQueue가 링크구조를 사용하는 이유는, 타임스탬프를 기준으로, 정렬을 수행하는 것, 중간에 삽입하는 것 과도 관련이있습니다.
MessageQueue에는 Message가 실행 타임스탬스순으로 삽입되고 링크로 연결되어, 실행 시간이 빠른 것부터 순차적으로 꺼내어진다.
post(), send() 메서드에서 실행 시간이 전달되고, 나중에 호출한 것이라도 타임스탬프가 앞서면 큐 중간에 삽입된다. 이것이 삽입이 쉬는 링크 구조를 사용한 이유다.