Search code examples
androidmemory-leaksandroid-viewpagerparcelable

Memory leak with writeToParcel() called multiple times after onSaveInstanceState()


In some activity, I have to save MVC model as a parcelable. The parcelable is built according to the doc, which I've read more than enough (but who knows, I obviously could have missed something). There is a leak in this activity, but I'm struggling to understand its cause. The SO question Communication objects between multiple fragments in ViewPager was interesting but my code was already following guidelines from the answer.

The activity own a viewpager with around 60 fragments inside (but up to 350). The model is passed from the activity to all fragments, and user actions in the fragments are saved into the model.

Whenever I pause my activity, onSaveInstanceState is triggered once, and immediately after multiple triggers of my parcelable's writeToParcel method. The number of triggers depends on the number of Fragments ever loaded in the viewpager + 1. So at activity startup, if I turn the emulator off and back on, writeToParcel is called 3 times (only 1st and 2nd fragment are loaded), if I swipe once right and do it again, it is called 4 times (the 2nd is showing and the 3rd is loaded), if I setExtPosition() on the adapter and go to 10th fragment, writeToParcel is called 7 times (9th, 10th and 11h are loaded).

Of course if my user swipe every fragment, it will eventually get an ugly TransactionTooLargeException, which brings me here.

Here is some code. There may be a ton of code/concept improvements here, and any tips is very welcome, but my main problem is this dirty little leak I've found.

In my activity:

@Override
public void onSaveInstanceState (Bundle outState) {
    outState.putParcelable("model", myParcelable);
    super.onSaveInstanceState(outState);
}

In my fragment:

public static MyFragment newInstance(Model model) {
    MyFragment fragment = new MyFragment();
    Bundle args = new Bundle();
    args.putParcelable(KEY_MODEL, model);
    fragment.setArguments(args);
    return fragment;
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Bundle args = getArguments();
    mModel = args.getParcelable(KEY_MODEL);
}

In my parcelable model:

@Override
public void writeToParcel(Parcel dest, int flags) {
    int startSize = dest.dataSize();
    dest.writeString(foo); //one of these string is supposed to be null
    dest.writeString(bar);
    dest.writeString(foobar);
    dest.writeByte((byte) (isMyObjectTrue ? 1 : 0));
    dest.writeList(someStringList);
    dest.writeList(someIntegerList);
    dest.writeBundle(someBundle); //I use a Bundle to save a Map<String,String>
    int endSize = dest.dataSize();
}

I ran the debugger inside the writeToParcel() method, and I was surprised to see that startSize is never 0. Is it normal ?

I searched throughout my code, and putParcelable() or any writing method with parcelable in its name is only called in this activity and in the fragment newInstance().

How can I find the cause of this weird exponential behaviour ?

PS: of course feel free to ask for more code.

EDIT

I've implemented the solution advised by @Ben P., and the problem have improved a lot, but is not totally solved. My activity implements an interface which now has a getModel() method called in onAttach(), and a setUserInput(char userInput) I use to update the model from the fragment. The fragment's newInstance() method don't save the model anymore.

MyFragment

@Override
public void onAttach(Context context) {
    super.onAttach(context);
    try {
        callBack = (MyInterface) context; //callBack is a class field
        mModel = callBack.getModel();     //mModel too
    } catch (ClassCastException e) {
        throw new ClassCastException(context.toString() + " must implement MyInterface");
    }
}

This turned the exponential problem into a linear problem which is obviously better but still a problem.

Now, writeToParcel() is only called once, but the total parcel size is still growing with the number of item loaded. My model takes around 3kb inside the parcel (+/-10% depending on the number of inputs), as measured by endSize-startSize.

How can I know where the growth comes from ?


Solution

  • Before I get into your problem specifically, I want to point out that the Bundle passed to setArguments() is part of the fragment's instance state. Every time a fragment is destroyed and recreated, these arguments need to be persisted. So anything you put into that Bundle has the potential to be parceled and unparceled during configuration changes.


    The activity own a viewpager with around 60 fragments inside (but up to 350). The model is passed from the activity to all fragments, and user actions in the fragments are saved into the model.

    This sounds like you have one single Model object that all fragments share. If this is the case, I recommend not passing the model object to each fragment as part of its arguments Bundle. Doing this will cause tremendous duplication when instance state is saved and restored. Instead, I'd expose a method in your Activity (something like getModel()) and then call that from your fragments to retrieve the model instance.

    On the other hand, it also sounds like maybe you're only starting with the same Model object, and that each fragment can mutate it in some way. This would mean that you do have to save something to instance state for each fragment... but it's possible you can optimize here. Rather than saving and restoring the entire Model object, perhaps you could just store the diffs. That is, if fragment #1 changes the model's name, and fragment #2 changes the model's value, then you could have fragment #1 only save the new name and have fragment #2 only save the new value. Doing this instead of saving two extra copies of the model object could potentially amount to huge savings.