Aleksandr Belkin
Aleksandr Belkin

Reputation: 422

Rotating the screen on Android spams saved instance state and ActivityResultRegistry

My app was saving a bigger bundle in activity onSaveInstanceState() each time the screen was rotated, and I came across something really confusing. Here is a minimal example:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)

        val rcs = outState.getIntegerArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_RCS")
        val keys = outState.getStringArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS")

        Log.d("MainActivity", "${rcs?.size}: $rcs, $keys")
    }
}

I get the following output each time the screen is rotated:

3: [1332505437, 1835553837, 670316111], [FragmentManager:StartIntentSenderForResult, ...
6: [1332505437, 91080073, 1835553837, 381123153, 1187376284, 670316111], ...
...

See the full log after 6 rotations: https://pastebin.com/yfE04Fmc

Each time the screen is rotated, 3 elements are added to these two entries. After rotating the screen for a while, the instance state becomes significantly bigger for no apparent reason.

Target API: 30

Tested on: API 28 and 30

Does anyone know why this is happening?

UPDATE: I want to clarify that I do not save any state into the Bundle. The values I show are created by Android itself: in ActivityResultRegistry. I've created an empty project for this example, and the code I provided is the only code I have - there is nothing else that interacts with the state Bundle.

UPDATE 2: Bug report submitted: https://issuetracker.google.com/issues/191893160

UPDATE 3: Bug fixed, will be a part of Activity 1.3.0-rc02 and 1.2.4 releases

Upvotes: 5

Views: 1122

Answers (4)

S Haque
S Haque

Reputation: 7271

Does anyone know why this is happening?

AppCompatActivity is extended from ComponentActivity (AppCompatActivity -> FragmentActivity -> ComponentActivity)

ComponentActivity is responsible for keeping the reference of ViewModelFactory, so that when the Activity recreates if can instantiate the same ViewModel before screen rotation.

Now inside ComponentActivity you will notice this:

@CallSuper
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
    Lifecycle lifecycle = getLifecycle();
    if (lifecycle instanceof LifecycleRegistry) {
        ((LifecycleRegistry) lifecycle).setCurrentState(Lifecycle.State.CREATED);
    }
    super.onSaveInstanceState(outState);
    mSavedStateRegistryController.performSave(outState);
    mActivityResultRegistry.onSaveInstanceState(outState);
}

Here the important part is mActivityResultRegistry.onSaveInstanceState(outState)

Inside this method you will see

public final void onSaveInstanceState(@NonNull Bundle outState) {
    outState.putIntegerArrayList(KEY_COMPONENT_ACTIVITY_REGISTERED_RCS,
            new ArrayList<>(mRcToKey.keySet()));
    outState.putStringArrayList(KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS,
            new ArrayList<>(mRcToKey.values()));
    outState.putStringArrayList(KEY_COMPONENT_ACTIVITY_LAUNCHED_KEYS,
            new ArrayList<>(mLaunchedKeys));
    outState.putBundle(KEY_COMPONENT_ACTIVITY_PENDING_RESULTS,
            (Bundle) mPendingResults.clone());
    outState.putSerializable(KEY_COMPONENT_ACTIVITY_RANDOM_OBJECT, mRandom);
}

So you can see from there that the Android platform itself is saving this key in the bundle. Hence you can not do much about it.

In other word, One of the parents of AppCompatActivity is saving these keys inside the Bundle. If you are not using ViewModel and do not want these keys to be saved; you can remove super.onSaveInstanceState(outState) from MainActivity (NOT RECOMMENDED). You will still get your saved keys but there could be some side effect, I am not aware of.

Good thing that you have reported an issue regarding this, let's see how the Google Team respond to it.

Upvotes: 2

Darkman
Darkman

Reputation: 2981

This may not solve your problem but one thing that you could try is removing the duplicate values. Keep the latest ones and remove the olders. Something like this:

Kotlin

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    removeDuplicates(outState)
}

private fun removeDuplicates(bundle: Bundle) {
    val prevRCS = bundle.getIntegerArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_RCS")
    val newRCS = bundle.getIntegerArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_RCS")
    val newKeys = bundle.getStringArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS")
    if (newRCS == null || newKeys == null) return
    if (prevRCS == null) {
      
  bundle.putIntegerArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_RCS", newRCS)
      
  //bundle.putStringArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS", newKeys)
    } else if(prevRCS.size() != newRCS.size()) {
        for (rcs in prevRCS) {
            val index = newRCS.indexOf(rcs)
            newRCS.remove(index)
            newKeys.remove(index)
    }
    bundle.remove("KEY_COMPONENT_ACTIVITY_REGISTERED_RCS")
    bundle.remove("KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS")
 
   bundle.putIntegerArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_RCS", newRCS)
 
   bundle.putIntegerArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_RCS", newRCS)
 
   bundle.putStringArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS", newKeys)
 
   //bundle.putStringArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS", newKeys)
    }
}

Java

@Override
protected void onSaveInstanceState(Bundle outState)
{
    super.onSaveInstanceState(outState);
    removeDuplicates(outState);
}

private static final void removeDuplicates(final Bundle bundle)
{
    if(bundle == null) return;

    final ArrayList<Integer> prevRCS = bundle.getIntegerArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_RCS");
    final ArrayList<Integer> newRCS = bundle.getIntegerArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_RCS");
    final ArrayList<String> newKeys = bundle.getStringArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS");

    if(newRCS == null || newKeys == null) return;

    if(prevRCS == null) {
 
   bundle.putIntegerArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_RCS", newRCS);
 
   //bundle.putStringArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS", newKeys);
    } else if(prevRCS.size() != newRCS.size()) {
        for(Integer rcs : prevRCS) {
            final int index = newRCS.indexOf(rcs);
            newRCS.remove(index);
            newKeys.remove(index);
        }
    bundle.remove("KEY_COMPONENT_ACTIVITY_REGISTERED_RCS");
    bundle.remove("KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS");
 
   bundle.putIntegerArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_RCS", newRCS);
 
   bundle.putIntegerArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_RCS", newRCS);
   bundle.putStringArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS", newKeys);
 
   //bundle.putStringArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS", newKeys);
    }
}

Upvotes: 0

Gkm
Gkm

Reputation: 96

It all depends on how you store the data in Bundle, according to standard Android developer document https://developer.android.com/guide/components/activities/activity-lifecycle#restore-activity-ui-state-using-saved-instance-state

Saving the state:

 override fun onSaveInstanceState(outState: Bundle?) {
    // Save the user's current game state
    outState?.run {
        putInt(STATE_SCORE, currentScore)
        putInt(STATE_LEVEL, currentLevel)
    }
    
    // Always call the superclass so it can save the view hierarchy state
    super.onSaveInstanceState(outState)
    }
    
    companion object {
       val STATE_SCORE = "playerScore"
       val STATE_LEVEL = "playerLevel"
    }

Restoring the state:

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) // Always call the superclass first

// Check whether we're recreating a previously destroyed instance
if (savedInstanceState != null) {
    with(savedInstanceState) {
        // Restore value of members from saved state
        currentScore = getInt(STATE_SCORE)
        currentLevel = getInt(STATE_LEVEL)
    }
} else {
    // Probably initialize members with default values for a new instance
}
// ...
}

Am not sure why you are getting the data from the bundle in onSaveInstanceState() method.

Also since you are saving list, want to highlight this point.

Saved instance state bundles persist through both configuration changes and process death but are limited by storage and speed, because onSavedInstanceState() serializes data to disk. Serialization can consume a lot of memory if the objects being serialized are complicated. Because this process happens on the main thread during a configuration change, long-running serialization can cause dropped frames and visual stutter.

Do not use store onSavedInstanceState() to store large amounts of data, such as bitmaps, nor complex data structures that require lengthy serialization or deserialization. Instead, store only primitive types and simple, small objects such as String. As such, use onSaveInstanceState() to store a minimal amount of data necessary, such as an ID, to re-create the data necessary to restore the UI back to its previous state should the other persistence mechanisms fail. Most apps should implement onSaveInstanceState() to handle system-initiated process death

Upvotes: 1

First of all, why u don't use ViewModel instead?

Upvotes: 0

Related Questions