Regular Motion

개발자가 상팔자

[Android] Fragment Transactions & Activity State Loss

이 글은 http://www.androiddesignpatterns.com에 소개된 글을 저자의 동의를 얻고 번역한 것임을 알려드립니다.

원문 : http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html

 

Honeycomb이 최초로 배포된 이후로 아래의 Stack trace와 Exception message는 Stack Overflow에 지속적으로 올라왔다.

이 글은 위의 Exception이 언제 그리고 왜 발생하는지 알아보고 몇가지 해결 방안을 제시할 것이다.

 

Why was the exception thrown?

위의 Exception은 Activity의 상태가 저장된 이후 FragmentTransaction을 commit하려고 할 때, Activity State Loss 현상이 발생하고 그에 따른 결과로 발생한다. Activity State Loss가 정확히 뭘 의미하는지 알아보기 전에, onSaveInstanceState()가 호출될 때 어떠한 작업이 내부에서 이루어지는지 알아보자.

지난번 Binders & Death Recipients 포스팅에서 언급했듯이, 안드로이드 실행 환경에서 어플리케이션은 자신의 운명에 대해 약간의 권한만을 갖고 있다. 안드로이드 시스템은 메모리를 회수하기 위해 아무때나 백그라운드 액티비티를 종료시킬 수 있는 권한을 갖고 있다.

이런 불규칙한 일을 사용자가 눈치채지 못하도록 하기 위해, 안드로이드 프레임워크는 Activity를 종료시키기 전에 자신의 상태를 저장할 수 있도록 onSaveInstanceState() 메소드를 호출해준다. 그리고 이후 Activity가 Restore될 때 저장한 상태 값들을 통해 상태를 복원한다.

프레임워크가 onSaveInstanceState()를 호출할 때, dialog, fragment, view들의 상태를 저장할 수 있도록 Bundle object를 전달한다. 이후 Activity가 메소드를 반환할 때 시스템은 Bundle object를 System server process에 안전하게 저장한다.

이후 시스템이 Activity를 재생성 할 때, Bundle을 Activity에게 돌려주고 돌려받은 Bundle 통해 Activity의 마지막 상태를 복원한다.

근데 왜 Exception이 발생하는가? Bundle object는 Activity의 onSaveInstanceState()가 호출될 때의 snapshot과 같다. 그런데 onSaveInstanceState()가 호출된 이후에 FragmentTransaction#commit()을 호출한다면 해당 transaction은 snapshop에 저장되지 않을 것이다.

사용자의 관점에서보면, 해당 transaction이 유실되어 UI State가 유실된 것처럼 보여질 것이다.  Android는 User Experience를 지키지 위해 State Loss가 발생 할 때마다 IllegalStateException을 발생시켜 이를 방지한다.

 

When is the exception thrown?

이미 위의 Exception을 경험했다면, Platform의 버전에 따라 Exception의 발생 시점이 불규칙하다는 사실을 눈치 챘을수도 있다.

예를 들어 오래된 단말기에서는 자주 발생하지 않는다던가, 정식 Framework Class를 사용할 때보다 support library를 사용할 때 더 자주 발생한다던가. 위와 같은 불규칙으로 인해 많은 사람들은 support library에 버그가 많은것 아니냐? 믿을 수 없다. 라는 등의 의심을 해왔다. 하지만 이는 사실이 아니다.

위와 같은 불규칙이 발생하게된 원인은 Honeycomb때 생긴 Acitivty Lifecycle의 중요한 변화와 관련이 있다. Honeycomb 이전에 Activity는 onPause() 함수 이후에 killable 상태로 변경되었다. 하지만 Honeycomb 부터 Activity는 onStop() 이후에 killable 상태로 변경되었다. 그리고 이는 onSaveInstanceState() 메소드가 onPause() 전이 아닌 onStop() 전에 호출되도록 변경됐다는 것을 의미한다. 아래 표에 차이점이 정리되어 있다.

Screen shot 2015-03-15 at 오후 11.47.34

Activity Lifecycle과 관련된 변화로 인해, support library는 Platform 버전에 따라 다르게 동작해야 할 필요가 있다.

예를 들어, Honeycomb 이후의 기기에 대해서는 onSaveInstanceState() 이후의 시도하는 모든 commit()에 대해서 exception을 발생시킬 필요가 있다.  그러나 Honeycomb 이전의 기기에 대해 동일하게 Exception을 발생시키는 것은 지나치게 제한적인 측면이 있다. 따라서 Android 팀은 아래와 같이 정리했다.

Screen shot 2015-03-15 at 오후 11.55.55

 

How to avoid the exception?

실제로 어떠한 작업들이 이루어 지는지 이해하고나면 Activity State Loss를 방지하는 일은 훨씬 수월하다.  만약 여기까지 이해했다면 support library가 어떻게 동작하는지 그리고 어플리케이션을 개발할 때 State Loss를 예방하는게 얼마나 중요한지 이해할 수 있을 것이다.

만약 이 글을 Quick fix 용도로 활용한다면 FragmentTransaction을 사용할 때의 유의사항이 아래와 같다.

  • – Be careful when committing transactions inside Activity lifecycle methods. 대부분의 어플리케이션에서 transaction commit은 onCreate() 메소드 또는 사용자의 입력에 대한 response로 이루어지고 이는 아무런 문제를 발생시키지 않을 것이다. 하지만 onStart(), onResume()과 같은 Activity의 다른 lifecycle 메소드에서 transaction commit이 이루어진다면 조금 까다로워진다. 예를 들어 FragmentActivity#onResume() 메소드는 Activity state가 복원되기 전에 호출될 수 있기 때문에 transaction commit을 하지 말아야 한다. 만약 어플리케이션이 onCreate()가 아닌 다른 lifecycle 메소드에서 transaction을 commit 해야 한다면 onResumeActivity() 또는 onPostResume() 메소드를 활용해라. 위의 두개의 메소드는 Activity의 상태가 복원된 이후에 호출됨을 보장하기 때문에 Activity State Loss를 방지할 수 있다.
  • – Avoid performing transactions inside asynchronous callback methods. 일반적으로 많이 사용하는 AsyncTask#onPostExecute()LoaderManager.LoaderCallbacks#onLoadFinished()를 포함한 Asynchronous의 callback에서는 Activity lifecycle과 관련한 정보를 갖고 있지 않기 때문에 transaction을 commit하는 것은 위험할 수 있다.
    구글 개발자를 포함한 모두가 Asynchronous callback에서 transaction을 commit하지 않는게 가장 바람직하다는 사실에는 동의한다.
    Asynchronous callback은 onSaveInstanceState() 메소드 이후에 호출되지 않는걸 보장하지 않기 때문에 commitAllowingStateLoss()를 사용해야만 할지도 모른다. (commitAllowingStateLoss() 함수는 State Loss가 발생할 수 있는 상황에서 예외를 발생시키지 않는다.)
  • – Use commitAllowingStateLoss() only as a last resort. commit() 메소드와 commitAllowingStateLoss() 메소드의 유일한 차이점은 commitAllowingStateLoss() 메소드의 경우 State Loss가 발생해도 Exception을 발생시키지 않는다는 것이다. commitAllowingStateLoss() 메소드는 state loss가 발생할 수 있음을 함축하고 있기 때문에 일반적으로는 사용하지 않는다. 최선책은 Activity의 상태가 저장되기 전에 commit() 메소드를 사용하는 것이다.

 

 

 

1 Comment

  1. REGULARMOTION

    2015년 8월 13일 at 2:21 오후

    으앜 누추한 곳까지 방문 후 읽어주셔서 감사합니다!

답글 남기기

© 2017 Regular Motion

Theme by Anders NorenUp ↑