Android应用篇 - 内存泄漏如何分析

今天来聊聊 Android 中的内存泄漏如何分析。

目录:

  1. 什么是内存泄漏
  2. Android 中的内存泄漏场景
  3. 分析内存泄漏的几种方法
  4. 内存优化的一些点

1. 什么是内存泄漏

内存不在 gc 掌控之内了,当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用,从而就导致该对象不能被回收。这种本该被回收的对象不能被回收而停留在内存中的现象,就是内存泄漏。

关于引用,可以看看我之前的这篇文章:Java篇 - 四种引用(Reference)实战

2. Android 中的内存泄漏场景

  • 2.1 单例导致内存泄露

单例模式在 Android 开发中会经常用到,但是如果使用不当就会导致内存泄露。因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄露。比如 Activity。

  • 2.2 静态变量导致内存泄露

静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。比如类的变量定义为静态,它内部持有了 Activity 的强引用。

  • 2.3 非静态内部类导致内存泄露

非静态内部类 (包括匿名内部类) 默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。非静态内部类导致的内存泄露在 Android 开发中有一种典型的场景就是使用 Handler。

  • 2.4 未取消注册或回调导致内存泄露

比如我们在 Activity 中注册广播,如果在 Activity 销毁后不取消注册,那么这个刚播会一直存在系统中,同上面所说的非静态内部类一样持有 Activity 引用,导致内存泄露。因此注册广播后在 Activity 销毁后一定要取消注册。

  • 2.5 Timer 和 TimerTask 导致内存泄露

当 Activity 销毁的时,有可能 Timer 还在继续等待执行  TimerTask,它持有 Activity 的引用不能被回收,因此当 Activity销毁的时候要立即 cancel 掉 Timer 和 TimerTask,以避免发生内存泄漏。

  • 2.6 集合中的对象未清理造成内存泄露

这个比较好理解,如果一个对象放入到 ArrayListHashMap 等集合中,这个集合就会持有该对象的引用。当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用 (而此对象已经无用了),这个对象就造成了内存泄露。并且如果集合被静态引用的话,集合里面那些没有用的对象更会造成内存泄露了。所以在使用集合时要及时将不用的对象从集合 remove,或者 clear 集合,以避免内存泄漏。

  • 2.7 资源未关闭或释放导致内存泄露

在使用 IOFile 流或者 SqliteCursor 等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果及时不关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。因此我们在不需要使用它们的时候就及时关闭,以便缓冲能及时得到释放,从而避免内存泄露。

  • 2.8 属性动画造成内存泄露

动画同样是一个耗时任务,比如在 Activity 中启动了属性动画 (ObjectAnimator),但是在销毁的时候,没有调用 cancel方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用 Activity,这就造成 Activity 无法正常释放。因此同样要在 Activity 销毁的时候 cancel 掉属性动画,避免发生内存泄漏。

  • 2.9 WebView 造成内存泄露

关于 WebView 的内存泄露,因为 WebView 在加载网页后会长期占用内存而不能被释放,因此在 Activity 销毁后要调用它的destory() 方法来销毁它以释放内存。

3. 分析内存泄漏的几种方法

  • 3.1 LeakCanary

通过 LeakCanary 的使用,它可以为我们快速找到内存泄漏的位置,但并不能够提供我们内存泄漏的原因,有的时候,内存泄漏的位置是由于其他原因导致的。教程:LeakCanary 中文使用说明

  • 3.2 StrictMode

StrictMode 在Android 2.3 的时候就已经引入了,虽然到当前这个工具年代比较久远了,但属实还是非常好用的, 在开发阶段使用这个工具,能够很好的帮助发现开发中的一系列不规范的编码,例如主线程访问网络,主线程读写磁盘,等等耗时操作,另外的一大特性就是可以帮助开发时,发现程序存在内存泄漏的情况。

StrictMode的功能主要分为两大块:一块是关于 Thread,线程规范的监测,另一块是关于 VM,内存的监测。在 StrictMode 下分别是 ThreadPolicy 和 VMPloicy。教程:StrictMode(严格模式)使用

  • 3.3 AndroidMonitor + .hprof + MAT

首先需要明确的一点是 LeakCanary、StrictMode 并不能检查出所有的内存泄漏,所以在我们对应用程序反复点击调试之后,需要我们人工审查这些类的实例。通常绝大多数类的实例例如 Activity、Fragment 等每个类的实例是只有一个的,当然也是要看具体的程序实现来看待,在 ViewPager 中等情况就可以出现一个类有多个实例的情况,这也因实际情况来看待。如果超出了实际情况的实例对象数那么就很有可能是存在了内存泄漏。在我们人工审查这些类的实例通常都会先检查 Activity、Fragment、自定义View 等等的实例情况,因为伴随这些一起泄漏的都是高概率。

所以一般是先用 LeakCanary 或者 StrictMode 确定有内存泄漏,然后再分析 .hprof 定位具体详细的信息。

主要难点在于 .hprof 的分析,教程:Android studio结合MAT分析hprof文件

4. 内存优化的一些点

  • 4.1 数据结构优化

频繁使用字符串拼接用 StringBuilder (字符串之间通过+的方式进行字符串拼接会产生中间字符串,这样就造成了内存浪费,并且字符串进行拼接也是比较耗时的)。ArrayMap、SparseArray 替换 HashMap (内存使用更少,并且在数据量大的时候效率会比较高)。

内存抖动 (就是在短时间之内申请了很多空间,但是使用一下子就不用了,之后过了一会以后又申请很多的空间,类似的反复下去,这样就会当空间不足的时候,不断的让 GC 被调用,导致 APP 卡顿)。

  • 4.2 对象复用
  • 复用系统中自带的资源。
  • ListView/GridView 的 ConvertView 复用。
  • 避免在 onDraw 方法里面执行对象的创建 (因为 onDraw 在界面,图像或者 View 一有变化的化就会重新调用,如果在里面执行对象的创建的话,就会影响绘制的时间)。
  • 4.3 万恶的 static

static 是个好东西,声明赋值调用就是那么的简单方便,但是伴随而来的还有性能问题。由于 static 声明变量的生命周期其实是和 APP 的生命周期一 样的,有点类似与 Application。如果大量的使用的话,就会占据内存空间不释放,积少成多也会造成内存的不断开销,直至挂掉。static 的合理 使用一般用来修饰基本数据类型或者轻量级对象,尽量避免修复集合或者大对象,常用作修饰全局配置项、工具类方法、内部类。

  • 4.4 无关引用

很多情况下,我们需求用到传递引用,但是我们无法确保引用传递出去后能否及时的回收。比如比较有代表性的 Context 泄漏,很多情况下当 Activity 结束掉后,由于仍被其他的对象指向导致一直迟迟不能回收,这就造成了内存泄漏。

  • 4.5 善用 SoftReference/WeakReference/LruCache

Java、Android 中有没有这样一种机制呢,当内存吃紧或者 GC 扫过的情况下,就能及时把一些内存占用给释放掉,从而分配给需要分配的地方。答案是肯定的,Java 为我们提供了两个解决方案。如果对内存的开销比较关注的 APP,可以考虑使用WeakReference,当 GC 回收扫过这块内存区域时就会回收;如果不是那么关注的话,可以使用 SoftReference,它会在内存申请不足的情况下自动释放,同样也能解决 OOM 问题。同时 Android 自 3.0 以后也推出了 LruCache 类,使用 LRU 算法就释放内存,一样的能解决 OOM,如果兼容 3.0 一下的版本,请导入 v4包 。关于无关引用的问题,我们传参可以考虑使用WeakReference 包装一下。

  • 4.6 谨慎 handler

在处理异步操作的时候,handler + thread 是个不错的选择。但是相信在使用 handler 的时候,大家都会遇到警告的情形,这个就是 lint 为开发者的提醒。handler 运行于 UI 线程,不断处理来自 MessageQueue 的消息,如果 handler 还有消息需要处理但是Activity 页面已经结束的情况下,Activity 的引用其实并不会被回收,这就造成了内存泄漏。解决方案,一是在 Activity 的onDestroy 方法中调用 handler.removeCallbacksAndMessages(null); 取消所有的消息的处理,包括待处理的消息;二是声明handler 的内部类为 static。

  • 4.7 Bitmap 终极杀手

Bitmap 的不当处理极可能造成 OOM,绝大多数情况都是因这个原因出现的。Bitamp 位图是 Android 中当之无愧的胖小子,所以在操作的时候当然是十分的小心了。

  • 4.8 Cursor 和 I/O 流及时关闭

在查询 SQLite 数据库时,会返回一个 Cursor,当查询完毕后,及时关闭,这样就可以把查询的结果集及时给回收掉。I/O 流操作完毕,读写结束,记得关闭。

  • 4.9 页面背景和图片加载

在布局和代码中设置背景和图片的时候,如果是纯色,尽量使用 color;如果是规则图形,尽量使用 shape 画图;如果稍微复杂点,可以使用 9patch 图;如果不能使用 9patch 的情况下,针对几种主流分辨率的机型进行切图。

  • 4.10 BroadCastReceiver、Service

绑定广播和服务,一定要记得在不需要的时候给解绑。

  • 4.11 使用线程池

线程线程不再需要继续执行的时候要记得及时关闭,开启线程数量不易过多,一般和自己机器内核数一样最好,推荐开启线程的时候,使用线程池。

发布了126 篇原创文章 · 获赞 215 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/u014294681/article/details/88694996
今日推荐