상황 별 호출되는 액티비티의 생명주기 메소드들

안드로이드의 앱을 구성하는 일명 4대 컴포넌트는 모두 생명주기가 있다. 이 중, 가장 복잡한 생명주기를 가진 컴포넌트가 액티비티다. 액티비티의 생명주기에 대한 글은 수없이 많다. 하지만, 그런 글들을 읽고 액티비티의 생명주기를 이해하더라도, 문제 분석 시 ‘그래서 지금 사용자가 뭘 했길래 이렇게 호출된거야?’라는 생각이 드는 경우가 있다. 그러므로, 여기서는 액티비티 생명주기 소개 대신에 상황 별로 호출되는 액티비티의 생명주기 메소드들을 정리한다.

https://developer.android.com/reference/android/app/Activity

참고로, 여기서 상황이란 ‘앱 아이콘을 통해 액티비티를 최초 실행 시’ 등 액티비티에게 변화를 주는 액션을 사용자가 하는 때이며, O OS 단말 기준으로 테스트했다.

onCreate() > onStart() > onResume() 순으로 호출되는 경우

  • (액티비티 미실행 상태) – 앱아이콘 클릭 – 액티비티 최초 실행
  • (최근 앱 목록 표시 중, 액티비티는 미실행 상태) – 액티비티 선택 – 액티비티 실행

onCreate() > onStart() > onResume() > onPause() > onStop() 순으로 호출되는 경우

  • (화면 잠김 상태) – ADB 커맨드를 통한 액티비티 실행

onRestart() > onStart() > onResume() 순으로 호출되는 경우

  • (액티비티 foreground 상태에서 화면 잠김 상태) – 화면 잠금 해제(홈이나 전원 버튼, 필요하다면 비밀번호 입력 등) – 액티비티 실행
  • (액티비티를 실행한 적이 있으며 숨겨져 있는 상태) – 액티비티 실행(앱 아이콘 클릭 등) – 액티비티 실행

onPause()만 호출되는 경우

  • (액티비티 foreground 상태) – 다른 액티비티 실행

onPause() > onStop() 순으로 호출되는 경우

  • (액티비티 foreground 상태) – 전원 버튼 누르기 – 화면 잠금
  • (액티비티 foreground 상태) – 홈 버튼 누르기 – 런쳐 홈으로 이동

onPause() > onStop() > onDestroy() 순으로 호출되는 경우

  • (액티비티 foreground 상태) – 뒤로가기 버튼 눌러 액티비티 나가기

onDestroy()만 호출되는 경우

  • (최근 앱 목록 표시 중) – 액티비티 제거하기(밀어내기나 ‘X’ 버튼)

그 밖에

  • 액티비티를 실행한 적이 있다는 말의 의미는, 액티비티 컴포넌트가 소멸되지 않고 메모리에 상주하고 있어 재사용될 수 있다는 의미다.
  • 최근 목록 앱에 액티비티가 표시되더라도 액티비티는 미실행 상태일 수 있다. 최근 목록 앱은 파일 형태로 저장되어 단말 재부팅 후에도 히스토리 관리를 위해 유지되기 때문이다.
  • 액티비티 위에 다이얼로그를 표시하는 경우는, 생명주기 변화가 없다. 액티비티 위에 액티비티가 뜨는 경우에만, 변화가 있다.
  • 액티비티 컴포넌트의 onDestroy() 메소드가 호출되지 않고도, 자원 부족으로 인해 컴포넌트가 사라지기도 한다.

참고

java.lang.IllegalStateException: Failure saving state: … has target not in fragment manager: …

프레그먼트를 사용하다보면, 아래와 같은 에러를 만날 수 있다.

Shutting down VM
FATAL EXCEPTION: main
Process: me.sunphiz.android.fragment, PID: 18882
java.lang.IllegalStateException: Failure saving state: CalleeFragment{68fee09 #7 CalleeFragment} has target not in fragment manager: CallerFragment{9c5480e}
    at android.support.v4.app.FragmentManagerImpl.saveAllState(FragmentManager.java:1843)
    at android.support.v4.app.FragmentController.saveAllState(FragmentController.java:134)
    at android.support.v4.app.FragmentActivity.onSaveInstanceState(FragmentActivity.java:566)
    at android.support.v7.app.AppCompatActivity.onSaveInstanceState(AppCompatActivity.java:498)
    at android.app.Activity.performSaveInstanceState(Activity.java:1474)
    at android.app.Instrumentation.callActivityOnSaveInstanceState(Instrumentation.java:1317)
    at android.app.ActivityThread.callCallActivityOnSaveInstanceState(ActivityThread.java:5398)
    at android.app.ActivityThread.performStopActivityInner(ActivityThread.java:4700)
    at android.app.ActivityThread.handleStopActivity(ActivityThread.java:4774)
    at android.app.ActivityThread.access$1400(ActivityThread.java:222)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1819)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:158)
    at android.app.ActivityThread.main(ActivityThread.java:7229)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)

먼저 로그를 옮기자면, CalleeFragment가 프레그먼트에 없는 프레그먼트를 타겟(target)으로 가지고 있어, 상태를 저장할 수 없다는 뜻이다.

“Failure saving state”로그를 보자. CalleeFragment는 라이프사이클에 따라 onSaveInstanceState()가 호출되었다. onSaveInstanceState()는 프래그먼트가 포그라운드(foreground)에서 물러날 때, onPause() ~ onDestroy() 사이에 호출되는 메소드로, 개발자에게 프레그먼트의 상태나 데이터를 저장할 수 있는 기회를 주는 메소드다. 이 때, 문제가 생겨 위와 같은 에러가 발생했다.

“CalleeFragment{68fee09 #7 CalleeFragment} has target not in fragment manager: CallerFragment{9c5480e}”로그를 살펴보자. CalleeFragment의 setTargetFragment()를 호출할 때, 프레그먼트의 인스턴스를 건넸기 때문에 타겟 프레그먼트가 처음부터 생성되지 않았을리는 없다. 그보다는, FragmentManager.replace()나 remove() 메소드로 제거되었을 것이다.

이에 대한 해결책으로는 크게 두 가지가 있다. 하나는, CalleeFragment의 onSaveInstanceState()가 호출 될 때, 이 메소드 안에서 setTargetFragment( null, -1 ) 줄을 추가하여 타겟을 날려버리는 것이다. 다른 하나는, 프레그먼트의 흐름을 파악하여, 적절한 곳에서 remove(), replace() 등을 해주는 것이다.

앞에서 설명한 두 가지 방법 중 좋은 방법은 당연히 후자이다. onSaveInstanceState()는 프레그먼트의 라이프 사이클 중 일부로 당연히 호출되는 것이다. 이를, 감안하여 개발해야지 암의로 초기화 하는 미봉책으로 수정하는 것은 대부분의 경우 다른 사이드 이펙트를 발생시킨다. 예를 들어 targetFragment를 초기화 해버리면, getTargetFragment().onActivityResult()와 같은 메소드를 더이상 호출할 수 없게 된다.

참조