Android Studio3.6的内存泄漏检测功能 VS LeakCanary

2020年2月,谷歌发布了Android Studio 3.6版。它包括一个新的“内存泄漏检测”功能。这是否意味着我们不再需要流行的内存泄漏检测库“Leak Canary”了?在过去的几天里,我花了一些时间来研究android studio的新特性,希望在这里分享我的发现和想法。

内存泄露示例程序

我创建了一个示例应用程序,其中包含一个名为 LeakingActivity 的活动。顾名思义,此活动演示了导致泄漏的常见原因。它将侦听器定义为内部类,并将该侦听器注册到具有较长生命周期的对象。在我们的例子中,这个对象是一个单例,它的寿命与应用程序的寿命一样长。由于我们在离开活动时没有注销侦听器,所以即使在Android框架调用 onDestroy() 方法之后, Singleton 仍然保留对它的引用。侦听器是活动的内部类,因此对活动有一个隐式引用,因此不会对其进行垃圾收集,也不会从中释放内存空间。这最多会导致不必要的高内存使用率,并导致应用程序崩溃,因为 java.lang.OutOfMemoryErrors 错误在最坏的情况下。

class LeakingActivity: AppCompatActivity() {

    private val listener = Listener()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_leaking)
    }

    override fun onStart() {
        super.onStart()
        GlobalSingleton.register(listener)
    }

    override fun onStop() {
        super.onStop()
        
        // we forget to unregister our listener so a reference
        // of the Singleton to the listener and to the Activity
        // still exists after the user navigates away fro that
        // activity
        
        // GlobalSingleton.unregister(listener)
    }

    // inner class has implicit reference to enclosing Activity
    private inner class Listener : GlobalSingletonListener {
        override fun onEvent() { }
    }
}
object GlobalSingleton {

    // a reference to the listener of the Activity is kept as long as 
    // unregister isn't called
    private val listeners = mutableListOf()

    fun register(listener: GlobalSingletonListener) {
        listeners.add(listener)
    }

    fun unregister(listener: GlobalSingletonListener) {
        listeners.remove(listener)
    }
}

interface GlobalSingletonListener {
    fun onEvent()
}

使用Android Studio 3.6查找内存泄漏

为了使用新的“泄漏检测”功能检查应用程序是否存在泄漏,您必须启动Android Studio内存探查器。如果您是在Android 7.1或更低版本的设备上运行应用程序,则必须启用高级分析才能查看所有分析数据。单击导航栏中的“配置文件应用程序”,安装、启动并配置您的应用程序。

要开始分析已经运行的应用程序,只需单击Android Studio底部栏中的“Profiler”,然后单击(+)添加会话:

我切换到我的模拟器,打开和关闭泄漏活动两次。在探查器中,我们看到 LeakingActivity 被创建,然后被销毁。

现在我们想知道这个活动是否被垃圾收集。因此,我们必须通过单击内存通道来打开内存探查器,这在我看来有点不直观。我们可以看到为不同类别的对象(Java、Native、Graphics…)分配的所有内存。

接下来,我们必须通过单击顶部的图标来转储Java堆。

通过单击force garbage collection first强制垃圾收集是有意义的,只是为了确保垃圾收集确实发生了。

转储完成后,我们可以看到所有内存分配:

Android Studio 3.6中的新功能。是以下复选框:

通过选中此新复选框,我们可以看到我们的 LeakingActivity 泄漏了两次:

调查内存泄密的原因

下一步我们显然要做的是修复漏洞。当对不再使用的“死”对象的引用仍然存在时,就会发生内存泄漏。我们需要找到这些参考资料并把它们处理掉。让我们通过单击 LeakingActivity 打开右侧面板上的实例视图来进行一些调查。

通过单击实例前面的箭头,我们可以在“引用视图”中看到对该实例的所有引用:

老实说,我不知道找到导致泄漏的确切参考路径的最佳方法是什么。我做了一些研究来找出LeakCanary是如何得到导致泄漏的参考路径的。在LeakCanary中,此路径称为“Leak Trace”,根据其文档,它是“从垃圾收集根到保留对象的最佳强引用路径”。在其他地方,我看到它是最短路径。垃圾收集根是永远不会被垃圾收集的对象(比如应用程序对象,或者在我们的例子中是单例对象)。

保留的对象是 LeakingActivity 。我不确定是否可以在实例视图中看到引用的类型(强、弱等)。但是,我们可以根据深度来排序引用,这是根据内存探查器文档“从任何垃圾收集根到所选实例的最短跃点数”。

当我们查看最短深度为3的引用的第一条路径时,我们可以确定以下引用链:GlobalSingleton(垃圾收集根)有一个对 listener ArrayList 的引用,它有一个对活动的 listener 内部类的引用,该类有一个对 LeakingActivity 的(隐式)引用。

这看起来很可疑,因为GlobalSingleton不应该引用已销毁的活动,然后我们意识到忘记注销侦听器。

class LeakingActivity: AppCompatActivity() {

    private val listener = Listener()

    ...

    override fun onStop() {
        super.onStop()
        
        // fixing the leak by unregistering the listener
        GlobalSingleton.unregister(listener)
    }

   ...
}

我们发现并修复了新的泄漏通过Android Studio检测功能!所以我们不再需要金丝雀了,对吧?

LeakCanary还有用吗?

嗯,使用LeakCanary来检测漏洞还是有很多好处的。

首先,LeakCanary总是监视你的应用程序是否存在漏洞。使用Android Studio Profiler,你必须主动监控你的应用程序,我们都知道,作为开发人员,我们通常有很多其他事情要做,而且倾向于“稍后”进行分析(也就是从不)。LeakCanary不断地“提醒”你还有一些漏洞需要修复,这增加了你实际修复它们的机会。

第二,使用LeakCanary更容易找到泄漏的原因。我们不必深究android studio令人困惑的“参考视图”,但可以得到一个很好的“泄漏跟踪”。它甚至在最有可能导致泄漏的参考点下方画了一条红色的曲线。

第三,LeakCanary收集了库和Android框架中已知的漏洞,因此它不会向您显示无法修复的漏洞。此外,通过将LeakCanary添加到objectWatcher,您不仅可以检测活动或片段的泄漏,还可以检测任何其他对象(例如服务或Dagger组件)的泄漏。

此外,其他特性,如在生产中计数保留对象或在运行仪表测试时进行泄漏检测,也非常有用。

结论

android studio3.6新的“泄漏检测”功能是一种很好的、方便的方法,可以检测泄漏的片段和活动,而无需向应用程序中添加第三方库。在我看来,偶尔检查一下小型或业余爱好的应用程序是否有漏洞就足够了。不过,找出内存泄漏的原因并不是那么容易。对于任何关心低内存占用率和 java.lang.OutOfMemoryError 错误崩溃了,我想没有办法使用LeakCanary。它不断地监视应用程序的内存泄漏,除了其他有用的特性外,还使开发人员很容易识别内存泄漏的原因。