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.
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).