Android中的回退栈Fragment占用过多内存

23

问题:

我有一个Android应用程序,允许用户浏览到用户的个人资料ViewProfileFragment。在ViewProfileFragment中,用户可以单击图像,跳转到StoryViewFragment,其中会显示各种用户的照片。用户可能会单击用户个人资料照片,然后跳转到新用户个人资料的另一个实例ViewProfileFragment。如果用户反复点击用户的个人资料,点击跳转到相册的图像,然后点击另一个个人资料,Fragment会快速堆积在内存中,导致可怕的OutOfMemoryError。下面是我描述的流程图:

UserA 点击 Bob 的个人资料。在 Bob 的个人资料中,UserA 点击 ImageA 跳转到各种用户照片的图库(包括 Bob 的照片)。UserA 点击 Sue 的个人资料,然后点击她的一张照片,这个过程会不断重复。

UserA -> ViewProfileFragment
         StoryViewFragment -> ViewProfileFragment
                               StoryViewFragment -> ViewProfileFragment

从典型的流程中可以看到,ViewProfileFragmentStoryViewFragment的实例会不断堆积在后退栈中。

相关代码

我使用以下逻辑将它们加载为片段:

//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的显示,仍然有很多内存被GalleryFragmentViewProfileFragment占用。

2) 我还删除了对addToBackStack()的调用,但显然这会给用户带来糟糕的体验,因为他们无法返回(当用户按下返回按钮时,应用程序将关闭)。

3) 我使用MAT找到了所有占用大量空间的对象,并在onPause(和onResume)方法中以各种方式处理它们以释放资源,但它们仍然占用相当大的空间。

enter image description here

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)我是否忽略了一种方法,可以使一个片段保留在后台堆栈中,但同时释放它的资源,以便.replace(fragment)的周期不会耗尽我的所有内存?

2)当预计有很多片段加载到后台堆栈时,“最佳实践”是什么?开发人员如何正确处理这种情况?(或者我的应用逻辑本质上是有缺陷的,我只是做错了吗?)

非常感谢您提供任何有关解决此问题的想法。


我编辑了我的问题,展示了我在onPause方法中所做的事情,以帮助清除位图(通过调用setImageBitmap(null)并在listview适配器上调用.clear())。是否有另一种更好的方法来释放位图? - Max Worg
@MaxWorg 这并不会释放位图使用的资源。要释放它们,您需要手动调用 ((BitmapDrawable)ImageView.getDrawable()).getBitmap().recycle()。但是这样做后,当恢复片段时,您将不得不重新加载位图,否则会崩溃。 - Drew
@ZeLuis 我相信这更多是错误的导航流程设计问题 - 它既是主从又是循环的。即使话题发起者释放了所有可能的资源,框架仍会保留太多内存来跟踪返回堆栈状态,最终会导致相同的崩溃,尽管需要更多用户的努力。 - Drew
@Ze Luis,在RecyclerViews/ListViews(具有公平的位图数量)中,调用recycle()仍然是我发现可以解决OutOfMemoryError的唯一方法,即使在Lollipop API上也是如此。没有其他的方法有效,我已经尝试过各种各样的东西:图像加载器首选项、图像加载器库等等。就是这样。 - Drew
你最后想出了什么解决方案?你能分享一下如何清理各种资源的例子吗? - jlively
显示剩余8条评论
2个回答

23
尽管您向我们展示了很多信息,但没有具体的访问源代码,很难看到整个图片,我确信如果不是不可能就是不切实际。
话虽如此,在使用Fragment时有一些需要记住的事情。首先是免责声明。
当Fragment被引入时,它们听起来像是有史以来最好的想法。能够同时显示一个以上的活动,有点像。这是卖点。
于是整个世界慢慢开始使用Fragment。它是新来的孩子。每个人都在使用Fragment。如果你不使用Fragment,那么“你做错了”的机会很大。
几年后,随着新API的推出(可以在用户几乎没有注意到的情况下在活动之间进行转换,如Transition API等),趋势(幸运的是)正在逐渐回归更多的活动,更少的片段。
总之,我讨厌Fragment。我认为它是Android历史上最糟糕的实现之一,仅因为在活动之间缺乏Transition Framework(如今存在)而获得流行。如果说Fragment的生命周期是什么的话,那么它就是一堆随机回调函数,不能保证在您期望的时候被调用。
(好吧,我有点夸张,但问问任何经验丰富的Android开发人员是否曾经遇到过Fragment问题,答案肯定是肯定的。)
尽管如此,Fragment确实起作用。所以你的解决方案应该能够工作。
那么让我们开始看看谁/哪里可能会保存这些硬引用。
注意:我只是在这里提出一些我如何调试的想法,但不太可能提供直接的解决方案。请将其用作参考。

发生了什么?: 您正在将片段添加到后退栈中。 后退栈存储一个硬引用到Fragment,而不是弱引用或软引用。(source

现在,谁存储了后退栈呢?FragmentManager……正如您所猜测的那样,它也使用硬实时引用source)。

最后,每个活动都包含对FragmentManager的硬引用

简而言之:在您的活动死亡之前,所有对其片段的引用都将存在于内存中。无论在FragmentManager级别/后退栈中发生了哪些添加/删除操作。

你可以做什么? 我想到了几件事情。

  1. 尝试使用像Picasso这样的简单图像加载器/缓存库,以确保图像没有泄漏。如果需要,您可以稍后将其删除并使用自己的实现。尽管有其缺点,Picasso非常简单易用,并且已经达到了处理内存的“正确方式”的状态。

  2. 在解决了“我可能正在泄漏位图”问题之后(开个玩笑),现在是时候重新审视您的Fragment生命周期了。当您将一个片段放入后退栈时,它不会被销毁,但是…您有机会清除资源:Fragment#onDestroyView()将被调用。这里是您想要确保片段将任何资源设置为空值的地方。

您没有提及您的片段是否使用了setRetainInstance(true),请小心使用,因为这些不会在Activity被销毁/重新创建(例如:旋转)时被销毁/重建,如果没有正确处理,所有视图可能会泄漏。

  1. 最后,但这更难诊断,也许您想重新审视您的架构。您正在多次启动相同的片段(viewprofile),您可能要考虑改为重用相同的实例并在其中加载“新用户”。可以通过跟踪按加载顺序排列的用户列表来处理回退堆栈,因此您可以截取onBackPressed并向下移动堆栈,但始终在用户导航时加载新/旧数据。对于您的StoryViewFragment也是如此。

总之,这些都是我从经验中得出的建议,但除非我们能看到更详细的内容,否则真的很难帮助您。

希望它能成为一个起点。

祝好运。


我的问题似乎更与我难以追踪的内存泄漏有关。在理想的情况下,每个片段在onPause被触发时不应该占用太多内存(如果编码正确),因此在遇到任何内存问题之前需要很长时间。这只是我整个应用程序中唯一出现这种情况的地方(没有其他片段显示这种行为)。我正在使用MAT来缩小泄漏可能来自哪里的范围。我将很快用详细信息更新我的问题。 - Max Worg
1
@MartínMarconcini 很好,那是非常有用和详尽的回答。+1 我还没有足够关注过Transition Framework. - Drew
非常好的回复。感谢您抽出时间。这真的帮助我解决了我的碎片内存问题。 - Marty Miller

8
原来,片段与其父活动共享相同的生命周期。根据Fragment documentation
“片段必须始终嵌入在活动中,并且片段的生命周期直接受到宿主活动生命周期的影响。例如,当活动暂停时,其中所有片段也会暂停,当活动销毁时,其中所有片段也会被销毁。但是,在活动运行时(处于恢复的生命周期状态),您可以独立地操作每个片段。”
因此,在片段的onPause()中清理某些资源所采取的步骤,除非父活动暂停,否则不会触发。如果您有多个由父活动加载的片段,则最有可能使用某种机制来切换哪个片段处于活动状态。
你可以通过不依赖于onPause,而是覆盖片段中的setUserVisibleHint来解决问题。这为您提供了一个很好的位置,以确定何时进行资源设置或清理(例如,当您有一个PagerAdapter从FragmentA切换到FragmentB时,该位置可以在视图中进入和退出片段)。
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)和内存分析的基础知识。这里是一个很好的起点。在Android中泄漏内存非常容易,所以我认为熟悉这个概念以及内存如何消耗的必要性。您的问题可能是由于在片段不可见时未释放资源以及某种类型的内存泄漏导致的,因此如果您选择使用setUserVisibleHint来触发清理资源,并且仍然看到大量内存被使用,则可能存在内存泄漏问题,请确保排除两者。

我忽视了onPause只有在父级的onPause被调用时才会执行,这是最初让我犯错的地方。所以现在我的清理方法会在片段消失时被调用,就像你建议的那样。谢谢! - Max Worg
setUserVisibleHint方法不是由系统调用的,因此依赖它没有任何意义。 - RunLoop
在 FragmentManager.replace 事务期间,片段的 onPause() 方法确实会被调用。 - duggulous

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接