안드로이드 개발사이트(http://android-developers.blogspot.com/2009/01/avoiding-memory-leaks.html) 에 있는 내용으로,

너무나도 중요하며 안드로이드 개발자라면 반드시 숙지해야 하는 부분이다.

많은 안드로이드 개발자들이 메모리 문제로 고생하지 않길 바라는 마음에 번역하였다.


메모리 릭 피하기

안드로이드 어플리케이션들은 T-Mobile G1 모델을 포함하여 최소한 16MB 의 heap 용량을 갖는다. 이는 전화기에 사용하기에는 충분한 메모리이지만 몇몇 개발자들에게는 여전히 부족할 수도 있다. 심지어 이 메모리를 다 사용하려 하지 않는다 하더라도 한번쯤은 주의할 필요가 있다. 우리의 앱이 많은 메모리를 사용한다면 다른 앱이 실행될 때 우리의 앱을 죽일 수도 있기 때문이다. 즉, 안드로이드가 가능한 많은 앱을 메모리에 담을 수 있도록 하여, 앱의 빠른 실행전환을 돕는 것이 좋다.

내가 맡은 일의 한 부분으로, 나는 안드로이드 어플리케이션의 메모리 릭 부분에 특히 집중하였으며 대부분이 같은 실수 때문인 것이라는 것을 알게 되었다. 실수란 바로 Context  참조를 오랫동안 갖고 있는 객체를 생성하는 것을 말한다.

안드로이드에서는 대부분의 동작에 Context 를 사용하며 코드에서는 주로 리소스를 불러들이거나 접근하는 데 사용한다. 이러한 이유로 모든 안드로이드 widget(android.widget 패키지 내의 클래스들 - 역자 주) 들은 생성자에 Context 를 파라미터로 가진다. 일반적인 안드로이드 앱에서는 두 종류의 Context 를 사용하기 마련인데 바로 Activity 와 Application 이다. 개발자들은Context 가 필요한 코드에서, 전자를 사용하는 경우가 많은데 예를 들면 다음의 코드와 같다.

@Override
protected void onCreate(Bundle state) {
 
super.onCreate(state);
 
 
TextView label = new TextView(this);
  label
.setText("Leaks are bad");
 
  setContentView
(label);
}

이 코드를 살펴보자면 뷰들은 activity 전체에 대한 참조를 가진다. activity 뿐 아니라 activity 가 가진 모든 것들, 즉 View 전체의 계층구조 및 개별 View들이 사용하는 리소스들에 대한 참조를 가진단 뜻이다. 그러므로, Context 릭(leak) 이 발생한다는 것은 (릭이란 객체의 참조가 사라지지 않아 GC 대상에서 제외되는 객체를 만든다는 뜻임), 많은 메모리 낭비를 의미한다. 따라서, 조심하지 않으면 전체 Activity 에 대한 릭은 매우 빈번하게 발생한다.

간단한 예를 들어보자. 화면 방향이 바뀔 경우, 시스템은 기본적으로 현재의 Activity 를 제거한 다음 그 상태를 보존한 채로 새로운 Activity 를 만든다. 이 과정 동안 안드로이드는 앱의 UI를 리소스부터 새로 읽어들인다. 

그렇다면 매번 화면 방향을 바꿀 때 마다 그림을 읽어들이는 것을 원하지 않아, 다음과 같은 코드를 작성했다고 가정해 보자. 가장 쉬운 방법은 이미지를 static 참조로 바꾸어 그 상태를 확인함으로써 매번 불러들이지 않도록 하게 하는 것이다.

private static Drawable sBackground;

@Override
protected void onCreate(Bundle state) {
 
super.onCreate(state);
 
 
TextView label = new TextView(this);
  label
.setText("Leaks are bad");
 
 
if (sBackground == null) {
    sBackground
= getDrawable(R.drawable.large_bitmap);
 
}
  label
.setBackgroundDrawable(sBackground);
 
  setContentView
(label);
}

이 코드는 매우 빠르며 또한 매우 잘못되었다. 위 코드대로라면 최초에 화면변환이 일어날 때 최초의 Activity 참조를 놓치게 된다(leaks). Drawable 이 View 에 붙을 때(attached) View 는 drawable 을 callback 으로 갖게 된다. 코드를 다시 살펴보면, drawable 은TextView 에 대한 참조를 가지며  TextView  는 activity(Context) 에 대한 참조를 다시 갖는 구조이다. 코드에 따라 다르긴 하겠지만 이는 즉, 하나의 drawable 이 TextView 뿐 아니라 더 많은 것들에 대한 참조를 가지게 될 수도 있다는 것이다.

이 예제는 Context 릭이 발생하는 가장 간단하고 흔한 경우를 보여주며, 이를 막기 위해 안드로이드 개발팀이 홈 스크린의 소스코드 에서 어떤 노력을 하는지 확인할 수 있다. (unbindDrawables() 메소드를 살펴볼 것). 홈 스크린 소스 코드에서는 Activity 가  제거될 때, drawable 에 대한 callback 을 null 로 설정하여 이를 해결하고 있다. 흥미롭게도 놓친 context 들에 대한 참조들은 계속 이어질 수 있으며(GC되지 않았으므로 참조 가능함. 즉, 사라진 화면에 대한 참조조차도 가능하다는 뜻 - 역자 주), 또한 더욱 나쁘다. 메모리를 훨씬 빨리 고갈시킬 수 있기 때문이다.

Context 에 의한 메모리 릭을 피할 수 있는 쉬운 두 가지의 방법이 있다.

가장 명백한 첫번째 해결책은  Context 참조의 범위를 최소한으로 두라는 것이다. 위의 예제는 static 참조에 대한 예제이지만, 내부 클래스(inner class, http://docs.oracle.com/javase/tutorial/java/javaOO/innerclasses.html 참조 - 역자 주) 와 외부 클래스의 암묵적인 참조 또한 위험하다.

두 번째 해결책은 Application 의 Context를 사용하는 것이다. 이 Context는 우리의 앱이 살아있는 동안 계속 유효하며 activity 의 생명주기와는 전혀 무관하다. 만약 Context 가 필요한 장주기 객체(long-lived object)를 사용해야 할 경우  Context.getApplicationContext()또는 Activity.getApplication() 를 호출하여 획득한 Application 의 Context 사용을 고려하는 것도 좋다.

결론짓자면, Context 참조와 관련한 메모리 릭을 피하려면 다음의 내용을 숙지해야 한다.

  • 장주기 객체의 activity 참조는 가능한 피한다. (activity를 참조하는 객체는 activity 가 죽을 경우 제거되어야 함 - 선언한 activity 이외의 곳에서는 GC 대상이 되도록 설계해야 한다. - 역자 주)
  • Activity 의  Context 대신  Application 의 Context 사용한다.
  • 객체 생명주기를 직접 제어하지 않는 경우  Activity 내에서 non-static 내부 클래스의 사용을 가급적 피하고 불가피하게 사용해야 하는 경우라면 참조 타입을 약한 참조로 두도록 한다. 이 문제는 ViewRoot 구현에서 했던 것 처럼  외부 클래스를WeakReference 참조로 둔 static 내부 클래스를 활용하는 것으로 해결할 수 있다.
  • 가비지 컬렉터는 메모리 릭 방지를 보장해 주지 않는다.


'Programing > Android' 카테고리의 다른 글

Processes and Application Life Cycle  (0) 2016.05.04
Google I/O - Memory Management For Android  (0) 2016.05.03
Activity, Fragment Lifecycles  (0) 2016.05.03
Handler, Looper  (0) 2016.04.19
Parcelable vs. Serializable  (0) 2016.03.22

출처: http://baiduhix.blogspot.com.br/2015/08/android-how-to-do-findviewbyid-in.html

★ http://www.vogella.com/tutorials/AndroidLifeCycle/article.html

  • Activity and Fragment Lifecycle





  • Activity Lifecycle (출처: http://philosymbol.net/?p=571)
    : 안드로이드 어플리케이션은 자신의 프로세스 수명을 직접 제어하지 않으며, 안드로이드 런타임이 각 어플리케이션의 프로세스와 그 안에 있는 각 activity를 관리한다.
    : 런타임이 프로세스와 activity의 종료와 관리를 다루는 동안, activity의 상태는 자신의 부모 어플리케이션 프로세스의 우선순위를 결정하는데 사용된다.
    : 어플리케이션 우선순위는 런타임이 어플리케이션과 그 안에 실행중인 activity를 종료시킬 가능성에 영향을 미치게된다.

    - 활성(Active)
     : activity가 stack의 최상위에 있을 경우 이 activity는 현재 화면에 보이고 사용자 입력을 받는다.
     : 안드로이드는 무슨 일이 있어도 활성 상태에 있는 activity가 살아있도록 노력하며, 이 activity가 필요로 하는 리소스를 확보하기 위해 필요에 따라 stack의 아래쪽에 있는 activity들을 종료시킬 수 있다.
     : 다른 activity가 활성화되면 기존의 활성 activity는 일시 중지(Pause)된다.

    - 일시 중지(Active)
     : 경우에 따라 activity는 화면에는 보이지만 포커스를 지니지 않을 수 있다.
     : 이 상태는 투명한 activity나 화면 전체를 사용하지 않는 activity가 그 앞에 활성화되어 있는 경우이다.
     : 일시 중지 상태가 되는 경우 activity는 활성 상태인것처럼 다뤄지지만 사용자 입력을 받지 않는다.
     : 극단적인 경우, 안드로이드는 활성 activity를 위한 리소스 확보를 위해 일시 중지 상태의 activity를 종료시킬 수 있을 것이다.
     : 만약 activity가 완전히 가려지게 되면 그 activity는 중지상태가 된다.

    - 중지(Stop)
     : activity는 화면에 보이지 않는다.
     : 이 activity는 모든 상태 및 멤버 정보를 메모리에 남기지만, 만약 시스템이 활성 activity를 위해 메모리를 요청할 경우 리소스 확보를 위한 정리 후보 1순위가 된다.
     : activity가 중지될 때는 데이터와 현재 UI상태를 저장하는 것이 중요하다.
     : activity가 화면 밖으로 나가거나 닫히고 나면 그 activity는 비활성 상태가 된다.

    - 비활성(Inactive)
     : activity는 종료되고 난 이후와 시작되기 이전 비활성 상태에 머문다.
     : 비활성 activity는 activity stack에게 제거되며, 화면에 다시 나타내기 위해서는 재시작되어야 한다.

     



    - Activity 생명주기를 구성하는 메소드

    메소드 

    설명

     onCreate

     액티비티가 최초 생성시에 호출된다. 초기화 설정을 하는 곳으로 보관된 상태의 액티비티가 있다면, 그 상태를 저장중인 Bundle 객체를 받는다. onStart() 메소드가 이어진다. 강제종료가 불가능하다.

     onRestart

     액티비티가 정지 후 다시 시작되기 바로 직전에 호출된다.
    onStart() 메소드가 이어진다. 강제종료가 불가능하다. 

     onStart

     액티비티가 사용자에게 보여지기 직전에 호출된다. 액티비티가 보여지게되면 onResume() 메소드가, 안보이게 되면 onStop() 메소드가 이어진다. 강제종료가 불가능하다.

     onResume

     액티비티가 사용자와 상호작용하기 직전에 호출된다. (스택의 최상위에 위치) onPause() 메소드가 이어진다. 강제종료가 불가능하다.

     onPause

     시스템이 다른 액티비티를 시작하려 할 때 호출된다. 일반적으로 데이터 저장을 하기에 좋은 곳이다. 소스코드의 속도가 빨라야 한다. 왜냐하면 이 메소드가 끝나기 전까지 다음 액티비티가 실행되지 않기 때문인데 액티비티가 되돌아오면 onResume(), 보이지않게되면 onStop()이 이어진다. 강제종료가 불가능하다.

     onStop

     액티비티가 사용자에게 보이지 않을때 호출 된다. 액티비티가 제거되거나 다른 액티비티가 실행되어 해당 액티비티를 덮어버렸을때, 호출된다.
    액티비티가 되돌아오면 onRestart(), 액티비티가 사라지면 onDestroy() 가 이어진다. 강제종료가 가능하다.

     onDestroy

     액티비티 삭제 직전에 호출된다. 액티비티가 받는 마지막 호출 메소드로 시스템이 메모리 확보를 위해 액티비티 인스턴스를 없애버리거나, finish() 메소드가 호출되면 호출되는 메소드이다.
    isFinishing() 메소드로 두 가지를 분기할 수 있다. onStart() 메소드가 이어진다. 강제종료가 불가능하다. 



    - Activity 상태 저장 (출처: http://namsieon.com/286)


    시스템이 액티비티를 강제종료 했을때, 사용자는 이전의 액티비티로 돌아가고 싶을 수 있습니다. 이럴 경우 액티비티가 강제종료 되기 전에 상태를 저장할 수 있는
     
    onSaveInstanceState() 메소드를 구현하면 저장이 가능해 집니다.



    즉, 액티비티가 파괴되기전에 호출되는 메소드 인데요. ( onPause() 호출 이전에 호출됩니다. )
    이 메소드는 이름/값 쌍으로 이루어진 번들 객체(Bundle) 를 인수로 가집니다. 액티비티가 다시 시작되면 번들은 onSaveInstanceState() 와 onStart() 이후에 호출되는 onRestoreInstanceState() 에게 전달됩니다.



    ☞ onSaveInstanceState() , onRestoreInstanceState() 메소드는 생명주기 메소드는 아닙니다.
    따라서 항상 호출되지는 않으며 특정 상황 ( 액티비티 강제종료전에 onSaveInstance() 호출처럼 ) 에서만 호출됩니다. 단, 사용자 액션에 의해 종료될 때는 ( 사용자가 직접종료 ) 호출되지 않습니다.
    - 사용자가 되돌아가지 않을 생각으로 종료한 것으로 판단한 것이겠죠...

    onSaveInstanceState() 는 액티비티의 일시적인 상태 저장을 위한 것이므로 , 데이터 등을 안전하게 저장하려면 onPause() 메소드에서 처리해야 합니다.



  • Fragment
















'Programing > Android' 카테고리의 다른 글

Google I/O - Memory Management For Android  (0) 2016.05.03
Avoiding Memory Leaks  (0) 2016.05.03
Handler, Looper  (0) 2016.04.19
Parcelable vs. Serializable  (0) 2016.03.22
안드로이드 - 자바 시스템 프레임워크  (0) 2016.01.26
출처 : https://realm.io/kr/news/android-thread-looper-handler/

소개

안드로이드의 애플리케이션을 실행하면 시스템은 메인 액티비티를 메모리로 올려 프로세스로 만들며, 이 때 메인 스레드가 자동으로 생성됩니다. 메인 스레드는 안드로이드의 주요 컴퍼넌트를 실행하는 곳이자 UI를 그리거나 갱신하는 일을 담당할 수 있는 유일한 스레드이므로 UI 스레드라고도 불립니다.

안드로이드 화면을 구성하는 뷰나 뷰그룹을 하나의 스레드에서만 담당하는 원칙을 싱글 스레드 모델이라고 합니다. 싱글 스레드 모델의 규칙은 첫째, 메인 스레드(UI 스레드)를 블럭하지 말 것, 둘째, 안드로이드 UI 툴킷은 오직 UI 스레드에서만 접근할 수 있도록 할 것, 이 두 가지입니다. 이런 싱글 스레드 모델의 영향을 고려하지 않으면 애플리케이션의 성능이 저하될 수 있습니다. 긴 시간이 걸리는 작업을 메인 스레드에서 담당한다면 애플리케이션의 반응성이 낮아질 수 있고, 급기야 사용자의 불편함을 방지하고자 시스템이 애플리케이션을 ANR(Appication Not Responding) 상태로 전환시킬 수도 있습니다. 따라서 시간이 걸리는 작업을 하는 코드는 여분의 스레드를 사용하여 메인 스레드에서 분리해야 하고, 자연스럽게 메인 스레드와 다른 스레드가 통신하는 방법이 필요하게 됩니다.

다른 스레드에서 메인 스레드로 접근하기 위해 Looper와 Handler를 사용할 수 있으며, 안드로이드는 Java의 Thread를 좀 더 쉽게 사용할 수 있도록 래핑한 HandlerThread, 더 나아가 Thread나 Message Loop 등의 작동 원리를 크게 고려하지 않고도 사용이 가능한 AsyncTask 등의 클래스를 제공합니다. 이 글에서는 먼저 Thread-Looper-Handler의 개념을 이해하고, 나아가 HandlerThread와 AsyncTask에 대해 정리해보도록 하겠습니다.

Looper와 Handler의 사용 목적

왜 안드로이드는 메인 스레드에서만 UI 작업이 가능하도록 제한할까요? 메인 스레드가 아닌 스레드가 병렬적으로 실행되고 있을 때, 메인 스레드와 다른 스레드, 두 개 이상의 스레드가 동시에 같은 텍스트뷰에 setText()를 시도하는 경우를 생각하면 간단합니다.

둘 중 어느 스레드의 setText()가 적용될지 예측할 수 없고, 사용자는 둘 중 하나의 값만을 볼 수 있어 다른 한 스레드의 결과는 버려집니다. 이같이 두 개 이상의 스레드를 사용할 때의 동기화 이슈를 차단하기 위해서 Looper와 Handler를 사용하게 됩니다.

Looper와 Handler의 작동 원리

먼저 스레드와 Looper, Handler가 어떻게 작동하는지 알아볼까요? 메인 스레드는 내부적으로 Looper를 가지며 그 안에는 Message Queue가 포함됩니다. Message Queue는 스레드가 다른 스레드나 혹은 자기 자신으로부터 전달받은 Message를 기본적으로 선입선출 형식으로 보관하는 Queue입니다. Looper는 Message Queue에서 Message나 Runnable 객체를 차례로 꺼내 Handler가 처리하도록 전달합니다. Handler는 Looper로부터 받은 Message를 실행, 처리하거나 다른 스레드로부터 메시지를 받아서 Message Queue에 넣는 역할을 하는 스레드 간의 통신 장치입니다.

이제 Handler와 Looper, Message Queue에 대해 좀 더 자세히 살펴보겠습니다.

Handler

Handler는 스레드의 Message Queue와 연계하여 Message나 Runnable 객체를 받거나 처리하여 스레드 간의 통신을 할 수 있도록 합니다. Handler 객체는 하나의 스레드와, 해당 스레드의 Message Queue에 종속됩니다. 새로 Handler 객체를 만든 경우 이를 만든 스레드와 해당 스레드의 Message Queue에 바인드됩니다. 다른 스레드가 특정 스레드에게 메시지를 전달하려면 특정 스레드에 속한 Handler의 post나 sendMessage 등의 메서드를 호출하면 됩니다. 
앞서 Message Queue는 전달받은 Message를 선입선출 형식으로 보관한다고 설명했지만, 전달 시점에 다른 메서드를 사용하여 Queue의 맨 위로 보내거나, 원하는 만큼 Message나 Runnable 객체의 전송을 지연시킬 수도 있습니다. 자주 쓰이는 Handler의 메서드를 아래 표에 정리했습니다.

리턴값메서드명인자설명
voidhandleMessageMessage msgLooper가 Message Queue에서 꺼내준 Message나 Runnable 객체를 처리 
(상속 시 구현 필수)
final booleanpostRunnable rMessage Queue에 Runnable r을 전달
final booleansendMessageMessage msgMessage Queue에 Message msg를 전달
final booleanpostAtFrontOfQueueRunnable rMessage Queue의 맨 앞에 Runnable r을 전달
final booleansendMessageAtFrontOfQueueMessage msgMessage Queue의 맨 앞에 Message msg를 전달
final booleanpostDelayedRunnable r, long delayMillisdelayMillis만큼 지연 후Message Queue에 Runnable r을 전달
final booleansendMessageDelayedMessage msg, long delayMillisdelayMillis만큼 지연 후Message Queue에 Message msg를 전달

외부, 혹은 자기 스레드로부터 받은 메시지를 어떤 식으로 처리할 지는 handleMessage() 메서드를 구현하여 정합니다. sendMessage()나 post()로 특정 Handler에게 메시지를 전달할 수 있고, 재귀적인 호출도 가능하므로 딜레이를 이용한 타이머나 스케줄링 역할도 할 수 있어 편리합니다.

Looper Message Queue

Looper는 무한히 루프를 돌며 자신이 속한 스레드의 Message Queue에 들어온 Message나 Runnable 객체를 차례로 꺼내서 이를 처리할 Handler에 전달하는 역할을 합니다. 메인 스레드는 Looper가 기본적으로 생성돼 있지만, 새로 생성한 스레드는 기본적으로 Looper를 가지고 있지 않고, 단지 run 메서드만 실행한 후 종료하기 때문에 메시지를 받을 수 없습니다. 따라서 기본 스레드에서 메시지를 전달받으려면 prepare() 메서드를 통해 Looper를 생성하고, loop() 메서드를 통해 Looper가 무한히 루프를 돌며 Message Queue에 쌓인 Message나 Runnable 객체를 꺼내 Handler에 전달하도록 합니다. 이렇게 활성화된 Looper는 quit()이나 quitSafely() 메서드로 중단할 수 있습니다. quit() 메서드가 호출되면 Looper는 즉시 종료되고, quitSafely() 메서드가 호출되면 현재 Message Queue에 쌓인 메시지들을 처리한 후 종료됩니다.

Message Runnable

Message란 스레드 간 통신할 내용을 담는 객체이자 Queue에 들어갈 일감의 단위로 Handler를 통해 보낼 수 있습니다. 일반적으로 Message가 필요할 때 새 Message 객체를 생성하면 성능 이슈가 생길 수 있으므로 안드로이드가 시스템에 만들어 둔 Message Pool의 객체를 재사용합니다. obtain() 메서드는 빈 Message 객체를, obtain(Handler h, int what …)은 목적 handler와 다른 인자들을 담은 Message 객체를 리턴합니다.
Runnable을 설명하려면 스레드를 만드는 두 가지 방법부터 말씀드려야 합니다. 새 스레드는 Thread() 생성자로 만들어서 내부적으로 run()을 구현하던지, Thread(Runnable runnable) 생성자로 만들어서 Runnable 인터페이스를 구현한 객체를 생성하여 전달하던지 둘 중 하나의 방법으로 생성하게 됩니다. 후자에서 사용하는 것이 Runnable로 스레드의 run() 메서드를 분리한 것입니다. 따라서 Runnable 인터페이스는 run() 추상 메서드를 가지고 있으므로 상속받은 클래스는 run()코드를 반드시 구현해야 합니다.
앞서 언급한대로 Message가 int나 Object같이 스레드 간 통신할 내용을 담는다면, Runnable은 실행할 run() 메서드와 그 내부에서 실행될 코드를 담는다는 차이점이 있습니다.

HandlerThread

Looper에서 언급했듯이 안드로이드의 스레드는 Java의 스레드를 사용하기 때문에 안드로이드에서 도입한 Looper를 기본으로 가지지 않는다는 불편함이 있습니다. 이 같은 불편함을 개선하기 위해 생성할 때 Looper를 자동으로 보유한 클래스를 제공하는데, 이것이 바로 HandlerThread입니다.
HandlerThread는 일반적인 스레드를 확장한 클래스로 내부에 반복해서 루프를 도는 Looper를 가집니다. 자동으로 Looper 내부의 Message Queue도 생성되므로 이를 통해 스레드로 Message나 Runnable을 전달받을 수 있습니다.

AsyncTask

AsyncTask는 스레드나 메시지 루프 등의 작동 원리를 몰라도 하나의 클래스에서 UI작업과 backgrond 작업을 쉽게 할 수 있도록 안드로이드에서 제공하는 클래스입니다. 캡슐화가 잘 되어 있기 때문에 사용시 코드 가독성이 증대되는 장점이 있으며, 태스크 스케쥴을 관리할 수 있는 콜백 메서드를 제공하고, 필요할 때 쉽게 UI 갱신도 가능하며 작업 취소도 쉽습니다. 따라서 리스트에 보여주기 위한 데이터 다운로드 등 UI와 관련된 독립된 작업을 실행할 경우 AsyncTask로 간단하게 구현할 수 있습니다.

그림: AsyncTask의 구조

그러나 AsyncTask를 사용해서 스케줄링 할 수 있는 작업 수의 제한이 있고, 몇 초 정도의 짧은 작업에서만 이상적으로 동작한다는 한계가 있습니다. 또한, 안드로이드의 버전 별로 병렬 처리 동작이 다르므로 허니콤 이후 버전에서 멀티 스레드로 병렬적인 동작을 원한다면 AsyncTask를 실행할 때 AsyncTask.THREAD_POOL_EXECUTOR 스케줄러를 지정해야 합니다. 
한편 앞서 살펴본 Handler와 Looper를 사용한다면 작동 원리를 고려해야 하며 구현을 직접 해야 하고 코드가 복잡해져서 가독성을 저해한다는 단점이 있지만 그만큼 개발 범위가 자유롭습니다. 또한 UI 스레드에서만 작업하지 않아도 되므로 보다 많은 자율성을 가지고 코드를 제어하기를 원한다면 Handler나 HandlerThread 사용을 고려해 보세요.

+ Recent posts