FragmentManager(v4) 不会从 mCreatedMenu 中删除片段



泄漏在我的代码中发现了泄漏

* classifieds.yalla.features.ad.page.seller.SellerAdPageFragment has leaked:
* GC ROOT android.view.inputmethod.InputMethodManager$1.this$0 (anonymous subclass of com.android.internal.view.IInputMethodClient$Stub)
* references android.view.inputmethod.InputMethodManager.mNextServedView
* references android.support.v4.widget.DrawerLayout.mContext
* references classifieds.yalla.features.host.HostActivity.fragNavController
* references com.ncapdevi.fragnav.FragNavController.mFragmentManager
* references android.support.v4.app.FragmentManagerImpl.mCreatedMenus
* references java.util.ArrayList.elementData
* references array java.lang.Object[].[0]
* leaks classifieds.yalla.features.ad.page.seller.SellerAdPageFragment instance

但是当我看着FragmentManagerImpl

我没有发现FragmentManagerImpl.mCreatedMenus何时被清除。我找到的唯一代码是添加新片段时。难道不应该以某种方式管理它吗?

public boolean dispatchCreateOptionsMenu(Menu menu, MenuInflater inflater) {
boolean show = false;
ArrayList<Fragment> newMenus = null;
if (mAdded != null) {
for (int i=0; i<mAdded.size(); i++) {
Fragment f = mAdded.get(i);
if (f != null) {
if (f.performCreateOptionsMenu(menu, inflater)) {
show = true;
if (newMenus == null) {
newMenus = new ArrayList<Fragment>();
}
newMenus.add(f);
}
}
}
}
if (mCreatedMenus != null) {
for (int i=0; i<mCreatedMenus.size(); i++) {
Fragment f = mCreatedMenus.get(i);
if (newMenus == null || !newMenus.contains(f)) {
f.onDestroyOptionsMenu();
}
}
}
mCreatedMenus = newMenus;
return show;
}

这个问题现在在 androidx.fragment v1.10(所以 2019 年 11 月)上仍然相关,所以这里有一些见解。

假设 setHasOptionsMenu() 被调用,对于片段 f 来说是一个 true 值。 分离 f 时,与 f 关联的片段管理器 (FM) 将不会处理菜单上隐含的更改。 请记住,菜单可能会受到同一 FM 托管的多个片段的影响。其中一个 f 分离的事实应该导致 FM 重建菜单,但话又说回来,这没有处理。 此外,当 f 分离时,在支持菜单的上下文中与 f 关联的资源也不会被清理。 特别是,onDestroyOptionsMenu() 不会在 f 上调用,FM 在其提供菜单选项的片段列表中保留对 f 的引用。

在 Google 修复片段管理器以从该列表中删除泄露的片段之前,一些选项是:

  • 与泄漏的碎片一起生活。 当活动被销毁时,片段管理器将被清除,然后片段将被 GC 声明。
  • 不要使用 setHasOptionsMenu() 机制。例如,您可以提出自己的菜单实现。
  • 使用反射从该列表中删除片段。 当然,反射并不理想,但泄漏碎片要糟糕得多。 在其他泄漏片段中,添加如下所示的内容
@Override
public void onDetach() {
super.onDetach();
// get the fragment manager associated with this fragment
FragmentManager fragmentManager = getFragmentManager();
if (fragmentManager != null) {
try {
Field field = 
fragmentManager.getClass().getDeclaredField("mCreatedMenus");
field.setAccessible(true);
if (field.get(fragmentManager) instanceof ArrayList) {
ArrayList fragments = (ArrayList)field.get(fragmentManager);
if (fragments != null && fragments.remove(this)) {
Log.d(TAG, "Yay, no leak today");
}
}
} catch (NoSuchFieldException | SecurityException | 
IllegalAccessException e) {
e.printStackTrace();
}
}
}

注意:当然,当片段相关的代码更改时,此解决方案是脆弱的,但是,这是可测试的。此外,如果使用 proguard,您可能希望确保避免对该字段进行混淆,因此您可以添加如下所示的 proguard 指令:

-keep class androidx.fragment.app.FragmentManagerImpl { *; }

或者更好的是,尝试弄清楚如何使用 -keepclassmembers 来保存 mCreatedMenus。

这是Android SDK中的泄漏。看看这个线程。

如果您在 gradle 应用文件 (build.gradle) 中更新到 Target-Support-26.0.0-beta1 支持库,则此问题已修复。

如果由于某些原因您无法更新以支持LibVersion>=26-beta1,则有一个解决方法:

public class FragmentUtils { 
/** 
* Hack to force update the LoaderManager's host to avoid a memory leak in retained/detached fragments. 
* Call this in {@link Fragment#onAttach(Activity)} 
*/ 
public static void updateLoaderManagerHostController(Fragment fragment) { 
if (fragment.mHost != null) { 
fragment.mHost.getLoaderManager(fragment.mWho, fragment.mLoadersStarted, false); 
} 
} 
/** 
* This hack is to prevent the root loader manager to leak previous instances of activities 
* accross rotations. It should be called on activities using loaders directly (not via a fragments). 
* If the activity has fragment, you also have to also {@link #updateLoaderManagerHostController(Fragment)} above 
* for each fragment. 
* Call this in {@link FragmentActivity#onCreate} 
* 
* @param activity an actvity that uses a loader and leaks on rotation. 
*/ 
public static void updateLoaderManagerHostController(FragmentActivity activity) { 
if (activity.mFragments != null) { 
try { 
final Field mHostField = activity.mFragments.getClass().getDeclaredField("mHost"); 
mHostField.setAccessible(true); 
FragmentHostCallback mHost = (FragmentHostCallback) mHostField.get(activity.mFragments); 
mHost.getLoaderManager("(root)", false, true /* the 2 last params are not taken into account*/); 
} catch (IllegalAccessException e) { 
e.printStackTrace(); 
} catch (NoSuchFieldException e) { 
e.printStackTrace(); 
} 
} 
} 
}

我在使用androidX FragmentManager时遇到了这个问题。

通过在片段事务后调用此内容来修复泄漏。 此方法进入活动内部。

private fun clearFragmentManagersAddedMenus() {
Handler(mainLooper).post {
val field = FragmentActivity::class.java.getDeclaredField("mFragments")
field.isAccessible = true
(field.get(this) as FragmentController).dispatchCreateOptionsMenu(null, null)
}
}

最新更新