FragmentManager(v4)不从mCreatedMenus中删除片段

ar-*_*r-g 6 android android-fragments android-support-library leakcanary

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
Run Code Online (Sandbox Code Playgroud)

但当我看着 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;
    }
Run Code Online (Sandbox Code Playgroud)

Hai*_*imS 5

时至今日,这个问题在androidx.fragment v1.10(截至2019年11月)上仍然存在,因此这里有一些见解。

假设使用片段f的真值调用setHasOptionsMenu()。分离f后,与f关联的片段管理器(FM)将无法处理菜单上隐含的更改。请记住,菜单可能会受到同一FM托管的多个片段的影响。其中之一f脱离的事实应该导致FM重建菜单,但是同样,这没有得到处理。此外,当分离f时,在支持菜单的上下文中与f关联的资源也不会被清除。特别是,不会在f上调用onDestroyOptionsMenu(),并且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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

甚至更好的方法是尝试弄清楚如何使用-keepclassmembers保留mCreatedMenus。


Red*_*d M 0

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

如果您更新到 gradle 应用程序文件 ( ) 中的 Target-Support-26.0.0-beta1 支持库,该问题即可解决build.gradle

如果由于某些原因您无法更新到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(); 
            } 
        } 
    } 
}
Run Code Online (Sandbox Code Playgroud)