Android知识进阶

Android中的内存泄漏模式

2018-06-07  本文已影响16人  公子小水

原文

什么是内存泄漏?

每个应用程序都需要内存作为资源来完成其工作。为了确保Android中的每个应用都有足够的内存,Android系统需要高效管理内存分配。当内存不足时,Android运行时会触发垃圾收集(GC)。GC的目的是通过清理不再有用的对象来回收内存。它通过三个步骤实现它。

  1. 从GC根开始遍历内存中的所有对象引用,并标记具有GC根引用的活动对象。
  2. 所有未标记的对象(垃圾)都会从内存中清除掉。
  3. 重新排列活着的对象
从GC根标记活体对象

简而言之,为用户提供服务的所有内容都应该保存在内存中,其他内容都会从内存中清除以释放资源。

但是,如果代码写得很糟糕,未使用的对象以某种方式从可访问对象中引用,则GC会将未使用的对象标记为有用的对象,因此无法将其删除。这被称为内存泄漏。

内存泄漏[2]

为什么内存泄漏不好?

没有任何对象应该长时间停留在内存中。它们占用宝贵的资源,否则这些资源可用于为用户提供有实际价值的东西。特别地,对于Android,它会导致以下问题。

1。发生内存泄漏时可用内存不足。结果,Android系统将触发更频繁的GC事件。GC事件是世界末日事件。这意味着GC发生时,UI的呈现和事件处理将停止。Android有一个16ms的绘图窗口。当GC需要很长时间时,Android就会开始丢帧。一般来说,100到200ms是一个阈值,在此以上用户在应用程序中会感觉到缓慢[1]。

Android绘图窗口[3] 由于频繁GC而丢帧[3]

在Android中,应用程序响应性由活动管理器(Activity Manager)和窗口管理器(Window Manager )系统服务进行监视。当Android检测到以下某种情况时,它将显示特定应用程序的ANR对话框:[1]:

Android未响应(ANR)

我相信没有用户会喜欢看到这个应用程序没有响应的弹出框。

2。当你的应用程序有内存泄漏时,它不能从未使用的对象声明内存。因此,它会问Android系统要更多的内存。但是有一个限制。系统最终会拒绝为你的应用分配更多内存。发生这种情况时,应用程序用户将会发生内存不足(out-of-memory)的崩溃。当然没有人喜欢崩溃。用户可能会卸载你的应用程序或开始给你的应用程序不好的评论。

3。内存泄漏问题在QA测试中很难找到。他们很难重现。而崩溃报告通常很难推理,因为它可能在Android系统拒绝内存分配的任何时间,任何地方发生。

如何识别内存泄漏?

发现泄漏需要对GC工作原理有很好的理解。它需要努力编写代码并做代码审查。但是在Android中,有一些很好的工具可以帮助你识别可能的泄漏,或者在某些代码看起来可疑时确定是否有泄漏。

1。 来自Square的Leak Canary 是一款检测应用程序内存泄漏的好工具。它会在你的应用中创建对活动(activities)的弱引用。(你也可以通过添加到任何其他对象的观察点来自定义它。)然后它检查GC之后引用是否被清除。如果没有,它将堆(heap)转储到一个.hprof文件并分析它,以确认是否有泄漏。如果有,它会显示一个通知,并在一个单独的应用程序中显示泄漏发生的引用树。你可以在这篇文章中找到更多关于Leak Canary的信息: LeakCanary:检测所有内存泄漏 。我强烈建议你将Leak Canary安装到你的开发人员/测试版本中。它可以帮助开发人员和QA在你的应用到达用户手中之前找到内存泄漏。

Leak Canary的屏幕截图

2。Android Studio有一个方便的工具来检测内存泄漏。如果你怀疑你的应用程序中有一段代码可能会泄漏一个活动(activity),你可以执行此操作。

步骤1:编译并在连接到你计算机的设备或仿真器上运行调试版本。

步骤2:转到可疑活动(activity),然后返回到上一个活动(activity),这将从任务堆栈中弹出可疑活动(activity)。

第3步:在Android Studio -> Android Monitor窗口 -> Memory 部分,单击Initiate GC按钮。然后点击Dump Java Heap按钮。

第4步:按下Dump Java Heap按钮时,Android Studio将打开转储的.hprof文件。在hprof文件查看器中,有几种方法可以检查内存泄漏。你可以使用右上角的Analyzer Tasks工具自动检测泄漏的活动。或者你可以从左上角的切换器将视图模式切换到Package Tree View ,找到应该销毁的活动(activity)。检查活动对象的Total Count 。如果有一个或多个实例,则表示存在泄漏。

第5步:一旦找到泄漏的活动,请检查底部的引用树,找出哪些对象正在引用应该已经死掉了(should-have-been-dead)的活动。

你可以从HPROF Viewer and Analyzer中找到有关Android Studio功能的更多信息。

什么是常见的泄漏模式?

有很多方法可以导致Android中的内存泄漏。总而言之,主要有三类。

  1. 将活动泄漏到静态引用(static reference)
  2. 将活动泄漏到工作者线程(worker thread)
  3. 泄漏线程本身

在我的Github的repoSinsOfMemoryLeaks中,我做了一个应用程序,它以各种方式泄漏内存。

frank-tan/SinsOfMemoryLeaks
SinsOfMemoryLeaks - Android开发中一些常见的内存泄漏模式以及如何修复/避免它们

Leak分支中,你可以看到具有各种内存泄漏的所有代码。你也可以在设备或仿真器上运行它,并使用前面提到的工具来跟踪泄漏。在FIXED分支中,你将看到泄漏是如何修复的。如果你不确信,你可以再次使用前面提到的工具来查看泄漏是否真的被修复。这两个分支具有不同的应用ID,因此你可以将它们安装在同一设备上同时试用。

现在我将迅速浏览3个主要类别中不同方式的泄漏。

将活动泄漏到静态引用

只要你的应用程序在内存中,静态引用就会存在。一个活动的生命周期通常会在应用程序的生命周期中被释放并重新创建多次。如果你直接或间接从静态引用处引用活动,活动在被销毁后不会被垃圾收集。一个活动的大小范围可以从几千字节到许多兆字节,具体取决于它的内容。如果它具有大视图层次结构或高分辨率图像,则会导致大量内存泄漏。

这个类别的一些泄漏可以是

  1. 将活动泄漏到静态视图
  2. 将活动泄漏到静态变量
  3. 将活动泄漏到单例对象(singleton object)
  4. 将活动泄漏到活动的内部类的静态实例

将活动泄漏到工作者线程

一个工作者线程也可以放生(out-live)一个活动。如果你直接或间接地从活动时间比较长的工作者线程中引用活动(Activity),就会泄漏活动(Activity)对象。这个类别的几个方式可以是

  1. 泄漏活动到一个线程
  2. 泄漏活动到处理程序(handler)
  3. 将漏洞活动添加到

同样的原则适用于其他线程技术,如thread poolExecutorService

泄漏线程本身

每当你从一个活动开始一个工作者线程时,你就有责任自己管理工作者线程。因为工作者线程的活动时间可能比活动时间长,所以当活动被销毁时,应该正确停止工作者线程。如果你忘记了这一点,你就冒着泄漏工作者线程的危险。示例在这里

特定泄漏的影响是什么?

理想情况下,你应该避免编写任何导致内存泄漏的代码,并修复应用程序中存在的所有内存泄漏。但实际上,如果你正在处理旧的代码库并需要优先处理不同任务(包括修复内存泄漏),则可以在以下几个方面评估严重性。

1。泄漏的内存有多大?

并非所有的内存泄漏都是相同的。一些泄漏几千字节;有些可能会泄漏很多兆字节。你可以使用前面提到的工具找出它,并确定内存泄漏的大小是否对你的用户群的设备至关重要。

2。泄漏的对象在内存中驻留多长时间?

只要工作者线程自己存在,工作者线程中的一些泄漏就会一直存在。你应该检查你的工作者线程在最糟糕的情况下会持续多久。在我的代码示例中,我在工作者线程中有无限循环,所以它永远持有泄漏对象的内存。但实际上,大多数工作者线程都会执行简单的任务,例如访问文件系统或进行网络调用,这可能是短暂的,或者你通常会设置超时。泄漏的最大时间是确定修复内存泄漏优先级的考虑因素。

3。有多少对象可以泄漏?

有些内存泄漏只泄露一个对象,比如我的repo中静态引用示例中的对象。只要创建新活动,该静态引用就开始引用新活动。泄露的旧活动很明显会被垃圾收集。所以最大泄漏总是一个活动实例的大小。但是,其他泄漏,在创建新对象后会不断泄漏。在Leaking Threads示例中,该活动每次创建时都会泄漏一个线程。所以如果你旋转设备20次,就会有20个工作者线程被泄漏。这可能是非常糟糕的,因为如果应用程序不断泄漏新的实例,该应用程序将很快将设备上的所有可用内存用完。即使一个对象实例相对较小,我也很可能会修复所有这种类型的泄漏。

如何修复/避免它?

看看我的repo的FIXED分支

关键要点是:

  1. 当你决定在你的活动类中有一个静态变量时要非常小心。它真的有必要吗?是否有可能静态变量直接或间接引用活动(间接可以引用内部类对象,附加视图(attached view)等)?如果是这样,你是否在Activity的onDestroy时清除引用呢?
  2. 当你将活动作为侦听器(listener)传递给单例对象或x管理器实例时,请确保你了解其他对象对你传入的活动实例的作用。如果需要,请在Activity onDestroy上清除引用(将侦听器(listener)设置为null)。
  3. 在活动类中创建内部类时,如果可能,请将其设置为静态。内部类和匿名类具有对包含类的隐式引用。因此,如果内部/匿名类的实例比包含类的实例寿命更长,那么你遇到了麻烦。例如,如果你创建一个匿名可运行类(anonymous runnable class)并将其传递给工作者线程或匿名处理程序类(anonymous handler class),并使用它将任务传递给其他线程,则可能会泄漏包含的类对象。为了避免泄漏风险,请使用静态类而不是内部/匿名类。
  4. 如果你正在编写单例或x管理器类,则需要存储侦听器(listener)实例的引用,并且不能控制类的用户如何管理引用,可以请使用WeakReference作为侦听器(listener)引用。WeakReference并不妨碍他们的引用被GC清除并回收[4]。虽然这个功能在防止内存泄漏方面听起来不错,但它也可能是一个副作用,因为不能保证被引用的对象在需要时处于活动状态。所以用它作为修复内存泄漏的最后手段。
  5. 在Activity的onDestroy()中,始终记住要终止你启动过的工作者线程。

## 总结

我们研究了什么是内存泄漏,它是如何发生的,以及它在Android系统中造成的后果。然后我们介绍了两种检测和识别内存泄漏的工具,分析了Android中常见的内存泄漏模式,如何评估泄漏的严重程度以及如何避免/修复常见泄漏。不要忘了查看我的Github仓库中常见内存泄漏模式和修复的代码示例。快乐制作Android应用,每个人:)

frank-tan/SinsOfMemoryLeaks
SinsOfMemoryLeaks - Android开发中一些常见的内存泄漏模式以及如何修复/避免它们

参考

[1] 保持你的应用程序响应性

[2] Java内存管理

[3] HPROF查看器和分析器

[4] WeakReference

[5] 最后了解引用在Android和Java中的工作原理

上一篇下一篇

猜你喜欢

热点阅读