安卓性能优化之被忽视的内存泄露

起因

写博客就像讲故事,得有起因,经过,结果,人物,地点和时间。今天就容我给大家讲一个故事。人物呢,肯定是我了。故事则发生在最近的这两天,地点在coder君上班的公司。那天无意中我发现了一个奇怪的现象,随着我点开我们App的页面,Memory Monitor中显示占用的内存越来越多(前面的页面已经finish掉了)。咦?什么鬼?

经过

有了问题就解决嘛,俗话说的好,有bug要上,没有bug写个bug也要上。那到底是是什么问题会引起这个现象呢?

Android中内存相关的问题无非就是这么几点:

  • Memory Leaks 内存泄漏
  • Memory Churn 内存抖动
  • OutOfMemory 内存溢出

阿西吧,仔细想想怎么这么像内存泄漏呢。那到底是不是呢?那我们就一点一点分析一下呗。

内存相关数据

关于内存我们可能想了解的数据大概有三点:

  • 总内存
  • 系统当前可用内存
  • 我们可以使用的内存每一个Android设备都会有不同的RAM总大小与可用空间,因此不同设备为app提供了不同大小的heap限制。你可以通过调用getMemoryClass())来获取你的app的可用heap大小。如果你的app尝试申请更多的内存,会出现OutOfMemory的错误。在一些特殊的情景下,你可以通过在manifest的application标签下添加largeHeap=true的属性来声明一个更大的heap空间。如果你这样做,你可以通过getLargeMemoryClass())来获取到一个更大的heap size。然而,能够获取更大heap的设计本意是为了一小部分会消耗大量RAM的应用(例如一个大图片的编辑应用)。不要轻易的因为你需要使用大量的内存而去请求一个大的heap size。只有当你清楚的知道哪里会使用大量的内存并且为什么这些内存必须被保留时才去使用large heap. 因此请尽量少使用large heap。使用额外的内存会影响系统整体的用户体验,并且会使得GC的每次运行时间更长。在任务切换时,系统的性能会变得大打折扣。另外, large heap并不一定能够获取到更大的heap。在某些有严格限制的机器上,large heap的大小和通常的heap size是一样的。因此即使你申请了large heap,你还是应该通过执行getMemoryClass()来检查实际获取到的heap大小。

Java中的四种引用

开始分析之前,有必要先了解下Java的内存分配与回收。

Java的数据类型分为两类:基本数据类型、引用数据类型。

基本数据类型的值存储在栈内存中,而引用数据类型需要开辟两块存储空间,一块在堆内存中,用于存储该类型的对象;另一块在栈内存中,用于存储堆内存中该对象的引用。

其中引用类型变量分为四类:

  • 强引用最常用的引用形式。把一个对象赋给一个引用类型变量,则为强引用。只要一个引用是强引用,则垃圾回收器永远都无法回收这个对象的内存空间,除非JVM终止。
  • 软引用当内存资源充足的时候,垃圾回收器不会回收软引用对应的对象的内存空间;但当内存资源紧张时,软引用所对应的对象就会被垃圾回收器回收。
  • 弱引用不管JVM内存资源是否紧张,只要垃圾回收器运行,弱引用所对应的对象就会被释放。
  • 虚引用虚引用等于没有引用,无法通过虚引用访问其对应的对象。软引用和弱引用在其对象被回收之后,这些引用会被添加到引用队列中去;而虚引用在其对象被回收之前,虚引用就被添加到引用队列中去了。因此虚引用可以在其对象被释放之前进行一些操作。虚引用和引用队列绑定的方法:

Garbage Collection Android中的垃圾回收

Android系统会在适当的时机触发GC操作,一旦进行GC操作,就会将一些不再使用的对象进行回收

执行GC操作的时候,所有线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行。

通常来说,单个的GC并不会占用太多时间,但是大量不停的GC操作则会显著占用帧间隔时间(16ms)。如果在帧间隔时间里面做了过多的GC操作,那么自然其他类似计算,渲染等操作的可用时间就变得少了

Memory Leaks内存泄漏

内存泄漏表示的是不再用到的对象因为被错误引用而无法进行回收。发生内存泄漏会导致Memory Generation中的剩余可用Heap Size越来越小,这样会导致频繁触发GC,更进一步引起性能问题。

总结起来其实很简单:存在无效的引用!

内存泄露可以引发很多的问题,常见的内存泄露导致问题如下:

  • 应用卡顿,响应速度慢(内存占用高时JVM虚拟机会频繁触发GC);
  • 应用被从后台进程干为空进程;
  • 应用莫名的崩溃(也就是超过了HeepSize阈值引起OOM);

内存泄漏分析工具

看到这些问题,突然发现好像离真相越来越近了0.0。

想要更加清楚地实时知晓当前应用程序的内存使用情况,我们需要通过一些工具来实现。比较好用的工具有两种:

  • Memory Analyzer Tool
  • LeakCanary

下面我们分开介绍。

Memory Analyzer Tool

Memory Analysis Tools(点我下载)是一个专门分析Java堆数据内存引用的工具,我们可以使用它方便的定位内存泄露原因,核心任务就是找到GC ROOT位置。接下来说下使用步骤。

抓取内存信息

AndriodStudio中抓取内存信息还是很方便的,有两种方法:

  • 使用Android Device Monitor点击Android Studio工具栏上的Tool–>Android Device Monitor在Android Device Monitor界面中选在你要分析的应用程序的包名,点击Update Heap来更新统计信息,然后点击Cause GC即可查看当前堆的使用情况,点击Dump HPROF file,将该应用当前的内存信息保存成hprof文件,放在桌面即可,操作如下图
  • 直接获取Android Studio的最新版本可以直接获取hprof文件,但是注意在使用之前一定要手动点击 Initiate GC按钮手动触发GC,这样抓到的内存使用情况就是不包括Unreachable对象的。稍等片刻,生成的文件会出现在captures中,然后选择文件,点击右键转换成标准的hprof文件,就可以在MAT中打开了。

使用MAT工具查看分析

这里我写了个简单的demo来测试,这个demo一共有两个页面,在跳转到第二个页面之后,新开一个现成去打印activity信息。

多次进入SecondActivity之后会发现内存一直在增长,并没有降低。

而且log里会不停的输出log,打印当前activity的name。

在MAT中打开抓取到的文件后如图

MAT中提供了非常多的功能,这里我们只要学习几个最常用的就可以了。上图最中央的那个饼状图展示了最大的几个对象所占内存的比例,这张图中提供的内容并不多,我们可以忽略它。红色框中有两个非常有用的工具是我们常用的。

Histogram可以列出内存中每个对象的名字、数量以及大小。

Dominator Tree会将所有内存中的对象按大小进行排序,并且我们可以分析对象之间的引用结构。

我们先来看Histogram

我们应该如何去分析内存泄漏呢?即分析大内存的对象。但是假如我们有目标对象的话,左上角值支持正则表达式的,我们输入SecondActivity。这里我们看到,我们有5个SecondActivity的实例,因为我们引用SecondActivity的现成没有销毁,导致会有很多实例。

接下来对着SecondActivity右键 -> List objects -> with incoming references查看具体SecondActivity实例,如下图所示:

如果想要查看内存泄漏的具体原因,可以对着任意一个MainActivity的实例右键 -> Path to GC Roots -> exclude weak references,结果如下图所示:

可以看到红色框中,因为我们的线程持有SecondActivity的实例,所有导致内存泄漏。

此外,我们可以选择以我们项目的包结构的形式来查看

接下来我们看下Dominator Tree。

关于Dominator Tree我们需要注意三点:

  • 首先Retained Heap表示这个对象以及它所持有的其它引用(包括直接和间接)所占的总内存,因此从上图中看,前两行的Retained Heap是最大的,我们分析内存泄漏时,内存最大的对象也是最应该去怀疑的。
  • 带有黄点的对象就表示是可以被GC Roots访问到的,根据上面的讲解,可以被GC Root访问到的对象都是无法被回收的。
  • 并不是所有带黄点的对象都是泄漏的对象,有些对象系统需要一直使用,本来就不应该被回收。我们可以注意到,有些带黄点的对象最右边会写一个System Class,说明这是一个由系统管理的对象,并不是由我们自己创建并导致内存泄漏的对象。

现在我们可以对着我们想查看的内容点击右键 -> Path to GC Roots -> exclude weak references,为什么选择exclude weak references呢?因为弱引用是不会阻止对象被垃圾回收器回收的,所以我们这里直接把它排除掉,然后一步一步分析。

LeakCanary

leakcanary是一个开源项目,一个内存泄露自动检测工具,是著名的GitHub开源组织Square贡献的,它的主要优势就在于自动化过早的发觉内存泄露、配置简单、抓取贴心,缺点在于还存在一些bug,不过正常使用百分之九十情况是OK的,其核心原理与MAT工具类似。

因为配置十分简单,这里就不多说了,官方文档。

我们看下分析结果

简单直白!

常见内存泄漏情况

  • 构造Adapter时,没有使用缓存的 convertView
  • Bitmap对象不在使用时调用recycle()释放内存
  • Context使用不当造成内存泄露:不要对一个Activity Context保持长生命周期的引用。尽量在一切可以使用应用ApplicationContext代替Context的地方进行替换。
  • 非静态内部类的静态实例容易造成内存泄漏:即一个类中如果你不能够控制它其中内部类的生命周期(譬如Activity中的一些特殊Handler等),则尽量使用静态类和弱引用来处理(譬如ViewRoot的实现)。
  • 警惕线程未终止造成的内存泄露;譬如在Activity中关联了一个生命周期超过Activity的Thread,在退出Activity时切记结束线程。一个典型的例子就是HandlerThread的run方法是一个死循环,它不会自己结束,线程的生命周期超过了Activity生命周期,我们必须手动在Activity的销毁方法中中调运thread.getLooper().quit();才不会泄露。
  • 对象的注册与反注册没有成对出现造成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等。
  • 创建与关闭没有成对出现造成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,流等对象必须手动关闭等。
  • 不要在执行频率很高的方法或者循环中创建对象(比如onmeasure),可以使用HashTable等创建一组对象容器从容器中取那些对象,而不用每次new与释放。
  • 避免代码设计模式的错误造成内存泄露;譬如循环引用,A持有B,B持有C,C持有A,这样的设计谁都得不到释放。

结果

真相只有一个,那就是确实是由于内存泄漏才出现我遇到的情况。程序员嘛,谁还不踩个坑,跳出来,拍拍身上的灰尘,总结一下,过两天又是一条帮帮的coder。源码

打赏支持我写出更多好文章,谢谢!

打赏作者

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

1 4 收藏 评论

关于作者:PleaseCallMeCoder

我是 PleaseCallMeCoder,一个小小的90后程序员。热衷于移动开发,喜欢研究新技术,奔跑在成为大神的路上。 个人主页 · 我的文章 · 29 ·   

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部