구성(Configuration)은 컴포넌트에서 어떤 리소스를 사용할지 결정하는 조건이고, 이 조건 항목은 프레임워크에서 정해져 있다.
보통 화면 회전할 시, 액티비티의 생명주기는 onDestroy()
까지 실행되고 다시 onCreate()
부터 실행되는데 이 때의 화면 방향
이 구성의 대표적인 항목이다.
android.content.res.Configuration의 멤버 변수를 나열해보면 아래와 같다
densityDpi, fontScale, hardKeyboardHidden, keyboard, keyboardHidden, locale, mcc, mnc, navigation, navigationHidden, orientation, screenHeightDp, screenLayout, screenWidthDp, smallestScreenWidthDp, touchScreen, uiMode
여기서 fontScale과 locale은 단말의 환경 설정에서 정할 수 있는 사용자 옵션이고 나머지는 단말의 현재 상태이다.
구성은 컴포넌트에서 사용하는 리소스를 결정하기 때문에, 구성이 변경되면 컴포넌트에서 사용하는 리소스도 변경된다.
만약 한국어에서 일본어로 언어를 바꾸는 거라면?
/res/values-ko/string.xml 대신 /res/values-ja/string.xml의 문자열로 변경해서 화면에 보여주는 것이다.
하나씩 문자열을 찾아서 변경하는게 아니라 액티비티를 재시작해서 변경되는 리소스를 사용하는 방식이다.
화면 회전도 그에 따라 /res/layout-port
와 /res/layout-land
디렉터리의 레이아웃을 교체하려면 액티비티를 재시작한다. 참고로 액티비티 외에 다른 컴포넌트는 구성이 변경되도 재시작하지 않는다.
구성이 변경되어 액티비티를 재시작하면 하나의 인스턴스를 가지고 새로 초기화해서 재사용하는 것이 아니다. 기존 인스턴스는 onDestroy()
까지 실행하고 새 인스턴스가 onCreate()
부터 실행하는 것이다.
그렇게 액티비티가 재시작하면서 메모리 누수 문제가 발생할 수 있다. onDestroy까지 액티비티가 불리어도 이 액티비티에 대한 참조가 남아있다면 GC 되지 않고 메모리를 계속 차지한다.
액티비티 목록 참조
액티비티 목록은 시스템이 알아서 관리하는 영역이기도 하고, 액티비티가 종료할 때 컬렉션에서 제거해야 하는데 실수로 빠뜨릴 가능성이 있기 때문에 따로 떠있는 액티비티 가지고 특별한 작업을 하겠다고 컬렉션에 모아두지 말자
액티비티의 내부 클래스나 익명 클래스 인스턴스가 액티비티에 대한 참조를 갖고 있다면 이들 인스턴스를 외부에 리스너로 등록한 경우에 해제도 반드시 되어야 한다.
싱글톤에서 액티비티 참조
싱글톤에 Context가 전달되어야 하는데 Activity 자신을 전달하는 경우에도 메모리 누수가 생길 수 있다. 이 때 applicationContext를 사용하거나 싱글톤 내부에서 context의 applicationContext를 뽑아서 쓰자.
구성이 변경되면 system_server
에서 동작하는 ActivityManagerService에서 앱 프로세스의 메인 클래스인 ActivityThread에 새로운 Configuration을 전달한다.
AsserManager을 거쳐서 Configuration에 맞는 리소스를 매번 선택해서 가져온다. 이 리소스 선택 로직은 내부적으로 최적화가 되어 있고 Providing Resources
문서에서의 Android가 가장 잘 일치되는 리소스를 찾는 방법을참고하자.
https://developer.android.com/guide/topics/resources/providing-resources?hl=ko#AlternativeResources
표를 참고하자!
구성 변경으로 액티비티가 재시작되어도 사용자 경험상 기존에 보던 화면을 유지하는 게 좋다.
이 때 상태를 임시 저장하고 복수하는 메서드인 onRestoreInstanceState()
와 onSaveInstanceState()
를 사용하면 된다.
참고로 onSaveInstanceState()
는 생명주기 메서드처럼 항상 호출되는 것이 아니다. 로그를 남겨서 실행 여부를 살펴보면, 구성이 변경되는 조건에서 (ex. 화면 회전) onSaveInstanceState()
가 호출되고 Activity는 onCreate부터 새로 시작한다.
onSaveInstanceState()
메서드는 targetSdkVersion에 따라 호출되는 시점이 다르다.
타겟팅된 버전이 11 미만이면 onPause()
이전에 호출되고 11 이상에서는 onStop()
이전에 호출된다.
onRestoreInstanceState()
는 onCreate() 메서드 이후, onResume() 메서드 이전에 호출된다.
Activity A -> Activity B 로 전환하고 화면을 회전하면 2개의 액티비티에서 한꺼번에 onSaveInstanceState()
가 호출될까? 그렇지 않다.
회전하는 순간에는 B에서만 호출된다. A -> B로 액티비티를 전환하면 A는 onStrp()
이 호출되는데, onStop()
이 전에 onSaveInstanceState()
가 호출되기 때문이다. 즉, Activity B가 포그라운드에 있을 때는 A에서는 이미 호출된 상태이다.
그럼 화면 회전에 대응하기 위해 A도 재시작될까? 그렇지 않다.
화면에 보이지 않는 액티비티가 아니면 재시작할 필요가 없다.
back 키로 B를 종료하면 onSaveInstanceState()
가 이미 호출된 A는 이제서야 재시작한다.
Q) 만약 A에서 다이알로그 액티비티로 액티비티 전환하는 경우는 어떨까?
DialogActivity
가 뜨면 아직 배경으로 보이는 Activity A의 onSaveInstanceState()
가 호출된다. 이 때 화면을 회전하면 DialogActivity
에서 onSaveInstanceState()
호출 후 재시작하고 그 이후에 A를 재시작한다.
구성이 변경되어도 액티비티를 재시작하지 않는 옵션이 있다. 바로 android:configChanges
라는 옵션이다.
이 속성을 넣으면 onConfigurationChanged()
를 오버라이드해서 직접 처리하겠다는 의미이다.
onConfigurationChanged()
가 불린 이후에는 화면을 다시 그린다. 이 메서드를 따로 오버라이드하지 않아도 액티비티의 기본 onConfigurationChanged() 메서드가 불리고서 다시 그린다.
보통 이 속성에 넣는 가장 흔한 항목으로는 화면 회전에 대응하는 orientation
이 있다. layout-port
, layout-land
디렉터리에 별도 레이아웃 리소스를 사용하지 않을 때 많이 쓰인다. 앞에서 얘기했듯이 이 메서드가 실행된 이후에 다시 그려지면서 View의 layout_width나 layout_height가 바뀌지 않아도 그려진 너비와 높이는 계산된 결과에 따라서 달라진다.
textSize 단위로 sp를 쓰지 않는다면, fontScale
도 가능하다.
권장사항대로 단순한 UI에 sp를 써도 되지만 복잡한 경우 디자인 문제로 textSize를 dp로 제한하는 경우가 많다. dp만을 사용한다면 환경 설정에서 글꼴 크기를 변경해도 화면에 영향이 없으므로 fontScale
을 추가해서 불필요하게 재시작하지 않게 할 수 있다.
예를 들면 configChanges=에는 여러개의 항목을 비트 OR로 넣을 수 있다.
orientation|screenSize|keyboardHidden
처럼
비트 OR 값에 모두 포함되어야 액티비티가 재시작하지 않고 onConfigurationChanged() 메서드에서 처리한다. 그렇다면 하나만 조건을 걸지 않고 이렇게 조건을 걸어야하는 이유가 있을까? 여러 조건이 한꺼번에 변경되는 경우가 있을까? 있다.
환경 설정에서 언어를 바꾸고 화면을 회전하고서 액티비티가 포그라운드를 돌아오면 locale
과 orientation
이 한꺼번에 바뀐다. 이 때 android:configChanges
에 orientation
만 있고 locale
이 포함되지 않는다면 액티비티는 재시작한다.
locale도 바뀌었는데 이에 대한 처리하는 방법이 없는 것으로 간주하는 것이다.
이 때문에 버전에 따른 업데이트 라든지, 하드웨어 키보드가 있는 핸드폰 대응이라든지 여러 예시 때문에 함께 붙어다녀야만 하는 항목들이 생겼다.
1)
허니콤 API 레벨 13이상에서는 화면을 회전할 때 화면 크기도 같이 변경된다. 그 이하면 호환 모드로 동작해서 화면 크기가 변경되는 것으로 간주하지 않는다.
앱을 업데이트하면서 targetSdkVersion을 13이상으로 올렸는데 화면 회전이 기존처럼 동작하지 않는다면 이 때문이다. 화면 회전을 정상적으로 처리하고자 한다면 screenSize
도 포함해서 orientation|screenSize
라고 써야한다.
2)
하드웨어 키보드가 있는 모델 때문에 키보드가 단말 뒷면에 있다가 화면을 위로 밀면 화면 하단에 있던 키보드가 나타난다. 이 때 세로 모드로 있었다면 키보드를 꺼내는 순간에 단말을 돌리지도 않았는데 가로 모드로 전환되었다.
keyboardHidden 속성이 변경되면서 화면 방향도 바뀌기 때문에 keyboardHidden
과 orientation
을 함께 쓰게 되었다.
참고로, android:configChanges
에 값을 넣을 때는 최대보단 최소
를 원칙으로 하는게 좋다. 14개(최대 속성 개수)를 다 넣는다고 액티비티가 재시작하는 걸 방지할 수 있는 것도 아니기 때문이다.
백그라운드로 이동했다가 앱이 종료되는 경우도 있기 때문에 완벽하게 대응할 수도 없다.
메이저 업체들의 단말에서는 환경 설정에서 화면 폰트를 바꿀 수도 있다. 이런 구성 변경은 액티비티를 재시작해서 반영할 수 밖에 없고
android:configChange
로 대응이 불가능하다.
android:configChanges
에 값을 넣어도 액티비티가 전환될 때 호출자에서 여전히 onSaveInstanceSate()는 불린다.
빈번한 화면 회전에 대응하기 위해 android:configChanges
를 사용하는 것이 나쁜 선택은 아니다. 다만, 중요한 정보를 어떤 상황에도 유지하기 위해서는 onSaveInstanceState()
와 onRestoreInstanceState()
도 함께 사용하는게 좋다
화면 방향과 관련해서 구성이 바뀌는 것은 포그라운드에 있는 액티비티가 기준이다. 만일 화면 방향 고정인 액티비티가 포그라운드에 있다면 아무리 화면을 회전해도 Application의 onConfigurationChange()
조차 불리지 않는다.
또한, Configuration은 한순간에 1개의 값만 존재한다.
보통 홈 스크린은 방향 고정인 경우가 많아서 홈 화면에서 아무리 화면을 많이 회전한다 해도 다른 앱의 Configuration에 영향을 주지 않는다. 액티비티가 스택에 남아 있는 채로 홈키를 통해 백그라운드를 이동해도 마찬가지다. 포그라운데 있는 액티비티가 아니라면 변경된 Configuration을 받지 않으므로, 다른 앱에서 바뀐 Configuration은 액티비티에 전달되지 않고 Aplication에만 전달된다!
환결 설정에서 언어나 글꼴 크기를 변경할 때도 실행 중인 모든 앱 프로세스에서 Application의 onConfigurationChanged()가 불리고 변경된 Configuration이 전달된다.