Tech & Programming/모바일(Android, Flutter)

구성변경(Configuration)에 따른 엑티비티 재시작

소스코드 요리사 2019. 9. 13. 15:00

이전 글 :

2019/09/13 - [Tech & Programming/안드로이드 & 모바일] - 안드로이드 가로/세로모드 고정하는 방법


2. 구성변경(Configuration)에 따른 엑티비티 재시작 

  이 전 포스팅(2019/09/13 - [Tech & Programming/안드로이드 & 모바일] - 안드로이드 가로/세로모드 고정하는 방법) 에서 언급한 것처럼 화면방향, 휴대폰 Locale 등이 변경되어 Activity 를 재시작하면 하나의 인스턴스를 가지고 재사용 하는 것이 아니라, 기존 인스턴스는 onDestroy()까지 실행하고 새 인스턴스가 onCreate() 부터 새로 시작됩니다.

 

1) 구성 한정자란?

  구성(Configuration) 은 Actvity를 비롯한 컴포넌트에서 어떤 리소스를 사용할 지 결정하는 조건이라고 생각하면 됩니다.

리소스 폴더를  /res/value-ko , /res/vaule-en 구성하고, String.xml 파일을 만들면 언어설정에 따라 한글/영어로 보여주게 할 수 있으며, res/layout-port, res/layout-land 로 구성하면 가로/세로에 따라 화면 레이아웃을 다르게 줄 수 있습니다.

 

이 때 value, layout 폴더에 붙는 ko, en, port, land 따위가 구성한정자 입니다.

자세한 사항은 아래 안드로이드 개발자 사이트를 참조하시면 쉽게 이해 하실 수 있을 겁니다.

https://developer.android.com/guide/topics/resources/providing-resources?hl=ko

 

2) 데이터 복구가 필요해~!

  구성 변경 시 새 인스턴스가 onCreate() 부터 새로 생성되기 때문에 기존 Data(View 안의 Data 각 멤버변수 등) 들은 초기화 되게 됩니다.

예를 들면 Editbox 에 문자가 입력된 상태이고, 이 Editbox의 입력된 문자가 Activity 내 멤버변수로 저장되어 있다면 화면 방향전환과 같은 구성변경이 일어나면 Editbox와 멤버변수의 Data는 초기화가 됩니다.

사용자 경험 상 기존에 보던 화면을 유지하는게 좋기 때문에 기존에 보던 화면에서 이후 UX를 이어갈 수 있도록 Editbox와 멤버변수의 DATA를 구성변경 전과 동일하게 되도록 onSaveInstanceState() 메소드를 사용하는 것이 좋습니다.

 

  onSaveInstanceState() 에 전달되는 Bundle 파라메터는 onCreate() 에서도 전달되지만, 보통은 호출 시점을 대칭되게 만들기 위해서 onRestoreInstanceState() 에서 사용하여 복구를 주로 합니다.

  onSaveInstanceState() 는 targetSdkVersion이 11 미만이면 onPause() 이전에 호출되고, 11이상에서는 onStop() 이전에 호출됩니다. onRestoreInstanceState() 는 onCreate() 메서드 이후, onResume() 이전에 호출됩니다.

 

  Fragment 역시 onSaveInstanceState()를 제공하고 있으며, onActivityCreated() 또는 onViewStateRestored() 를 통해 복구 할 수 있습니다.

 

  그리고, 한가지 기억해야할 것은 포그라운드 상태일 때 구성변경이 일어나야 복구가 진행됩니다.

예를 들면 Activity A 에서 Activity B를 전환하고 나서 방향전환을 하면 방향전환 시 onSaveInstanceState() 가 2번 호출 되는 것이 아닙니다.

 

  -. Activity A (세로) -> Activity B (세로) : Activity A의 onStop() 이 일어나기전에 onSaveInstanceState() 가 호출.

  -. Activity B 가로 전환 후 :  방향전환 시에는 포그라운드 상태인 Activity B의 onSaveInstanceState() 가 호출되고

      Activity B가 재시작 됨.

  -. Activity B를 세로 전환 후 Activity B Back버튼 눌러 finish.

      Activity A 전환 되지만 Activity B를 호출하기 전과 같은 세로 상태이기 때문에 Data 복구 및 재시작 안함. 

  -. 만약, Activity B finish 후 Activity A 전환 시 가로 상태로 구성변경이 일어나면 Activity A를 재시작하게 되어, onStop 이전에 호출된 onSaveInstanceState의 파라메터 값으로 복구할 수 있습니다.

 

글로만 읽을 때는 간단하다고 생각할 수 있으나 겹쳐지는 Activity가 많아지고, Fragment도 많아지는 경우에 방향전환과 같은 구성변경이 일어나서 재생성 주기를 타게 되면 생각보다 Data 복구 문제가 어렵게 다가오게 됩니다. 따라서, 생성주기에 따라 호출되는 메소드들을 잘 알고 있어야 햇갈리지 않습니다.

 

최근에는 Android jetpack 의 ViewModel이나 LiveData 를 이용하면 복잡한 객체 DATA를 복구하거나 Bundle에 하나하나 Put하고 get 하는 수고를 덜 수 있습니다. 자세한 활용법은 추후 샘플 예제들을 통해 블로깅 한번 하도록 하겠습니다.

 

3) 메모리 누수의 가능성

Activity가 종료되고, 재시작되면 알아서 GC가 동작해서 메모리를 회수하게 됩니다. 그런데, Activty에 대한 참조가 남아 있다면 이 Activity는 회수되지 않을 것입니다. 따라서, 아래에 대표적인 메모리 누수 사례를 소개합니다.

 

(1) Activity 를 Collection에 모아두는 행위

    Activity를 Collection에 저장을 해두고 Activity 종료 시에 삭제해야하는데 이를 잊기 쉬워, 쉽게 누수가 일어납니다.

(2) Activity 내부 클래스나 익명 클래스 인스턴스가 Activity 에 대한 참조를 가지고 있는 경우

    Activity 참조를 없애기 위해서 내부 클래스에 static을 붙이거나 WeakReference를 사용하는 것이 좋습니다. 그리고, 내부클래스를 외부에서 리스너로 등록하는 등과 같이 인스턴스화 해서 사용한 경우에도 해제 해야합니다.

(3) 싱글톤에서 Context가 아닌 Activity 자신을 전달하는 행위

(4) AsyncTask에서 Activity 참조 

    AsyncTask를 Activity 내부에 만드는 경우가 많습니다.

    이 때 AsyncTask 내부에서 Activity를 참조하게 되면 onDestroy 후에 AsyncTask 실행시간이 길어지면 Activity는 회수되지 않습니다.

    따라서, Activity가 onDestroy() 될 때 AsyncTask를 cancel 시키는 시나리오를 구현을 하는 것이 좋습니다.

 

4) AndroidManifest.xml 파일의 android:configChanges 속성

  위 1), 2) 의 설명처럼 안드로이드는 기본적으로 구성(configuration) 변경 시 실행 중인 Activity를 재시작 하는 것이 기본입니다.

하지만, onCreate 나 onResume에서 서비스 실행이나 DB 연결 등의 선행작업이 많거나 구성(configuration) 변경 시 직접 컨트롤 하겠다고 하면 아래와 같이 AndroidManifest.xml 파일에 android:configChanges 속성을 추가하면 됩니다.

<activity
     android:name=".TestActivity"
     android:label="@string/app_name"
     android:theme="@style/AppTheme.NoActionBar"
     android:configChanges="orientation">
 </activity>

이렇게 configChanges 에 구성을 추가하면 이 추가한 구성변경이 일어나도 Activity를 재시작하지 않고, 구성변경으로 인한 리소스 교체 및 기타 Action 들을 직접 컨트롤하게 됩니다.

 

configChanges 에는 | 기호로 여러개 입력이 가능한데, 입력가능한 값과 설명은 아래 사이트를 참조하면 됩니다.

https://developer.android.com/guide/topics/manifest/activity-element.html?hl=ko#config

 

5) onConfigurationChange() 메소드 활용

위 android:configChanges 에 입력한 구성변경이 일어날 경우 Activity를 재시작하는 대신에 onConfigurationChange() 만 호출됩니다.

아래는 화면 방향에 의해 구성변경이 일어났을 때 예시입니다.

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    
	// 아래 부분에 가로/세로 모드 별로 리소스 재정의나 행동들을 해주면 된다.
    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
    	mTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
        
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
    	mTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 21);
        
    }
}

아래는 가로, 세로 별로 dimes.xml 에 View의 width 크기를 다르게 지정해뒀다면 onConfigurationChange 에서 아래와 같이 적용할 수 있있습니다. 그러면, onConfigurationChange가 호출될때 구성한정자에 맞는 dimens.xml의 값을 읽어와서 mTest뷰에 적용하게 됩니다.

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    
    ViewGroup.LayoutParams lp = mTest.getLayoutParams();
    lp.width = getResources().getDimensionPixelSize(R.dimen.sample_width);
    mTest.setLayoutParams(lp);
}

그리고, 만약에 onConfigurationChange()에서 구성한정자에 맞는 layout xml 파일을 재정의 하는 등의 행위를 하게되면 findViewById로 객체를 받아 리스너를 달거나 객체에 적용한 것들을 새로 적용해야합니다.

 

상세한 사항은 아래 안드로이드 developer 사이트를 참조하시면 더 상세하게 알 수 있습니다.

https://developer.android.com/guide/topics/resources/runtime-changes.html?hl=ko#HandlingTheChange

 

참고서적 : 안드로이드 프로그래밍 Next Step