保留片段泄漏
Retained fragment leak
我将简单的 activity 与保留片段一起使用,该片段包含 activity 使用的一些数据。保留的片段使用加载器从内容提供者获取数据。在配置更改(屏幕旋转)时,activity 被重新创建并且旧实例被泄漏,如 LeakCanary 库所报告的那样(保留片段 -> 加载器管理器 -> 旧 activity)。这与 support-v4 23.0.0 库(以及以前的版本)一起复制。 activity 的示例,其中保留了泄漏的片段(此处没有有用的代码,仅用于演示泄漏):
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) {
}
}
}
来自 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[=11=]
08-29 19:52:34.129 15271-16632/com.leaksample D/LeakCanary﹕ * leaks com.leaksample.MainActivity instance
也许我做错了什么,忘了调用一些 close
或 release
方法?我认为将保留的片段与加载程序一起使用是一种常见的模式,在这里不应该是内存泄漏。
您在 ModelFragment 中对 getLoaderManager() 的调用最终会创建一个新的 LoaderManagerImpl 实例,其中包含对 FragmentActivity 的引用。见下文:
// FragmentActivity.java
LoaderManagerImpl getLoaderManager(String who, boolean started, boolean create) {
if (mAllLoaderManagers == null) {
mAllLoaderManagers = new SimpleArrayMap<String, LoaderManagerImpl>();
}
LoaderManagerImpl lm = mAllLoaderManagers.get(who);
if (lm == null) {
if (create) {
lm = new LoaderManagerImpl(who, this, started);
mAllLoaderManagers.put(who, lm);
}
} else {
lm.updateActivity(this);
}
return lm;
}
从 FragmentActivity 的进一步检查来看,当 Fragment 保留其实例时,loader 并没有被移除,并且仍然持有对被销毁的 activity.
的引用
@Override
public final Object onRetainNonConfigurationInstance() {
...
for (int i=0; i<N; i++) {
LoaderManagerImpl lm = loaders[i];
if (lm.mRetaining) {
retainLoaders = true;
} else {
lm.doDestroy();
mAllLoaderManagers.remove(lm.mWho);
}
}
...
}
解决此问题的一种方法是更改 ModelFragment,使 retainInstance 为 false。
除了使用下一个解决方法外,我还没有找到最佳解决方案。我已将 getLoaderManager().initLoader(0, null, this);
调用替换为 getActivity().getSupportLoaderManager().initLoader(0, null, this);
以使用 activity 的加载器管理器而不是片段的加载器管理器。我不知道这会产生哪些副作用,但似乎有效。如果您在 activity 和片段中都使用加载程序,则应确保没有潜在的加载程序 ID 冲突。此外,我将 initLoader
方法调用从 onCreate
移动到 onActivityCreated
(当从 onCreate
调用时,加载程序在配置更改后停止接收内容更新)。
我将简单的 activity 与保留片段一起使用,该片段包含 activity 使用的一些数据。保留的片段使用加载器从内容提供者获取数据。在配置更改(屏幕旋转)时,activity 被重新创建并且旧实例被泄漏,如 LeakCanary 库所报告的那样(保留片段 -> 加载器管理器 -> 旧 activity)。这与 support-v4 23.0.0 库(以及以前的版本)一起复制。 activity 的示例,其中保留了泄漏的片段(此处没有有用的代码,仅用于演示泄漏):
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) {
}
}
}
来自 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[=11=]
08-29 19:52:34.129 15271-16632/com.leaksample D/LeakCanary﹕ * leaks com.leaksample.MainActivity instance
也许我做错了什么,忘了调用一些 close
或 release
方法?我认为将保留的片段与加载程序一起使用是一种常见的模式,在这里不应该是内存泄漏。
您在 ModelFragment 中对 getLoaderManager() 的调用最终会创建一个新的 LoaderManagerImpl 实例,其中包含对 FragmentActivity 的引用。见下文:
// FragmentActivity.java
LoaderManagerImpl getLoaderManager(String who, boolean started, boolean create) {
if (mAllLoaderManagers == null) {
mAllLoaderManagers = new SimpleArrayMap<String, LoaderManagerImpl>();
}
LoaderManagerImpl lm = mAllLoaderManagers.get(who);
if (lm == null) {
if (create) {
lm = new LoaderManagerImpl(who, this, started);
mAllLoaderManagers.put(who, lm);
}
} else {
lm.updateActivity(this);
}
return lm;
}
从 FragmentActivity 的进一步检查来看,当 Fragment 保留其实例时,loader 并没有被移除,并且仍然持有对被销毁的 activity.
的引用@Override
public final Object onRetainNonConfigurationInstance() {
...
for (int i=0; i<N; i++) {
LoaderManagerImpl lm = loaders[i];
if (lm.mRetaining) {
retainLoaders = true;
} else {
lm.doDestroy();
mAllLoaderManagers.remove(lm.mWho);
}
}
...
}
解决此问题的一种方法是更改 ModelFragment,使 retainInstance 为 false。
除了使用下一个解决方法外,我还没有找到最佳解决方案。我已将 getLoaderManager().initLoader(0, null, this);
调用替换为 getActivity().getSupportLoaderManager().initLoader(0, null, this);
以使用 activity 的加载器管理器而不是片段的加载器管理器。我不知道这会产生哪些副作用,但似乎有效。如果您在 activity 和片段中都使用加载程序,则应确保没有潜在的加载程序 ID 冲突。此外,我将 initLoader
方法调用从 onCreate
移动到 onActivityCreated
(当从 onCreate
调用时,加载程序在配置更改后停止接收内容更新)。