泄漏金丝雀,Recyclerview 泄漏 mAdapter

Posted

技术标签:

【中文标题】泄漏金丝雀,Recyclerview 泄漏 mAdapter【英文标题】:Leak canary, Recyclerview leaking mAdapter 【发布时间】:2016-06-01 22:47:12 【问题描述】:

我决定是时候学习如何使用 Leak Canary 来检测我的应用程序中的泄漏了,并且像往常一样,我尝试在我的项目中实施它以真正了解如何使用该工具。实现它很容易,困难的部分是阅读该工具向我抛出的内容。 我有一个滚动视图,当我上下滚动时,它似乎在内存管理器中累积内存(即使它没有加载任何新数据),所以我认为这是一个很好的候选对象来跟踪泄漏,结果如下:

看起来 v7.widget.RecyclerView 正在泄漏适配器,而不是我的应用程序。但这不可能是对的……对吧?

这是适配器和使用它的类的代码: https://gist.github.com/feresr/a53c7b68145d6414c40ec70b3b842f1e

我开始为这个问题悬赏,因为它在两年后在一个完全不同的应用程序上重新出现

【问题讨论】:

看起来您正在传递应用程序上下文,而您可能应该使用 RecyclerView 的上下文或您的活动上下文。应用程序上下文是长期存在的,这会阻止收集。 GapWorker 是执行预取的静态类。当 RecyclerView 被销毁时,它会在 onDetachedFromWindow() 中正确地从 GapWorker 中注销自己。您是否在自定义 RecyclerView 中覆盖了 onDetachedFromWindow() 并忘记调用 super.onDetachedFromWindow() ? 【参考方案1】:

如果适配器的寿命比RecyclerView 长,您必须清除onDestroyView 中的适配器引用:

@Override
public void onDestroyView() 
    recyclerView.setAdapter(null);
    super.onDestroyView();

否则适配器将持有对应该已经耗尽内存的RecyclerView 的引用。

如果屏幕涉及到过渡动画,你实际上必须更进一步,只有在视图变得分离时才清除适配器:

@Override
public void onDestroyView() 
    recyclerView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() 
        @Override
        public void onViewAttachedToWindow(View v) 
            // no-op
        

        @Override
        public void onViewDetachedFromWindow(View v) 
            recyclerView.setAdapter(null);
        
    );
    super.onDestroyView();

【讨论】:

这就是我最终所做的,实际上解决了这个问题。我在其他一些 SO 问题中看到了同样的答案。我想了解为什么会这样。参考链应该是 Fragment -> Recyclerview -> Adapter。所以,如果片段停止引用 Recyclerview,recyclerview 和它的适配器都应该被垃圾回收。 当您使用RecyclerView.setAdapterRecyclerView 设置适配器时,该适配器会将RecyclerView 注册为AdapterDataObserver 并在内部列表中保存对它的引用。如果适配器的寿命比RecyclerView 长(例如,如果您在FragmentonCreate 中定义适配器就是这种情况),它将持有对应该被GC 处理的RecyclerView 的引用反而。调用 recyclerView.setAdapter(null) 会取消注册该适配器的 RecyclerView 如果我在动画屏幕应该怎么办? 我自己也遇到过这个问题。在这种情况下,您要确保在视图分离之后才清除适配器。 我已经更新了答案,包括处理屏幕动画的情况。【参考方案2】:

我可以通过覆盖 RecyclerView 来解决这个问题。这是因为 RecyclerView 永远不会从 AdapterDataObservable 注销自己。

@Override protected void onDetachedFromWindow() 
    super.onDetachedFromWindow();
    if (getAdapter() != null) 
        setAdapter(null);
    

【讨论】:

这个问题在一个全新的应用程序中再次出现,这仍然是解决它的最佳方法吗? @feresr 它从未消失 :) 自 android 21 以来我仍然使用相同的 HackyRecyclerView 哈哈很高兴知道我不是唯一一个面临这个问题的人。我目前正在做 onDestroyView() recyclerView.adapter = null 。这也解决了这个问题,但我想首先知道是什么原因造成的。 难以置信。我们还需要修补这个吗?有任何 Google 开发人员对此发表过评论吗? @Bolein95,你有 kotlin 解决方案吗?【参考方案3】:

首先,我引用的是this file。

看起来 v7.widget.RecyclerView 正在泄漏适配器,而不是我的应用程序。但这不可能是对的……对吧?

实际上是您的适配器泄漏了RecyclerView(跟踪图和 LeakCanary 活动的标题清楚地表明了这一点)。但是,我不确定它是“父” RecyclerView 还是 HourlyViewHolder 中的嵌套视图,或者两者兼而有之。 我认为罪魁祸首是你的 ViewHolders。通过使它们成为非静态内部类,您可以显式地为它们提供对封闭适配器类的引用,这几乎直接将适配器与回收的视图耦合在一起,因为您的持有者中每个 itemView 的父级都是 RecyclerView 本身。

我解决这个问题的第一个建议是通过将 ViewHolders 和 Adapter 设为 static 内部类来解耦它们。这样他们就没有对适配器的引用,因此他们将无法访问您的 context 字段,这也是一件好事,因为应该谨慎地传递和存储上下文引用(也避免大内存泄漏)。当您只需要上下文来获取字符串时,请在其他地方进行,例如在适配器构造函数中,但不要将上下文存储为成员。最后,DayForecastAdapter 似乎也很危险:你将它的同一个实例传递给每个HourlyViewHolder,这似乎是一个错误。

我认为修复设计和解耦这些类应该可以消除这种内存泄漏

【讨论】:

谢谢!我现在正在尝试这个!,我也会避免在每个 HourlyViewHolder 上传递相同的 DayForecastAdapter 实例 不走运,结果相同...如果您切换到分支“forecastadapter-leak”,您将看到我应用的更改。不幸的是,错误仍然存​​在。不过感谢您的提示,将 VH 设为静态是个好主意。 看看你的分支,我真的认为你应该创建 DayForecastAdapter 的每个持有者实例,并在第 157 行而不是 labelViewHolder.recyclerView.setAdapter(dayForecastAdapter) 做类似 labelViewHolder.recyclerView.adapter .setData(...) 也应该调用适配器上的 notifyDataSetChanged() 方法。因为你的 dayForecastAdapter 是每个 hourlyViewHolder 的,ForecastAdapter 和 dayForecastAdapter 的 viewHolder 以一种奇怪的方式耦合,也许这就是问题的根源? 感谢您抽出宝贵的时间来实际查看代码。我今天一下班就会检查一下,告诉你进展如何。 又不走运了,由于某种原因,泄漏量始终为 196B,并且泄漏发生时无需旋转屏幕(滚动通过 recyclerview 就可以了),我有点迷路了。 .. 我会尝试将所有 ViewHolders 提取到单独的类中,以使这个神级类更具可读性,看看我是否能以这种方式找到问题【参考方案4】:

我无法打开您的图像并查看实际泄漏,但如果您在 Fragment 中为您的 RecyclerView 定义一个局部变量并设置片段的 retainInstanceState true 它可能会导致旋转泄漏。

当使用FragmentretainInstance 时,您应该清除onDestroyView 中的所有用户界面引用

@Override
public void onDestroyView() 
     yourRecyclerView = null;
     super.onDestroyView();

您可以在此链接中找到详细信息: Retained Fragments with UI and memory leaks

【讨论】:

【参考方案5】:

所以,这可能是一个有点矫枉过正的解决方案,但我们的团队已经厌倦了不得不担心每个 RecyclerView 适配器泄漏。

这是一个抽象的 RecyclerView.Adapter 子类,它一劳永逸地解决泄漏问题。

abstract class AbstractRecyclerViewAdapter<VH : RecyclerView.ViewHolder>(fragment: Fragment) :
    RecyclerView.Adapter<VH>() 
    private val fragmentRef = WeakReference(fragment)

    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) 
        super.onAttachedToRecyclerView(recyclerView)
        setupLifecycleObserver(recyclerView)
    

    private fun setupLifecycleObserver(recyclerView: RecyclerView) 
        val fragment = fragmentRef.get() ?: return
        val weakThis = WeakReference(this)
        val weakRecyclerView = WeakReference(recyclerView)

        // Observe the fragment's lifecycle events in order
        // to set the Recyclerview's Adapter when the Fragment is resumed
        // and unset it when the Fragment is destroyed.
        fragment.viewLifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver 
            override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) 
                val actualRecyclerView = weakRecyclerView.get() ?: return
                when (event.targetState) 
                    Lifecycle.State.DESTROYED -> actualRecyclerView.adapter = null
                    Lifecycle.State.RESUMED -> 
                        val self = weakThis.get() ?: return
                        if (actualRecyclerView.adapter != self) 
                            actualRecyclerView.adapter = self
                        
                    
                    else -> 
                    
                
            
        )
    

基本上,Adapter 会观察包含它的 Fragment 的生命周期事件,并负责将自身设置和取消设置为 RecyclerView 的适配器。

您只需在自己的适配器中继承AbstractRecyclerViewAdapter,并在构建它们时提供容器片段。

用法:

你的适配器

class MyAdapter(fragment: Fragment): AbstractRecyclerViewAdapter<MyViewHolder>(fragment) 
    // Your usual adapter code...

你的片段

class MyFragment : Fragment 
    // Instantiate with the fragment.
    private val myAdapter = MyAdapter(this)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) 
        super.onViewCreated(view, savedInstanceState)
        // Set the RecyclerView adapter as you would normally.
        myRecyclerView.adapter = myAdapter
    

【讨论】:

以上是关于泄漏金丝雀,Recyclerview 泄漏 mAdapter的主要内容,如果未能解决你的问题,请参考以下文章

Android泄漏金丝雀 - 泄漏空活动

使用泄漏金丝雀构建错误

Android泄漏金丝雀日志

无法在泄漏金丝雀跟踪中找到原因

如何使用泄漏金丝雀

SearchView在Android应用中泄漏内存