转载请注明本文出自 clevergump 的博客:http://blog.csdn.net/clevergump/article/details/52013873, 谢谢!
在 Android 开发中, 内存优化是APP性能优化中很重要的一个部分. 而在内存优化中, 最重要的就是修复内存泄漏问题. 本文就来介绍一下内存泄漏的基本概念以及常用的检测手段.
1. 什么是内存泄漏
简单来说, 当一个对象不再被使用时, 理应不存在任何强引用指向他从而可以让垃圾回收器(GC)在未来的某个时间点将其回收的, 但由于某些原因导致有强引用依然指向该对象, 使得该对象无法被垃圾回收器(GC)回收的现象, 我们就称该对象被泄漏到内存中了, 简称”内存泄漏”. 其实, 在这种情况下, 不仅该对象会泄漏, 而且该对象内部包含的其他引用所指向的对象也将发生泄漏. 也就是一种牵连效应.
概念总是枯燥难懂的, 那就举个例子吧.
假如一个 Activity 的布局文件中定义了多个控件, 例如: 下面的 DemoActivity
public class DemoActivity extends Activity {
private TextView mTv;
private ImageView mIv;
private Button mBtn;
}
上面是我们通常的写法, DemoActivity
对其内部定义的所有控件都持有强引用 (请自行查阅四种引用的概念和区别). 如果我们在某个时刻需要销毁该 DemoActivity
, 但是实际上因为某些原因导致它没有销毁或者没有及时销毁(即: 延迟了一段时间才销毁), 依然存在于内存中, 那么我们就说该 DemoActivity
对象发生了内存泄漏, 对于后一种情况, 就是在延迟的那段时间内发生了内存泄漏. 同时, 由于该DemoActivity
对其内部定义的各个控件都持有了强引用, 导致这些控件也没有销毁, 也发生了内存泄漏. 同理可以推测, 这些控件内部包含的一些强引用所指向的对象也将发生内存泄漏…… 这一系列泄漏的对象组成了一个强引用的链条, 一旦该链条中处于头部的某个对象发生了内存泄漏, 那么这个链条中, 被该对象所直接或间接引用的对象也将受牵连而发生内存泄漏.
2. 为什么要检测并修复内存泄漏
为什么要检测并修复内存泄漏问题呢? 因为内存泄漏问题轻则造成 APP 卡顿不流畅, 重则导致 OOM 而让 APP 崩溃, 所以修复内存泄漏问题是非常必要的. 更具体地说, 一款手机的可用内存空间是非常有限的, 一个应用程序可申请的最大内存空间也是有限的, 如果一个对象发生了内存泄漏, 那么它所直接或间接强引用的那些对象也将发生内存泄漏, 可能该对象本身占用的内存空间并不大, 但是由它直接或间接强引用的那些对象中, 就可能存在着占用内存较大的对象. 所以, 一个占用内存不太大的小对象的泄漏, 由上述链式连锁反应所造成的整体泄漏量也可能是非常庞大的. 例如: 我们有一个页面, 该页面中包含了一个用于展示一系列图片的 ListView
或 GridView
控件. 如果用于表示该页面的 Activity
或 Fragment
对象发生了内存泄漏, 那么, 该页面中的 ListView
或 GridView
控件也会泄漏, ListView
或 GridView
的每个条目中所包含的 ImageView
对象也将发生泄漏, 而 ImageView
是用来展示图片的, 因此, 每一个 ImageView
对象所(强)引用的 Bitmap
对象也会发生泄漏. 而 Bitmap
对象的内存泄漏是非常严重的. 为什么这么说呢? 我们来具体分析一下一个 Bitmap
对象被加载到内存中所占据的内存空间大小吧. 例如: 我们要加载一张分辨率为 960 * 637 的图片, 存储方式采用 RGB565, RGB565 意味着该图片的每个像素点在内存中占用2个字节(5+6+5 = 16bit = 2Byte), 如果我们不对该图片进行压缩处理, 而是直接加载原图的话, 那么这张图片加载到内存中就会占据约 1.2MB 的空间 (960 * 637 * 2 = 1223040 Byte ≈ 1.2MB). 如果它发生了泄漏, APP 就会损失 1.2MB 的可用空间. 而一个包含有图片的 ListView
GridView
通常加载的图片数量远远不止一张, 那么多个 Bitmap
对象同时泄漏, APP的可用内存空间一下子就减少了很大一部分, 泄漏多了就会让页面的滑动变得非常卡顿, 甚至会提示 APP 已经崩溃. 所以我们说, Bitmap
对象的内存泄漏是非常严重的. 上个图证明一下前边我们对 Bitmap
对象占用内存大小的计算:
看图中右边 Watches 窗口中计算的 bitmap 在内存中的大小 bitmap.getByteCount()的值, 确实就是我们自己计算的 1223040 Byte.
3. 如何判断某个页面是否存在内存泄漏
介绍完内存泄漏的基本概念及其严重性以后, 我们可能会思考, 如何判断某个页面是否存在内存泄漏? 其实, Android Studio 已经为我们提供了相关的检测工具, 我们只需利用好这些工具即可进行判断了. 当然, 这里提醒一下各位朋友, 这些工具并不会直接告诉我们 “某个地方有泄漏”, “某个地方无泄漏” 等等这么直白的结论, 而只会提供给我们一些数据和图表, 至于是否有泄漏, 需要我们自己结合这些数据和图表去判断.
这些检测工具在哪里呢? 我们看下边这张图, Android Monitor 标签中有 logcat 和 Monitors 两个子标签:
其中, logcat 是我们非常熟悉的用于查看 log 的地方. 而我们判断内存泄漏, 则需要用到另外一个标签, 即: Monitors. 点击 Monitors 标签, 会看到下图:
可以看到, Monitors 标签可以展示内存(Memory), 网络(Network), CPU, GPU这些重要指标的实时数值以及变化曲线图. 我们要判断内存泄漏, 只需关注最上方的内存(Memory)即可. 上图中有几个重要的地方需要大家了解一下:
- Initial GC 按钮(
): 点击此按钮可手动触发 GC 进行垃圾回收. 每次垃圾回收都能回收掉内存中弱引用指向的对象.
- Dump Java Heap 按钮(
): 点击此按钮可保存这一瞬间该进程在 Java 堆内存中对象分配情况的快照 (“快照”可以理解为 “截图”的意思). 这里请大家稍微留意一下, “Dump Java Heap” 是个动词, 意思是获取 Java 堆内存的快照, 后文将要提到的 “Heap Dump”是个名词, 意思是 Java 堆内存的快照. 总之, 只要见到这几个单词的组合, 表示的含义就基本相同.
- Allocated 数值(
): 当前实时分配的堆内存数值.
我们要判断 APP 的某个页面是否存在内存泄漏, 只需在进入该页面前, 先点击几次 Initial GC 按钮(), 让垃圾回收器先进行几次回收, 然后记录 Allocated 的瞬时最小数值(一定要记录点击GC后的最小值, 因为只有最小值才是 GC 执行后的真实结果. 但如果你点击GC后过了几秒才去记录, 那么在这几秒内, 当前页面可能又会为某些弱引用所引用的对象再次分配内存空间从而使 Allocated 的数值又增长了上去, 那么这时的 Allocated 的数值就不准确了, 因为我们要记录的是无法被GC回收的内存大小), 记录完 Allocated 值以后, 进入该页面再退出回到先前的页面, 然后再次点击几次 Initial GC 按钮(
), 并再次记录点击GC以后那一瞬间几个 Allocated 数值中的最小值. 然后将前后两次记录的 Allocated 数值进行对比, 如果发现后一个数值要大于或者远大于前一个数值, 就说明该页面可能存在内存泄漏. 注意: 一定要在执行完GC后再记录数据, 因为执行完 GC 后的数值表示无法被回收的对象大小, 如果被测试的页面不存在内存泄漏或者只存在轻微以至于可以忽略不计的泄漏, 那么关闭该页面后再执行 GC 操作, 此时所占用的内存空间理应和进入该页面前所占据的内存空间大小基本相等. 而如果数值增长了较多, 就说明该页面可能存在内存泄漏.
下面用一个例子来演示一下我们刚才所介绍的判断过程. 假如我们的 APP 有如下两个页面:
</