Backstack上的Android Fragments占用太多内存
Posted
技术标签:
【中文标题】Backstack上的Android Fragments占用太多内存【英文标题】:Android Fragments on Backstack taking up too much memory 【发布时间】:2015-04-13 13:12:48 【问题描述】:问题:
我有一个 android 应用程序,它允许用户浏览到用户的个人资料 ViewProfileFragment
。在ViewProfileFragment
内,用户可以单击一张图片,将他带到StoryViewFragment
,其中会显示各种用户的照片。可以单击用户个人资料照片,将他们带到具有新用户个人资料的ViewProfileFragment
的另一个实例。如果用户反复单击用户的个人资料,单击将他们带到画廊的图像,然后单击另一个个人资料,这些片段会迅速堆积在内存中,从而导致可怕的OutOfMemoryError
。这是我所描述的流程图:
UserA 点击 Bob 的个人资料。在 Bob 的个人资料中,UserA 单击 ImageA,将他带到各种用户(包括 Bob 的)的照片库。 UserA 点击 Sue 的个人资料,然后点击她的一张图片 - 过程重复,等等。
UserA -> ViewProfileFragment
StoryViewFragment -> ViewProfileFragment
StoryViewFragment -> ViewProfileFragment
因此,您可以从典型流程中看到,大量 ViewProfileFragment
和 StoryViewFragment
实例堆积在后台堆栈中。
相关代码
我使用以下逻辑将它们作为片段加载:
//from MainActivity
fm = getSupportFragmentManager();
ft = fm.beginTransaction();
ft.replace(R.id.activity_main_content_fragment, fragment, title);
ft.addToBackStack(title);
我的尝试
1) 我专门使用FragmentTransaction
replace
以便在replace
发生时触发onPause
方法。在onPause
内部,我试图释放尽可能多的资源(例如清除ListView
适配器中的数据,“清空”变量等),以便当片段不是活动片段并推送到backstack 将释放更多内存。但我释放资源的努力只是部分成功。根据 MAT,我仍然有很多内存被 GalleryFragment
和 ViewProfileFragment
消耗。
2) 我还删除了对 addToBackStack() 的调用,但显然这提供了糟糕的用户体验,因为它们无法返回(当用户点击返回按钮时,应用程序只会关闭)。
3) 我已经使用 MAT 来查找占用大量空间的所有对象,并且我在 onPause
(和 onResume
)方法中以各种方式处理了这些对象以释放资源,但它们规模还是很大的。
4) 我还在两个片段的 onPause
中编写了一个 for 循环,使用以下逻辑将我的所有 ImageViews
设置为 null:
for (int i=shell.getHeaderViewCount(); i<shell.getCount(); i++)
View h = shell.getChildAt(i);
ImageView v = (ImageView) h.findViewById(R.id.galleryImage);
if (v != null)
v.setImageBitmap(null);
myListViewAdapter.clear()
问题
1) 我是否忽略了一种允许 Fragment 保留在后台堆栈上但也释放其资源的方法,以便 .replace(fragment) 的循环不会占用我所有的内存?
2) 当预期可以将大量 Fragment 加载到 backstack 时,“最佳实践”是什么?开发人员如何正确处理这种情况? (或者我的应用程序中的逻辑是否存在固有缺陷,而我只是做错了?)
我们将不胜感激任何帮助集思广益的解决方案。
【问题讨论】:
你在使用fragment的onPause方法吗?developer.android.com/reference/android/app/…我认为你应该在fragment的onPause方法上释放所有与fragment相关的位图资源。为了提高速度,特别是如果您的图像是从 Internet 加载的,请实施图像缓存。您可以使用Universal Image Loader 或实现您自己的。 是的,<currentFragment>.onPause()
在发出.replace(new_fragment)
时被命中。我不认为图像是我的问题,因为我在 onPause()
方法中调用 listAdapter.clear()
删除任何图像数据。这些是使用 Pico 加载的。
AFAIK,listAdapter.clear()
不会释放与整个片段相关的位图资源......至少所有这些资源。它只会删除列表中的所有元素。
我编辑了我的问题以显示我在onPause
方法中的内容,以帮助清除位图(通过调用setImageBitmap(null)
并在listview 适配器上调用.clear()
。还有其他/更好的方法吗释放位图?
看看this。我认为您已经在遵循这些步骤,但以防万一。
【参考方案1】:
如果不具体访问您的源代码,就很难看到全貌(即使您向我们展示了很多信息),我敢肯定,即使不是不可能,这也是不切实际的。
话虽如此,使用 Fragments 时需要记住一些事项。首先声明一个免责声明。
在引入 Fragments 时,它们听起来像是有史以来最好的主意。能够同时显示多个活动,有点。那是卖点。
于是整个世界慢慢开始使用 Fragments。那是街区里的新孩子。每个人都在使用 Fragments。如果你没有使用 Fragments,很可能“你做错了”。
几年后的应用程序,趋势是(谢天谢地)恢复到更多的活动,更少的碎片。这是由新的 API 强制执行的(在用户没有真正注意到的情况下在活动之间转换的能力,如转换 API 等所示)。
所以,总而言之:我讨厌片段。我相信它是有史以来最糟糕的 Android 实现之一,它之所以受欢迎,是因为活动之间缺乏过渡框架(就像今天一样)。 Fragment 的生命周期,如果有的话,就是一个随机回调的球,永远不能保证在你期望它们时被调用。
(好吧,我有点夸大其词了,但是询问任何一位经验丰富的 Android 开发人员是否在某些时候遇到过 Fragments 问题,答案将是肯定的)。
话虽如此,Fragments 仍然有效。所以你的解决方案应该有效。
让我们开始看看谁/在哪里可以保留这些硬引用。
注意:我只是在这里抛出一些关于如何调试它的想法,但我不太可能提供直接的解决方案。将其作为参考。
发生了什么?: 您正在向 Backstack 添加片段。 backstack 存储了对 Fragment 的 hard 引用,而不是弱引用或软引用。 (source)
现在谁存储一个backstack? FragmentManager 和……正如您所猜测的,它也使用硬实时引用 (source)。
最后,每个活动都包含对 FragmentManager 的硬引用。
简而言之:在您的活动终止之前,对其片段的所有引用都将存在于内存中。无论在 Fragment Manager 级别/backstack 发生的添加/删除操作。
你能做什么? 我想到了几件事。
尝试使用像Picasso 这样的简单图像加载器/缓存库,以确保图像不会被泄露。如果您想使用自己的实现,您可以稍后将其删除。尽管存在所有缺陷,毕加索使用起来确实很简单,并且已经达到了一种“以正确方式”处理记忆的状态。
从图片中删除“我可能正在泄漏位图”问题后(不是双关语!),是时候重新审视 Fragment 生命周期了。当你将一个片段放入 backstack 时,它并没有被销毁,但是……你有机会清除资源:Fragment#onDestroyView()
被调用。这里是您要确保片段使任何资源无效的地方。
您没有提及您的片段是否使用setRetainInstance(true)
,请注意这一点,因为当 Activity 被销毁/重新创建(例如:旋转)时,这些片段不会被销毁/重新创建,如果没有,所有视图都可能会泄漏妥善处理。
-
最后,但这更难诊断,也许您想重新审视您的架构。您多次启动相同的片段(视图配置文件),您可能需要考虑重用相同的实例并在其中加载“新用户”。可以通过按照加载顺序跟踪用户列表来处理 Backstack,因此您可以拦截 onBackPressed 并“向下”移动堆栈,但始终在用户导航时加载新/旧数据。您的
StoryViewFragment
也是如此。
总而言之,这些都是根据我的经验提出的建议,但除非我们能更详细地了解,否则很难帮助您。
希望它被证明是一个起点。
祝你好运。
【讨论】:
我的问题似乎与我很难追踪的内存泄漏有关。在理想情况下,当onPause
被命中(如果编码正确)时,每个片段不应该占用太多内存,因此遇到任何内存问题需要很长时间。在我的整个应用程序中只有一个地方存在此问题(没有其他片段显示此行为)。我正在使用 MAT 来缩小泄漏的来源。我会尽快更新我的问题的详细信息。
@MartínMarconcini 很好,这是一个非常有用和彻底的答案。 +1 尚未充分关注过渡框架
反响非常好。感谢您抽出宝贵的时间。它确实帮助我解决了片段记忆问题。【参考方案2】:
事实证明,片段与其父 Activity 共享相同的生命周期。根据Fragment documentation:
片段必须始终嵌入到活动中,并且片段的 生命周期直接受宿主活动生命周期的影响。为了 例如,当活动暂停时,其中的所有片段也是如此,并且 当活动被破坏时,所有片段也被破坏。然而,虽然 一个活动正在运行(它处于恢复的生命周期状态),你可以 独立操作每个片段。
因此,除非父活动暂停,否则您在片段的onPause()
中清理一些资源的步骤不会触发。如果您有多个由父活动加载的片段,那么很可能您正在使用某种机制来切换哪个是活动的。
您也许可以通过不依赖onPause
而是通过覆盖片段上的setUserVisibleHint 来解决您的问题。这为您提供了一个好地方,可以确定在哪里设置资源或在 Fragment 进出视图时清理资源(例如,当您有一个从 FragmentA 切换到 FragmentB 的 PagerAdapter 时)。
public class MyFragment extends Fragment
@Override
public void setUserVisibleHint(boolean isVisibleToUser)
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser)
//you are visible to user now - so set whatever you need
initResources();
else
//you are no longer visible to the user so cleanup whatever you need
cleanupResources();
正如已经提到的,您将项目堆叠在后堆栈上,因此预计至少会有一点内存占用,但您可以通过在片段不在视野范围内时清理资源来最小化占用以上技术。
另一个建议是非常善于理解内存分析器工具 (MAT) 的输出和一般的内存分析。 Here is a good starting point。在 Android 中内存泄漏真的很容易,所以我认为有必要熟悉这个概念以及内存如何远离你。您的问题可能是由于您在片段超出视图时没有释放资源以及某种内存泄漏,所以如果您使用setUserVisibleHint
触发清理您的资源,但您仍然看到大量内存正在使用,那么内存泄漏可能是罪魁祸首,因此请务必将它们都排除在外。
【讨论】:
我忽略了onPause
仅在调用父级的 onPause
时才被击中的事实最初让我感到困惑。所以现在我的清理方法会像你建议的那样在片段消失时被命中。谢谢!
setUserVisibleHint 未被系统调用,因此依赖它没有任何意义
Fragments 的 onPause() 方法确实在 FragmentManager.replace 事务期间被调用。以上是关于Backstack上的Android Fragments占用太多内存的主要内容,如果未能解决你的问题,请参考以下文章
如何将 android 导航 backstack 弹出到某个片段?
Android Reorder Fragment Backstack
Android TabLayout ViewPager 不会在 backstack 上膨胀标签片段