Android Backstack 上的碎片占用太多内存
Android Fragments on Backstack taking up too much memory
问题:
我有一个 Android 应用程序,允许用户浏览到用户的个人资料 ViewProfileFragment
。在 ViewProfileFragment
中,用户可以点击一张图片,然后将他带到 StoryViewFragment
,其中会显示各种用户的照片。可以单击用户个人资料照片,将他们带到具有新用户个人资料的 ViewProfileFragment
的另一个实例。如果用户反复点击用户的个人资料,点击一张图片将他们带到画廊,然后点击另一个个人资料,碎片会迅速堆积在内存中,从而导致可怕的 OutOfMemoryError
。这是我正在描述的流程图:
用户 A 点击了 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
适配器中的数据,"nulling" 输出变量等),以便当片段不是活动片段并推入后台堆栈将释放更多内存。但是我释放资源的努力只取得了部分成功。根据 MAT,我仍然有很多内存被 GalleryFragment
和 ViewProfileFragment
占用。
2) 我还删除了对 addToBackStack() 的调用,但显然这提供了糟糕的用户体验,因为它们无法返回(当用户点击后退按钮时应用程序只是关闭)。
3) 我已经使用 MAT 找到了我占用大量 space 的所有对象,并且我在 onPause
(和 onResume
) 释放资源的方法,但它们的大小仍然相当大。
4) 我还在两个片段的 onPause
中编写了一个 for 循环,使用以下逻辑将我所有的 ImageViews
设置为空:
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) 当期望大量的Fragments 可以加载到backstack 时,"best practices" 是什么?开发人员如何正确处理这种情况? (或者我的应用程序中的逻辑是否存在固有缺陷,而我只是做错了?)
任何帮助集思广益解决此问题的人都将不胜感激。
如果不具体访问您的源代码,很难看到全貌(即使您已经向我们展示了很多信息),我相信即使不是不可能,也是不切实际的。
话虽这么说,但在使用 Fragment 时需要牢记一些事项。先声明一下。
当 Fragments 被引入时,它们听起来像是有史以来最好的主意。能够同时显示多个activity,有点。这就是卖点。
于是全世界慢慢开始使用 Fragments。这是街区的新孩子。每个人都在使用 Fragments。如果您没有使用 Fragments,可能 "you were doing it wrong".
几年后的应用程序,趋势(谢天谢地)恢复到更多 activity,更少碎片。这是由新的 API 强制执行的(在用户没有真正注意到的情况下在活动之间转换的能力,如 Transition API 等所示)。
所以,总而言之:我讨厌片段。我认为这是有史以来最糟糕的 Android 实现之一,它之所以流行,是因为活动之间缺少转换框架(如今存在)。片段的生命周期,如果有的话,就是一团随机回调,永远不会保证在您期望的时候调用它们。
(好吧,我有点夸张了,但是问问任何 Android 经验丰富的开发人员,他是否在某些时候遇到过 Fragments 的问题,答案将是肯定的)。
综上所述,片段有效。所以你的解决方案应该有效。
所以让我们开始研究 who/where 可以保留这些硬引用。
注意:我只是想在这里抛出我将如何调试它的想法,但我不太可能提供直接的解决方案。作为参考。
发生了什么事?:
您正在向 Backstack 添加片段。
backstack 存储了一个 hard 对 Fragment 的引用,而不是 weak 或 soft。 (source)
现在谁存储了 backstack? FragmentManager 和……如您所料,它也使用硬实时引用 (source)。
最后,每个 activity 都包含对 FragmentManager 的硬引用。
简而言之:在您的 activity 死亡之前,对其片段的所有引用都将存在于内存中。不管 add/remove 在片段管理器级别/后台发生的操作。
你能做什么?
我想到了几件事。
尝试使用像 Picasso 这样的简单图像 loader/cache 库,如果有的话可以确保图像不会被泄露。如果您想使用自己的实现,您可以稍后将其删除。尽管存在所有缺陷,Picasso 确实易于使用,并且已经达到处理内存 "the right way".
的状态
从图片中删除 "I may be leaking bitmaps" 问题后(没有双关语!),是时候重新审视您的 Fragment 生命周期了。当您将片段放入后台堆栈时,它不会被销毁,但是……您有机会清除资源:调用 Fragment#onDestroyView()
。这是您要确保该片段使所有资源无效的地方。
如果您的片段正在使用 setRetainInstance(true)
,请注意不要提及,因为当 Activity 为 destroyed/recreated 时,这些不会得到 destroyed/recreated(例如: rotation),如果处理不当,所有视图都可能泄露。
- 最后,但这很难诊断,也许您想重新审视您的架构。您多次启动同一个片段 (viewprofile),您可能需要考虑重新使用同一个实例并在其中加载 "new user"。可以通过按加载顺序跟踪用户列表来处理 Backstack,因此您可以拦截 onBackPressed 并移动 "down" 堆栈,但始终在用户导航时加载 new/old 数据。您的
StoryViewFragment
. 也是如此
总而言之,这些都是根据我的经验得出的建议,但除非我们能看到更多细节,否则很难帮到你。
希望它能成为一个起点。
祝你好运。
事实证明,片段与其父片段共享相同的生命周期 activity。根据 Fragment documentation:
A fragment must always be embedded in an activity and the fragment's
lifecycle is directly affected by the host activity's lifecycle. For
example, when the activity is paused, so are all fragments in it, and
when the activity is destroyed, so are all fragments. However, while
an activity is running (it is in the resumed lifecycle state), you can
manipulate each fragment independently.
因此,除非父 activity 暂停,否则不会触发您在片段的 onPause()
中清理某些资源的步骤。如果您有多个片段被父 activity 加载,那么您很可能正在使用某种机制来切换哪个片段处于活动状态。
您可以不依赖 onPause
而是覆盖片段上的 setUserVisibleHint 来解决您的问题。这为您提供了一个很好的位置来确定在片段进入和离开视图时在哪里设置资源或清理资源(例如,当您有一个从 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();
}
}
}
正如已经提到的那样,您正在将项目堆叠在 backstack 上,因此预计至少会有一点内存占用,但您可以通过在片段不在视图中时清理资源来最大程度地减少内存占用用上面的技术。
另一个建议是真正了解内存分析器工具的输出 (MAT) and memory analysis in general. Here is a good starting point。在 Android 中很容易泄漏内存,所以我认为有必要熟悉这个概念以及内存如何从你身边消失。你的问题可能是由于当片段离开视图时你没有释放资源 以及 的内存泄漏某种形式,所以如果你使用 setUserVisibleHint
来触发资源清理,但你仍然看到正在使用大量内存,那么内存泄漏可能是罪魁祸首,因此请确保将它们都排除在外。
问题:
我有一个 Android 应用程序,允许用户浏览到用户的个人资料 ViewProfileFragment
。在 ViewProfileFragment
中,用户可以点击一张图片,然后将他带到 StoryViewFragment
,其中会显示各种用户的照片。可以单击用户个人资料照片,将他们带到具有新用户个人资料的 ViewProfileFragment
的另一个实例。如果用户反复点击用户的个人资料,点击一张图片将他们带到画廊,然后点击另一个个人资料,碎片会迅速堆积在内存中,从而导致可怕的 OutOfMemoryError
。这是我正在描述的流程图:
用户 A 点击了 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
适配器中的数据,"nulling" 输出变量等),以便当片段不是活动片段并推入后台堆栈将释放更多内存。但是我释放资源的努力只取得了部分成功。根据 MAT,我仍然有很多内存被 GalleryFragment
和 ViewProfileFragment
占用。
2) 我还删除了对 addToBackStack() 的调用,但显然这提供了糟糕的用户体验,因为它们无法返回(当用户点击后退按钮时应用程序只是关闭)。
3) 我已经使用 MAT 找到了我占用大量 space 的所有对象,并且我在 onPause
(和 onResume
) 释放资源的方法,但它们的大小仍然相当大。
4) 我还在两个片段的 onPause
中编写了一个 for 循环,使用以下逻辑将我所有的 ImageViews
设置为空:
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) 当期望大量的Fragments 可以加载到backstack 时,"best practices" 是什么?开发人员如何正确处理这种情况? (或者我的应用程序中的逻辑是否存在固有缺陷,而我只是做错了?)
任何帮助集思广益解决此问题的人都将不胜感激。
如果不具体访问您的源代码,很难看到全貌(即使您已经向我们展示了很多信息),我相信即使不是不可能,也是不切实际的。
话虽这么说,但在使用 Fragment 时需要牢记一些事项。先声明一下。
当 Fragments 被引入时,它们听起来像是有史以来最好的主意。能够同时显示多个activity,有点。这就是卖点。
于是全世界慢慢开始使用 Fragments。这是街区的新孩子。每个人都在使用 Fragments。如果您没有使用 Fragments,可能 "you were doing it wrong".
几年后的应用程序,趋势(谢天谢地)恢复到更多 activity,更少碎片。这是由新的 API 强制执行的(在用户没有真正注意到的情况下在活动之间转换的能力,如 Transition API 等所示)。
所以,总而言之:我讨厌片段。我认为这是有史以来最糟糕的 Android 实现之一,它之所以流行,是因为活动之间缺少转换框架(如今存在)。片段的生命周期,如果有的话,就是一团随机回调,永远不会保证在您期望的时候调用它们。
(好吧,我有点夸张了,但是问问任何 Android 经验丰富的开发人员,他是否在某些时候遇到过 Fragments 的问题,答案将是肯定的)。
综上所述,片段有效。所以你的解决方案应该有效。
所以让我们开始研究 who/where 可以保留这些硬引用。
注意:我只是想在这里抛出我将如何调试它的想法,但我不太可能提供直接的解决方案。作为参考。
发生了什么事?: 您正在向 Backstack 添加片段。 backstack 存储了一个 hard 对 Fragment 的引用,而不是 weak 或 soft。 (source)
现在谁存储了 backstack? FragmentManager 和……如您所料,它也使用硬实时引用 (source)。
最后,每个 activity 都包含对 FragmentManager 的硬引用。
简而言之:在您的 activity 死亡之前,对其片段的所有引用都将存在于内存中。不管 add/remove 在片段管理器级别/后台发生的操作。
你能做什么? 我想到了几件事。
尝试使用像 Picasso 这样的简单图像 loader/cache 库,如果有的话可以确保图像不会被泄露。如果您想使用自己的实现,您可以稍后将其删除。尽管存在所有缺陷,Picasso 确实易于使用,并且已经达到处理内存 "the right way".
的状态
从图片中删除 "I may be leaking bitmaps" 问题后(没有双关语!),是时候重新审视您的 Fragment 生命周期了。当您将片段放入后台堆栈时,它不会被销毁,但是……您有机会清除资源:调用
Fragment#onDestroyView()
。这是您要确保该片段使所有资源无效的地方。
如果您的片段正在使用 setRetainInstance(true)
,请注意不要提及,因为当 Activity 为 destroyed/recreated 时,这些不会得到 destroyed/recreated(例如: rotation),如果处理不当,所有视图都可能泄露。
- 最后,但这很难诊断,也许您想重新审视您的架构。您多次启动同一个片段 (viewprofile),您可能需要考虑重新使用同一个实例并在其中加载 "new user"。可以通过按加载顺序跟踪用户列表来处理 Backstack,因此您可以拦截 onBackPressed 并移动 "down" 堆栈,但始终在用户导航时加载 new/old 数据。您的
StoryViewFragment
. 也是如此
总而言之,这些都是根据我的经验得出的建议,但除非我们能看到更多细节,否则很难帮到你。
希望它能成为一个起点。
祝你好运。
事实证明,片段与其父片段共享相同的生命周期 activity。根据 Fragment documentation:
A fragment must always be embedded in an activity and the fragment's lifecycle is directly affected by the host activity's lifecycle. For example, when the activity is paused, so are all fragments in it, and when the activity is destroyed, so are all fragments. However, while an activity is running (it is in the resumed lifecycle state), you can manipulate each fragment independently.
因此,除非父 activity 暂停,否则不会触发您在片段的 onPause()
中清理某些资源的步骤。如果您有多个片段被父 activity 加载,那么您很可能正在使用某种机制来切换哪个片段处于活动状态。
您可以不依赖 onPause
而是覆盖片段上的 setUserVisibleHint 来解决您的问题。这为您提供了一个很好的位置来确定在片段进入和离开视图时在哪里设置资源或清理资源(例如,当您有一个从 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();
}
}
}
正如已经提到的那样,您正在将项目堆叠在 backstack 上,因此预计至少会有一点内存占用,但您可以通过在片段不在视图中时清理资源来最大程度地减少内存占用用上面的技术。
另一个建议是真正了解内存分析器工具的输出 (MAT) and memory analysis in general. Here is a good starting point。在 Android 中很容易泄漏内存,所以我认为有必要熟悉这个概念以及内存如何从你身边消失。你的问题可能是由于当片段离开视图时你没有释放资源 以及 的内存泄漏某种形式,所以如果你使用 setUserVisibleHint
来触发资源清理,但你仍然看到正在使用大量内存,那么内存泄漏可能是罪魁祸首,因此请确保将它们都排除在外。