泄漏在我的代码中发现了泄漏
* 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)
}
}