【问题标题】:FragmentManager(v4) not removing fragments from mCreatedMenusFragmentManager(v4) 不从 mCreatedMenus 中删除片段
【发布时间】:2017-06-21 11:20:57
【问题描述】:

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

* 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;
    }

【问题讨论】:

  • 你能发帖SellerAdPageFragment吗?
  • @azizbekian 它很大。我的观点是,当片段删除时 mCreatedMenus 没有被清除,它在使用菜单创建新片段时重新分配,然后 mCreatedMenus 中的旧片段将被释放
  • 那是因为您以某种方式保留了对片段的引用。
  • fragmentManager 在此字段中保留引用 ArrayList&lt;Fragment&gt; mCreatedMenus;
  • 我理解你的观点和 FragmentManagerImpl 中的代码;但是如何确保 mCreatedMenus 导致了内存泄漏?您还看到其他日志吗?

标签: android android-fragments android-support-library leakcanary


【解决方案1】:

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

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

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

  • 接受泄露的片段。当 Activity 被销毁时,Fragment Manager 将被清除,然后由 GC 认领 Fragment。
  • 不要使用 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。

【讨论】:

  • 代码收缩器应该自动保持通过反射访问的字段,不要引用我的话。
  • 我正在尝试使用 parentFragmentManager 进行此操作,但它无法说该字段不存在...有什么建议吗?
  • 我找到了这个问题的谷歌问题跟踪器条目:issuetracker.google.com/issues/131537919 他们仍然缺少一个最小的样本来解决这个问题,我现在提供了一个。如果您有关于该主题的更多信息,请随时添加到那里的对话中。
【解决方案2】:

这是 Android SDK 中的一个漏洞。看看this thread

如果您在 gradle 应用文件 (build.gradle) 中更新到 Target-Support-26.0.0-beta1 支持库,该问题已得到解决。

如果由于某些原因您无法更新到 supportLibVersion>=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(); 
            } 
        } 
    } 
}

【讨论】:

    【解决方案3】:

    我在使用 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)
            }
        }
    

    【讨论】:

      猜你喜欢
      • 2014-06-23
      • 1970-01-01
      • 2015-06-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-04-04
      • 1970-01-01
      相关资源
      最近更新 更多