From 89d7fc0321b92477fe319e34c6947d8b743cfed2 Mon Sep 17 00:00:00 2001 From: sery270 Date: Sun, 10 Oct 2021 17:56:48 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[feature/WEEK=202]=20WEEK2=20part2=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Week2/SIBA WEEK 2 Part 1.md | 10 ++- Week2/SIBA WEEK 2 Part 2.md | 145 +++++++++++++++++++++++++++++++++--- 2 files changed, 140 insertions(+), 15 deletions(-) diff --git a/Week2/SIBA WEEK 2 Part 1.md b/Week2/SIBA WEEK 2 Part 1.md index b86255b..3a6df45 100644 --- a/Week2/SIBA WEEK 2 Part 1.md +++ b/Week2/SIBA WEEK 2 Part 1.md @@ -2,6 +2,8 @@ # 메인 스레드와 Handler - Handler와 Looper 그리고 MessageQueue의 동작 방식 +> Handler와 Looper 그리고 MessageQueued의 의 동작 방식을 안드로이드 프레임 워크를 통해 알아봅니다. + 안드로이드의 메인스레드인 ActivityThread.java 를 이해하는데에 큰 배경 지식이 된다는 점에서 Handler와 Looper 그리고 MessageQueue의 동작 방식을 이해하는 것은 큰 가치가 있는 일입니다. 스레드 통신과 약간의 자료구조에 대한 부분까지 생각해보는 좋은 기회가 될 것입니다. 따라서 이번 아티클에서는 안드로이드 프레임워크의 코드와 함께 Handler와 Looper 그리고 MessageQueue의 동작 방식에 대해 깊게 알아보겠습니다. @@ -141,7 +143,7 @@ public static void loop() { [android.os.HandlerThread](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/HandlerThread.java?q=android.os.HandlerThread&hl=ko) 에선 Looper에 대한 널 체크를 한후, Looper에 정의된 동명의 함수를 호출합니다. -``` +```java public boolean quit() { Looper looper = getLooper(); if (looper != null) { @@ -164,7 +166,7 @@ public static void loop() { 물론 Looper.quit()과 Looper.quitSafely() 동작은 MessageQueue.quit() 함수의 boolean 형의 매개변수 값으로 분기 처리됩니다. -``` +```java public void quit() { mQueue.quit(false); } @@ -184,7 +186,7 @@ public static void loop() { > 무한 루프를 돌며 기존 Looper의 작업을 재개합니다. > 다만, 작업 재개에 있어 [uptimeMillis](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/SystemClock.java;drc=master;l=178?hl=ko)()과 when을 비교하는데, 만약 when이 [uptimeMillis](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/SystemClock.java;drc=master;l=178?hl=ko)()보다 더 미래일 경우 작업을 멈추고, MessageQueue의 남은 메세지에 대해 [recycleUnchecked](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;drc=master;l=324?hl=ko)()를 호출합니다. -``` +```java void quit(boolean safe) { if (!mQuitAllowed) { throw new IllegalStateException("Main thread not allowed to quit."); @@ -229,7 +231,7 @@ MessageQueue는 Message를 담고 있는 자료구조라 말씀드렸습니다. 먼저 [Message 클래스](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java?q=public%20final%20class%20Message%20implement%20Parcelable&ss=android&hl=ko) 에는 핸들러가 스레드간의 통신 매개로서 전달해야할, (다른 스레드에서 실행될) 작업에 대한 정보를 담고있습니다. -``` +```java public final class Message implements Parcelable { /** * User-defined message code so that the recipient can identify diff --git a/Week2/SIBA WEEK 2 Part 2.md b/Week2/SIBA WEEK 2 Part 2.md index a7b81a6..6f0c5a8 100644 --- a/Week2/SIBA WEEK 2 Part 2.md +++ b/Week2/SIBA WEEK 2 Part 2.md @@ -1,25 +1,148 @@ # 메인 스레드와 Handler Part2 - 안드로이드 애플리케이션에서의 메인 스레드 -- 안드로이드 애플리케이션에서 Handler와 Looper 그리고 MessageQueue 구조가 어떻게 쓰이는지 알아봅니다. +> 안드로이드 애플리케이션에서 Handler와 Looper 그리고 MessageQueue 구조가 어떻게 쓰이는지 알아봅니다. -앞서 Part 1에서 알아보았던, Handler와 Looper 그리고 MessageQueue의 동작이 과연 왜 필요할까요? 바로 여러 스레드를 사용하는 멀티스레드 환경에서, 특정 스레드에게 일을 위임하기 위한 수단으로 의미가 있습니다. +앞서 Part 1에서 알아보았던, Handler와 Looper 구조와 동작이 과연 왜 필요할까요? -백그라운드 스레드가 (UI와 관련하여 단일 스레드 모델이 적용된다는 점에서) 여러 특권을 가진 메인스레드에게 일을 분담하기 위해, 기본적으로 사용되는 구조가 되는 것입니다. 이번 아티클에서는 과연 그 '특권'이란 무엇이고 왜 생겨났는지, 어떻게 사용되어야하는지에 대해 알고자 합니다. 안드로이드 프레임 워크의 관점에서, 메인 스레드의 의미와 역할에 대해 알아보겠습니다. +이는 여러 스레드를 사용하는 멀티스레드 환경에서, 특정 스레드에게 일을 위임하기 위한 수단으로 의미가 있습니다. 백그라운드 스레드가 특권을 가진 메인스레드에게 일을 분담하기 위해, 기본적으로 사용되는 구조가 되는 것입니다. -UI 처리를 위한 메인 스레드 +이번 아티클에서는 과연 그 '특권'이란 무엇이고 왜 생겨났는지, 어떻게 사용되어야하는지에 대해 알고자 합니다. 안드로이드 프레임 워크의 관점에서의 메인 스레드의 의미와 역할에 대해 알아보겠습니다. -백그라운드 스레드에서 UI 업데이트 +## 안드로이드의 메인 스레드 -메인 스레드에서 다음 작업 예약 +### 안드로이드의 메인 스레드가 하는 일 -반복 UI 갱신 +- 안드로이드의 메인스레드를 찾기 위해, 일반적인 프로그램의 시작점인 main()를 찾아보도록 하겠습니다. Android FW의 main()는 [ActivityThread.java](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=247?q=ActivityThread&sq=&hl=ko) 에서 찾을 수 있습니다. -시간 제한 + ```java + public static void main(String[] args) { + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain"); + + // Install selective syscall interception + AndroidOs.install(); + + // CloseGuard defaults to true and can be quite spammy. We + // disable it here, but selectively enable it later (via + // StrictMode) on debug builds, but using DropBox, not logs. + CloseGuard.setEnabled(false); + + Environment.initForCurrentUser(); + + // Make sure TrustedCertificateStore looks in the right place for CA certificates + final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId()); + TrustedCertificateStore.setDefaultUserDirectory(configDir); + + // Call per-process mainline module initialization. + initializeMainlineModules(); + + Process.setArgV0(""); + + Looper.prepareMainLooper(); + + // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line. + // It will be in the format "seq=114" + long startSeq = 0; + if (args != null) { + for (int i = args.length - 1; i >= 0; --i) { + if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) { + startSeq = Long.parseLong( + args[i].substring(PROC_START_SEQ_IDENT.length())); + } + } + } + ActivityThread thread = new ActivityThread(); + thread.attach(false, startSeq); + + if (sMainThreadHandler == null) { + sMainThreadHandler = thread.getHandler(); + } + + if (false) { + Looper.myLooper().setMessageLogging(new + LogPrinter(Log.DEBUG, "ActivityThread")); + } + + // End of event ActivityThreadMain. + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); + Looper.loop(); + + throw new RuntimeException("Main thread loop unexpectedly exited"); + } + ``` -ANR +- 안드로이드의 메인스레드는 Activity, Service, Broadcast Reciver, Application와 같은 컴포넌트 생명주기 메서드와 관련된 작업들을 기본적으로 담당하고 있습니다. -Invalidate +### UI 작업에는 단일 스레드 모델을 적용 +- UI 작업에 필요한 UI 자원 공유를 하면서 발생할 수 있는 경합 상태, 교착 상태를 방지하고자, **메인스레드의 UI 작업에는 단일 스레드 모델**이 적용됩니다. +> 단일 스레드 모델은 자원 접근에 대한 동기화를 신경쓰지 않아도 되고, 작업전환(context switching) 비용을 요구하지 않으므로, 경합 상태와 교착 상태를 방지할 수 있다. -소결 +- 즉, 안드로이드에서의 단일 스레드 모델이란 안드로이드 화면을 구성하는 뷰나 뷰그룹을 하나의 스레드(메인 스레드)에서만 담당하는 원칙을 말합니다. 단일 스레드 모델은 아래 두 가지 규칙을 갖습니다. + + > 첫째, 메인 스레드(UI 스레드)를 블럭하지 말 것 + > + > 둘째, 안드로이드 UI 툴킷은 오직 UI 스레드에서만 접근할 수 있도록 할 것 + +### 메인스레드와 백그라운드 스레드 통신을 위한 Handler와 Looper 구조 + +- 단일 스레드에서의 긴 작업은 어플리케이션의 반응성을 낮추거나, ANR의 원인이 될 수 있습니다. 따라서 메인 스레드에선 정해진 최소한의 일만 담당하고, 특히 긴 작업은 다른 스레드가 담당하게 해야합니다. +- 따라서 이 메인 스레드와 다른 스레드가 협업하기위해, **스레드간 통신**이 필요하게 되었습니다. +- 안드로이드에선 **Looper와 Handler를 사용하여, 다른 스레드와 메인 스레드간의 통신**을 할 수 있습니다. +- 위 내용을 정리하면, **안드로이드에서의 Thread-Looper-Handler 구조는, 메인 스레드에 단일 스레드 모델이 적용되면서 요구되는 스레드간의 통신 방법을 지원하는 구조**라고 이해하면 좋습니다. + +## Handler 사용 예시 + +### 백그라운드 스레드에서 UI 작업 + +- 앞서 설명 드렸듯 UI 작업은 단일 스레드 모델이 적용되어 메인 스레드에서만 실행됩니다. 따라서 네트워크 통신 같은 백그라운드 작업을 하면서 발생하는 UI 변경 작업은 Handler 사용하여 메인 스레드로 위임되고, 메인 스레드에서 실행됩니다. + +### 반복 UI 갱신를 위한 recursive Runnable + +- 시계나 타이머처럼 반복적으로 UI 작업을 해야할 때가 있습니다. 이 UI 갱신 작업은 Runnable을 재귀적으로 설계하는 것으로 구현할 수 있습니다. + + ```kotlin + package com.example.myapplication + + import android.os.Bundle + import android.os.Handler + import android.widget.TextView + import androidx.appcompat.app.AppCompatActivity + + + class MainActivity : AppCompatActivity() { + private val DELAY_TIME = 2000L + private val handler = Handler(mainLooper) + private var updateTimeRunnable: Runnable? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + updateTimeRunnable = Runnable { + findViewById(R.id.tv1).text = System.currentTimeMillis().toString() + updateTimeRunnable?.let { handler.postDelayed(it, DELAY_TIME) } + } + + } + + + fun onClickButton(view: TextView){ + updateTimeRunnable?.let { handler.post(it) } + } + + } + + ``` + + + +### 타이머, ANR 판단 + +- + +## 요약 + +- 안드로이드의 메인 스레드는 [ActivityThread.java](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=247?q=ActivityThread&sq=&hl=ko) 에서 시작되며, 컴포넌트 생명주기 메서드와 관련된 작업들을 담당합니다. +- 경합 상태, 교착 상태를 방지하고자, UI 작업에는 단일 스레드 모델을 적용합니다. 따라서 UI 작업은 메인 스레드에서만 실행 가능합니다. +- UI 작업은 메인 스레드에서만 실행 가능하기 때문에, 작업에 따라 메인 스레드와 백그라운드 스레드간의 통신이 필요합니다. Handler와 Looper 구조는 스레드간 통신을 위한 구조 입니다. +- Handler 사용 (스레드간 통신)의 예시로는 백그라운드 스레드에서 UI 작업, UI 갱신, 시간 제한 작업이 있습니다. From 6afc27a061c4897dff01d636046fab5a66c867d6 Mon Sep 17 00:00:00 2001 From: sery270 Date: Sat, 23 Oct 2021 19:36:47 +0900 Subject: [PATCH 2/2] [feature/WEEK 3] update WEEK3 --- ...d \355\201\264\353\236\230\354\212\244.md" | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 "Week3/\353\260\261\352\267\270\353\235\274\354\232\264\353\223\234 \354\212\244\353\240\210\353\223\234 part1 - HandlerThread \355\201\264\353\236\230\354\212\244.md" diff --git "a/Week3/\353\260\261\352\267\270\353\235\274\354\232\264\353\223\234 \354\212\244\353\240\210\353\223\234 part1 - HandlerThread \355\201\264\353\236\230\354\212\244.md" "b/Week3/\353\260\261\352\267\270\353\235\274\354\232\264\353\223\234 \354\212\244\353\240\210\353\223\234 part1 - HandlerThread \355\201\264\353\236\230\354\212\244.md" new file mode 100644 index 0000000..11f4ff6 --- /dev/null +++ "b/Week3/\353\260\261\352\267\270\353\235\274\354\232\264\353\223\234 \354\212\244\353\240\210\353\223\234 part1 - HandlerThread \355\201\264\353\236\230\354\212\244.md" @@ -0,0 +1,190 @@ +# 백그라운드 스레드 part1 - HandlerThread 클래스 + +오늘은 백그라운드 스레드를 구현하는데 많이 사용되는 [HandlerThread](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/HandlerThread.java;l=40?q=handlert&sq=&hl=ko) 이 클래스에 대해 알아보면서, 지난 글에서 다뤄보았던 [Handler와 Looper 그리고 MessageQueue의 동작 방식](https://medium.com/write-android/%EB%A9%94%EC%9D%B8-%EC%8A%A4%EB%A0%88%EB%93%9C%EC%99%80-handler-part-1-handler%EC%99%80-looper-%EA%B7%B8%EB%A6%AC%EA%B3%A0-messagequeue%EC%9D%98-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D-f0bee443d71e) 에 대한 이해를 심화시켜보는 시간을 가져보려고 합니다. HandlerThread의 멤버에 대해 이해하고, HandlerThread가 필요한 이유를 이해해보도록 하겠습니다. + +더불어 이 글은 아래 문서들에 대한 자세한 설명입니다. 아래 공식 문서와 FW 코드 전문을 참고하시어 보시길 추천드립니다. + +- [HandlerThread 공식 문서](https://developer.android.com/reference/android/os/HandlerThread) + +- [HandlerThread 오픈 소스 전문](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/HandlerThread.java;l=40?q=handlert&sq=&hl=ko) + +## HandlerThread의 멤버 + +### HandlerThread 클래스 + +------ + +image + +- `HandlerThread` 는 `Thread` 클래스를 상속받고, 내부적으로 `looper.prepare()` 와 `looper.loop()` 을 실행하는 Looper 스레드이다. (Looper 스레드란 Looper를 갖는 스레드를 의미합니다. ) +- `Handler`를 가진 스레드가 아닙니다. `HandlerThread`는 **`Looper` 스레드이면서 `Handler`에서 사용하기 위한 스레드** 입니다. +- `Handler` 는 `HandlerThread` 에서 생성한 Looper에 연결하는데, 이때 `Handler` 의 세번째 생성자 `Handler(Looper looper, Handler.Callback callback)` 을 사용합니다. + +### 필드 + +### [`mPriority`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/HandlerThread.java;l=29?hl=ko) + +> The priority to run the thread at. + +- 스레드를 실행할 우선 순위입니다. + +### [`mTid`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/HandlerThread.java;l=30?hl=ko) + +- 현재 스레드의 아이디입니다. + - -1이 디폴트 값으로 설정되어있습니다. +- `run()` 내부에서 `Process.myTid()` 로 초기화됩니다. +- `loop()` 함수 종료 후 -1로 초기화됩니다. + +### [`mLooper`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/HandlerThread.java;l=31?hl=ko) + +- 현재 스레드와 연결된 루퍼입니다. +- HandlerThread의 `run()`에서 자동적으로 연결됩니다. + +### [`mHandler`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/HandlerThread.java;l=32?hl=ko) + +- 현재 스레드의 루퍼와 연결된 핸들러입니다. +- `mLooper`와는 달리, 개발자가 직접 연결해주어야하는 부분입니다. + +### 메서드 + +```java +public HandlerThread(String name) { + super(name); + mPriority = Process.THREAD_PRIORITY_DEFAULT; + } + + +public HandlerThread(String name, int priority) { + super(name); + mPriority = priority; + } +``` + +### `HandlerThread 생성자` + +- `mPriority` 값 설정 여부로 생성자가 2개로 나뉩니다. + + - `mPriority` 매개변수가 없는 경우엔 0로 초기화 됩니다. + + image + +```java +public void run() { + mTid = Process.myTid(); + Looper.prepare(); + synchronized (this) { + mLooper = Looper.myLooper(); + notifyAll(); + } + Process.setThreadPriority(mPriority); + onLooperPrepared(); + Looper.loop(); + mTid = -1; + } + public Looper getLooper() { + **if(!isAlive()) {** + return null; + } + + synchronized(this) { + **while(isAlive() && mLooper == null) {** + try { + **wait(); // (5)** + } catch (InterruptedException e) { + } + } + } + return mLooper; + } + public boolean quit() { + Looper looper = getLooper(); + if (looper != null ){ + looper.quit() + return true + } + return false + } + +public boolean quitSafely() { + Looper looper = getLooper(); + if (looper != null) { + looper.quitSafely(); + return true; + } + return false; + } +@NonNull + public Handler getThreadHandler() { + if (mHandler == null) { + mHandler = new Handler(getLooper()); + } + return mHandler; + } +public int getThreadId() { + return mTid; + } +``` + +### `run()`메서드 + +- `Looper.prepare()`, `Looper.loop()` 호출은 물론 멤버변수인 `mLooper`에 `Looper.myLooper()의 반환값` 을 할당하는 일도 합니다. + +- `getLooper()` 는 바로 `mLooper`를 반환하지 않으며, `quit()` 에서도 `mLooper.quit()` 으로 바로 Looper를 중지시키는 것이 아니라, `getLooper()` 를 통해서 얻은 Looper에 `quit()`을 호출합니다. + +- 왜 이렇게 할까? `HandlerThread`는 Looper를 멤버 변수로 갖는다. 만약 Looper가 생성되기 전에 `getLooper()` 의 반환값은 null일 것이고, 이는 NPE로 이어질 수 있습니다. `quit()`에서도 마찬가지로 `mLooper.quit()` 을 바로 호출한다면, Looper가 아직 생성되지 않았다면 NPE가 발생합니다. + + - 따라서 `getLooper()` 에서는 `mLooper`가 할당될 때까지 스레드를 대기시키는 작업을 하고, `mLooper`가 할당되었을 때 `mLooper`를 반환하도록 합니다. + - `quit()` 에서도 `getLooper()` 에서 반환된 Looper를 가지고 `quit()` 을 하도록 하여 NPE가 발생하지 않도록 합니다. + +### `getLooper()` 메서드 + +- `isAlive()` 호출을 통해서 `HandlerThread`에서 `start()` 메서드가 호출되었는지 체크합니다. 즉, Thread가 시작되었는지 체크합니다. + + - `isAlive()`는 스레드가 `start()`메서드로 시작되었고, 아직 종료되지 않았을 때 `true`를 반환합니다. 종료되었을 때는 `false` 를 반환합니다. + - `HandlerThread` 를 사용할 때는 항상 `start()`를 호출하여 스레드가 시작된 후에 사용해야 합니다. (특히 `getLooper()` 를 사용할때는 더욱더 유의!) + +- `HandlerThread`가 시작된 상태라면, `mLooper`가 null 인지 체크합니다. + + - `mLooper`가 null이라면 `wait()` 을 호출해서 `mLooper`가 할당될 때까지 `HandlerThread`를 블락상태로 만듭니다. 이 블락 상태는 + + `synchronized` 구문으로 이뤄집니다. + + - `wait()` 메서드는 `Object` 클래스에 속한 메서드입니다. + - 외부에서 `wait()`가 호출된 객체의 의 `notify()` 혹은 `notifyAll()` 를 호출할 때까지 thread가 블락상태가 됩니다. + + - 이렇게 하는 이유? + + `HandlerThread`의 `start()` 메서드가 호출되고 나서, `HandlerThread` 의 `run()` 메서드가 호출되는데, 이 `run()` 실행되는 시점을 알 수 없습니다. 따라서 `getLooper()`가 실행되는 시점에 mLooper가 null일 수 있습니다. + + - 따라서 `mLooper`에 Looper가 할당될때까지 대기하기 위해서 while 문을 돌면서 `mLooper`가 null인지를 계속해서 체크합니다. + - `Thread`에서 `start()` 이 호출되면, JVM에서 해당 `Thread` 의 `run()` 를 호출합니다. + - 호출 결과: 두 스레드(`start()` 를 호출한 스레드, `run()` 를 실행하는 스레드)가 동시에 실행되는 상태가 됩니다. + + - `getLooper()` 가 `mLooper`가 직접적으로 참조되는 유일한 곳이다. 즉, `mLooper`를 사용하려면 `getLooper()`를 통해서만 얻을 수 있기에, `mLooper`로 인한 NPE가 방지됩니다. + +1. 블락 상태인 `Thread`를 깨우는 곳: 에서 `wait()` 를 호출하여 `mLooper`가 할당될때가지 기다리다가, (2)에서 `mLooper` 가 할당된 후에 `notifyAll()` 을 통해서 `Thread`를 깨웁니다. + +### `quit()` 메서드 `quitSafely()` 메서드 + +해당 메서드들에 대한 설명은 [직전 글의 *Looper의 동작* 부분](https://medium.com/write-android/메인-스레드와-handler-part-1-handler와-looper-그리고-messagequeue의-동작-방식-f0bee443d71e) 에서 자세히 설명하고 있으니 참고 부탁드립니다. + +### `getThreadHandler` 메서드 + +- 현재 스레드의 루퍼와 연결된 핸들러가 + - 있다면 → 리턴해준다. `mHandler`를 리턴합니다. + - 없다면 → 새로 생성해준다. `mHandler`를 초기화 시켜줍니다. + +### `getThreadId` 메서드 + +- 현재 스레드의 아이디를 리턴합니다. +- `run()` 함수 호출전까진, 초기값인 -1이 리턴합니다. + +## HandlerThread가 필요한 이유 + +- 내부적으로 *looper.prepare()*와 *looper.loop()* 을 실행하는 Looper 스레드에게 Looper가 없는 상황을 방지해줍니다. + + - 해당 루퍼와 연결될 핸들러를 생성할 때엔 항상 `Handler(Looper looper, Handler.Callback callback)` 생성자를 사용하여, 루퍼를 명시적으로 정해줍니다. + +- 핸들러가 달려있지 않은 스레드에 대한 메모리릭을 방지해줍니다. + +- 개발자가 신경써야하는 위와 같은 상황들을 FW단에서 처리할 수 있게 하여, 개발자가 메세지 (작업) 에 집중할 수 있게 해줍니다.