[Android] Activity의 onCreate(...)는 왜 2개일까? (PersistableBundle)

부나·2023년 9월 18일
7

안드로이드

목록 보기
4/12
post-thumbnail

Activity 코드를 작성하다보면, 한 번쯤은 onCreate() 콜백 메서드를 본 적이 있을 것이다.
안드로이드 Activity의 대표적인 생명주기인 onCreate()는 사실 두 종류 가 존재한다.

첫 번째 fun onCreate(...)

아래 코드를 살펴보자.

override fun onCreate(savedInstanceState: Bundle?)

우리가 흔히 마주하는 Activity의 생성 을 알리는 메서드이다.
액티비티가 파괴되기 전까지는 오직 한 번만 호출된다.
따라서, onCreate() 에서 View Inflate 를 진행하고 Activity에서 사용하기 위한 객체를 초기화한다.

 override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
}

코드에서 볼 수 있다시피 setContentView(View) 를 호출하여, 액티비티에서 보여줄 View를 Inflate 있다.


두 번째 fun onCreate(...)

override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?)

반면, 인자가 조금 다른 다음 메서드를 살펴보자

안드로이드 개발을 어느정도 해본 사람이라면, onCreate()를 오버라이드 하려다가 위 메서드를 잘못 추가하여, setContentView(...) 를 호출해도 화면이 그려지지 않고 앱이 강제 종료 되는 상황을 경험해보았을 것이다.

처음 본 onCreate()와 얼핏 비슷해보이지만, PersistableBundle 이 인자로 추가되어 있다.

PersistableBundle

위 메서드를 이해하기 위해서는 PersistableBundle 에 대한 이해가 있어야 한다.
Persistable 을 번역기에 입력해보면 지속 가능한 이라는 의미로 해석된다.
이름 그대로, 지속 가능한 Bundle임을 의미한다.

공식 문서에 따르면, 아래와 같이 설명되고 있다.

문자열 키에서 다양한 유형의 값으로의 매핑입니다. 이 클래스에서 지원하는 유형 세트는 의도적으로 디스크에서 안전하게 유지되고 복원될 수 있는 간단한 개체로 제한됩니다.

일반적인 Bundle은 물리 메모리 에서 관리되다가, 애플리케이션 종료 또는 액티비티 종료 상황이 되면 GC(Garbage Collection) 에 의해 수거되거나 메모리 할당 해제 된다.

하지만 PersistableBundle 은 물리 메모리가 아니라, 디스크 에서 관리된다.
간단하게 내부 코드를 살펴보면 다음과 같다.

public final class PersistableBundle
	extends BaseBundle
	implements Cloneable,Parcelable, XmlUtils.WriteMapCallback {
    ...
    
    @Nullable
    public static byte[] toDiskStableBytes(@NonNull PersistableBundle bundle) throws IOException {
        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        bundle.writeToStream(outputStream);
        return outputStream.toByteArray();
    }
	
    public void writeToStream(@NonNull OutputStream outputStream) throws IOException {
        TypedXmlSerializer serializer = Xml.newFastSerializer();
        serializer.setOutput(outputStream, UTF_8.name());
        serializer.startTag(null, "bundle");
        try {
            saveToXml(serializer);
        } catch (XmlPullParserException e) {
            throw new IOException(e);
        }
        serializer.endTag(null, "bundle");
        serializer.flush();
    }
    
    public void saveToXml(XmlSerializer out) throws IOException, XmlPullParserException {
        saveToXml(XmlUtils.makeTyped(out));
    }

	...
}

PersistableBundle의 핵심 메서드는 위 3가지이다.

toDiskStableBytes(PersistableBundle) 을 호출하면, PersistableBundleOutputStream을 통해 Disk 에 쓰여진다는 사실을 알 수 있다.

이 때, writeToStream(OutputStream), saveToXml(XmlSerializer) 가 호출되는데, 이름에서 유추할 수 있는 것처럼 Xml 형태로 Disk에 읽기 쓰기된다.

따라서 앱 종료 후에도 영속적으로 저장해야 하는 간단한 데이터는 PersistableBundle를 통해 관리할 수 있다.

생명주기에 따른 콜백 실험

실험해본 결과, 해당 함수는 아무런 설정을 하지 않으면 호출되지 않는다.
또한 호출되는 시점이 꽤나 흥미로웠다.

<activity
    android:name=".MainActivity"
    android:persistableMode="persistAcrossReboots"/>

Manifest에서 위 메서드가 호출되기를 원하는 Activity에 persistableMode 속성 값으로 persistAcrossReboots 을 설정해주어야 한다.
그렇지 않으면, 두 번째 onCreate() 메서드는 호출되지 않는다.

onCreate

onCreate() 가 언제 호출되는지 알아보기 위해 아래 테스트용 코드를 작성해볼 것이다.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        Log.d("buna", "persist onCreate")
        super.onCreate(savedInstanceState, persistentState)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        Log.d("buna", "onCreate")
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
    ...

이렇게 두 메서드를 오버라이드 해놓고 앱을 실행해보자.

재밌게도 첫 번째 onCreate() 메서드만 호출되었다.
이렇게 보고 나니, 왜 두 번째 onCreate() 에서 setContentView() 를 설정해주면 화면을 그리지 못하는지 알 수 있었다.

onSaveInstanceState

다음으로, PersistableBundle 에 값을 저장하기 위해 실험해볼 메서드는 onSaveInstanceState() 이다.

override fun onSaveInstanceState(outState: Bundle) {
    Log.d("buna", "normal save")
    Log.d("buna", "############################")
    super.onSaveInstanceState(outState)
    
    // Bundle에 저장
    outState.putString("normal in normal", "normal in normal")
}

override fun onSaveInstanceState(
	outState: Bundle,
    outPersistentState: PersistableBundle
) {
	Log.d("buna", "persist save")
    Log.d("buna", "############################")![](https://velog.velcdn.com/images/buna1592/post/2bb6041f-44ae-4352-8a83-f9494e183950/image.png)
    super.onSaveInstanceState(outState, outPersistentState)

	// Bundle에 저장
    outState.putString("normal in persist", "normal in persist")
    outPersistentState.putString("persist in persist", "persist in persist")
}

onCreate() 실험과 동일하게, 구분을 위해 PersistableBundle 을 가진 메서드아닌 메서드 를 모두 호출해보겠다.

위 코드에서는 다음과 같이 로깅하고 있다.

  • normal : Bundle 만 가지고 있는 메서드
  • persist : PersistableBundle 을 가지고 있는 메서드

그리고 화면을 회전시켜 구성 변경 을 시켜보겠다.

호출되는 순서는 fun onSaveInstanceState(Bundle, PersistableBundle) 이 먼저 호출된다.
그리고 해당 함수에서 fun onSaveInstanceState(Bundle) 를 호출하고 있다.

이제 저장한 값들을 복원해보는 실험을 해보자.

onRestoreInstanceState

override fun onRestoreInstanceState(savedInstanceState: Bundle) {
    Log.d("buna", "normal restore : ${savedInstanceState.getString("normal in normal")}")
    Log.d("buna", "normal restore : ${savedInstanceState.getString("normal in persist")}")
    Log.d("buna", "############################")
    super.onRestoreInstanceState(savedInstanceState)
}

override fun onRestoreInstanceState(
    savedInstanceState: Bundle?,
    persistentState: PersistableBundle?,
) {
    Log.d("buna", "persist restore : ${savedInstanceState?.getString("normal in normal")}")
    Log.d("buna", "persist restore : ${savedInstanceState?.getString("normal in persist")}")
    Log.d("buna", "persist restore : ${persistentState?.getString("persist in persist")}")
    Log.d("buna", "############################")
    super.onRestoreInstanceState(savedInstanceState, persistentState)
}

구성 변경 으로 인해 액티비티가 파괴되면서 onSaveInstanceState() 가 호출되었던 것을 봤다.
그리고 다시 Activity가 재생성되면서 onRestoreInstanceState() 를 통해 액티비티의 상태를 복원할 것이다.

한 번 로그를 찍어보자.

onRestoreInstanceState() 도 마찬가지로 PersistableBundle 을 가지고 있는 메서드가 우선 호출된다.
결과를 보면, onRestoreInstanceState(Bundle)BundleonRestoreInstanceState(Bundle?, PersistableBundle?)Bundle 은 서로 같다는 사실을 알 수 있다.
그리고 PersistableBundle 의 데이터도 정상적으로 가져옴을 확인할 수 있다.

그렇다면, fun onCreate(Bundle, PersistableBundle) 은 언제 호출될까?

onCreate(Bundle, PersistableBundle) 호출 시기

위 함수는 일반적인 상황에서는 호출되지 않는다.
PersistableBundle 에 대한 추가적인 설명이다.

활동이 이전에 종료되거나 전원이 꺼진 후 다시 초기화되는 경우 이 번들은 onSaveInstanceState에 가장 최근에 제공한 데이터를 포함합니다.
참고: 그렇지 않으면 null 입니다.

즉, 전원을 종료 했다가 다시 전원을 켰을 때 PersistableBundle 에 저장했던 데이터들을 불러온다.
늘 그랬듯이 한 번 실험해보자.

실제 과정을 영상으로 올리고 싶지만 너무 긴 관계로 실험 순서 만 공유하겠다.

  1. 위 코드대로 ActivityManifest코드를 작성 하고 앱을 실행 시킨다.
  2. 휴대폰을 종료한다.
  3. 재부팅하고 다시 앱을 실행시킨다.

그럼 아래와 같은 순서로 로그가 출력된다.

그냥 앱을 실행시켰을 때와 달리, persist onCreate 가 출력되었고, 맨 아래에 PersistableBundle 에 저장했던 persist in persist 값까지 정상 출력되고 있다.
그 외에 일반 Bundle 의 값은 당연히 메모리에서 소멸되었기 때문에 null 을 나타낸다.

삽질 TIP. AVD 에서 실험했을 때에는 잘 안 될 수 있기 때문에, 실제 기기 로 테스트하는 것을 권장한다.

글을 마무리하며..

onCreate(Bundle) 에 대해 다룬 글은 많지만, onCreate(Bundle, PersistableBundle) 은 다룬 글은 (내 기준) 아예 보지 못했다.

보통 onCreate() 를 호출해도 화면이 그려지지 않는 버그! 라는 주제와 함께 아래의 내용으로 매우 짧게 포스팅이 마무리되는 경우가 많았다.

onCreate(Bundle) 로 변경하세요.

이러한 이유로, 직접 실험을 해보았고 그제서야 무엇인지 파악할 수 있었다.
일반적으로 Disk에 저장하기 위해서는 Database 또는 가벼운 데이터의 경우 SharedPreference 에 자주 저장하기 때문에, 아직까지는 PersistableBundle 의 필요성은 느끼지 못하고 있다.


전체 코드

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        Log.d("buna", "persist onCreate")
        super.onCreate(savedInstanceState, persistentState)

        Log.d("buna", "${persistentState?.getString("persist in persist")}")
        Log.d("buna", "############################")
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        Log.d("buna", "onCreate")
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

    override fun onSaveInstanceState(outState: Bundle) {
        Log.d("buna", "normal save")
        Log.d("buna", "############################")
        super.onSaveInstanceState(outState)

        // Bundle에 저장
        outState.putString("normal in normal", "normal in normal")
    }

    override fun onSaveInstanceState(
        outState: Bundle,
        outPersistentState: PersistableBundle,
    ) {
        Log.d("buna", "persist save")
        Log.d("buna", "############################")
        super.onSaveInstanceState(outState, outPersistentState)

        // Bundle에 저장
        outState.putString("normal in persist", "normal in persist")
        outPersistentState.putString("persist in persist", "persist in persist")
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        Log.d("buna", "normal restore : ${savedInstanceState.getString("normal in normal")}")
        Log.d("buna", "normal restore : ${savedInstanceState.getString("normal in persist")}")
        Log.d("buna", "############################")
        super.onRestoreInstanceState(savedInstanceState)
    }

    override fun onRestoreInstanceState(
        savedInstanceState: Bundle?,
        persistentState: PersistableBundle?,
    ) {
        Log.d("buna", "persist restore : ${savedInstanceState?.getString("normal in normal")}")
        Log.d("buna", "persist restore : ${savedInstanceState?.getString("normal in persist")}")
        Log.d("buna", "persist restore : ${persistentState?.getString("persist in persist")}")
        Log.d("buna", "############################")
        super.onRestoreInstanceState(savedInstanceState, persistentState)
    }
}
profile
망각을 두려워하는 안드로이드 개발자입니다 🧤

0개의 댓글