Search code examples
androidandroid-fragmentsmemory-leaksloaderleakcanary

Retained fragment leak


I'm using the simple activity with the retained fragment which holds some data used by the activity. The retained fragment uses the loader to get the data from a content provider. On configuration change (screen rotation) the activity is recreated and the old instance is leaked as reported by LeakCanary library (retained fragment -> loader manager -> old activity). This reproduced with the support-v4 23.0.0 library (and the previous versions also). The sample of the activity with the retained fragment where leak is reproduced (no useful code here, only to demonstrate the leak):

package com.leaksample;

import android.database.Cursor;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    private ModelFragment mModelFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        FragmentManager fm = getSupportFragmentManager();
        mModelFragment = (ModelFragment) fm.findFragmentByTag(ModelFragment.TAG);
        if (mModelFragment == null) {
            mModelFragment = new ModelFragment();
            fm.beginTransaction()
                    .add(mModelFragment, ModelFragment.TAG)
                    .commit();
            fm.executePendingTransactions();
        }
    }

    public static class ModelFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
        private static final String TAG = ModelFragment.class.getSimpleName();

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setRetainInstance(true);
            getLoaderManager().initLoader(0, null, this);
        }

        @Override
        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            return new CursorLoader(getActivity(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    new String[]{MediaStore.Images.Media.DATA}, null, null, null);
        }

        @Override
        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        }

        @Override
        public void onLoaderReset(Loader<Cursor> loader) {
        }
    }
}

The stack from LeakCanary:

08-29 19:52:34.129  15271-16632/com.leaksample D/LeakCanary﹕ In com.leaksample:1.0:1.
08-29 19:52:34.129  15271-16632/com.leaksample D/LeakCanary﹕ * com.leaksample.MainActivity has leaked:
08-29 19:52:34.129  15271-16632/com.leaksample D/LeakCanary﹕ * GC ROOT thread java.lang.Thread.<Java Local> (named 'Binder_1')
08-29 19:52:34.129  15271-16632/com.leaksample D/LeakCanary﹕ * references android.view.ViewRootImpl.mContext
08-29 19:52:34.129  15271-16632/com.leaksample D/LeakCanary﹕ * references com.leaksample.MainActivity.mModelFragment
08-29 19:52:34.129  15271-16632/com.leaksample D/LeakCanary﹕ * references com.leaksample.MainActivity$ModelFragment.mLoaderManager
08-29 19:52:34.129  15271-16632/com.leaksample D/LeakCanary﹕ * references android.support.v4.app.LoaderManagerImpl.mHost
08-29 19:52:34.129  15271-16632/com.leaksample D/LeakCanary﹕ * references android.support.v4.app.FragmentActivity$HostCallbacks.this$0
08-29 19:52:34.129  15271-16632/com.leaksample D/LeakCanary﹕ * leaks com.leaksample.MainActivity instance

Maybe I'm doing something wrong and forgot to call some close or release method? I think using the retained fragment with the loader is a common pattern and should not be memory leaks here.


Solution

  • I've not found the best solution other than using the next workaround. I've replaced getLoaderManager().initLoader(0, null, this); call with getActivity().getSupportLoaderManager().initLoader(0, null, this); to use activity's loader manager instead of the fragment's one. I don't know which side effects this can have but seems it works. You should make sure you don't have potential loader id conflicts if you use loaders both in the activity and fragment. Also I moved initLoader method call from onCreate to onActivityCreated (when called from onCreate the loader stopped to receive content updates after a configuration change).