Android内存泄漏的检测流程、捕捉以及分析

简述:

一个APP的性能,重度关乎着用户体验,而关于性能检测的一个重要方面,就是内存泄漏,通常内存泄漏的隐藏性质比较强,不同于异常导致的程序Crash,在异常导致的Crash中,我们能够及时的发现程序问题的存在,并通过log日志定位到问题所在的具体位置,然后及时进行解决,而内存泄漏则不同,在APP中存在内存泄漏的情况下,用户在低频率短时间的使用中,并不能察觉到有什么异样,反之,随着使用频率的提高和使用时长的增加,内存泄漏就会一直慢慢积累,消耗内存,从而会导致手机卡顿,直至APP崩溃,所以防止APP内存泄漏的出现,是至关重要的。

理论阐述内存泄漏导致的原因:

在android开发中,jvm具有自动回收的机制,会不定时不定期的去清理无用的被占用的内存,而在理论上不需要再被使用的内存,在实际中却还持有对这一块内存的引用,导致GC时,不会被回收释放掉,这部分内存就会随着程序的运行不断堆积,从而导致应用分配的内存不够使用导致卡顿、ANR异常等情况。

导语

关于内存泄漏的检测,我们分为了以下几个阶段: 
1. 开发编码过程中,在开发过程中就不断对代码进行内存泄漏的检测 
2. 项目或者模块开发完成后,对应用进行整体的内存泄漏检测 
3. 在项目上线后,远程端检测项目是否存在内存泄漏的情况

一:开发编码过程中,检测内存泄漏

编码过程中可能导致的内存泄漏案例分析:

关于在编码中可能导致的泄漏案例以及编码注意事项,本文不做过多赘述,在给到的链接中,作者对泄漏案例描述的都比较详细,也比较全面,想了解的小伙伴可以【点击这里—>内存泄漏案例

1.检测工具:LeakCanary

首先最容日上手并且效果还不错,那就要属LeakCanary,效果也直观,具体的使用配置也很简单。

  1. 在项目的build.gradle中加入以下引用:
//    内存存泄漏检测
    debugCompile   'com.squareup.leakcanary:leakcanary-android:1.5'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
    testCompile    'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
  • 1
  • 2
  • 3
  • 4
  1. 在application中初始化LeakCanary,到此处配置完成
/**        Explain : 初始化内存泄漏检测
    * @author LiXaing 
    private void initLeakCanary() {
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return;
        }
        LeakCanary.install(this);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  1. 发生内存泄漏提示 
    666

当前就从网络上获取到了一张关于,在发生内存泄漏的时候,会在通知栏出现一个提示图标,当点击进去之后,就是现在展示的这张图片,会直观的展示内存泄漏的位置。

注意:==通过LeakCanary的使用,它可以为我们快速找到内存泄漏的位置,但并不能够提供我们内存泄漏的原因,有的时候,内存泄漏的位置是由于其他原因导致的,本人曾经就碰到过一次,由于Fragment未能被回收,从而导致了EventBus未能解绑(在onDestory中有解绑EventBus),导致的EventBus也存在内存泄漏,而导致的原因并非是没有解绑;所以内存泄漏的位置,并不一定就是导致泄漏的根本原因,所以后面及有可能还需要其他的工具进行辅助。==

2.检测工具:StrictMode

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

StrictMode的功能主要分为两大块:

一块是关于Thread,线程规范的监测,另一块是关于VM,内存的监测。在StrictMode下分别是ThreadPolicy和VMPloicy。

ThreadPolicy:用于监测线程部分,监测 主线程中是否访问网络、主线程中是否读写磁盘等。

<!--代码示例:在application中进行配置即可-->
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectAll()//监测所有内容
                .penaltyLog()//违规对log日志
                .penaltyDeath()//违规Crash
                .build());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

相关API:

API描述
detectAll 监测所以违规内容
permitAll 禁用所以监测内容
permit**…如:permitNetwork 关闭检测网络访问违规
detectNetwork 监测主线程中是否存在有访问网络
detectDiskReads()、detectDiskWrites() 监测是否在主线程中读写磁盘
penaltyDeath 一旦触发任何违规操作就直接Crash掉程序
penaltyDeathOnNetwork 一旦触发网络访问违规操作就Crash掉程序
penaltyDialog 一旦触发违规操作,就弹出违规信息对话框

当然,以上列出的是常用的API,也还有其他的一些API没有列出来,在开发中以上的API基本够用了,要是还有其他需求的小伙伴,就自行去查找一下吧。

VMPolicy:用于监测内存,可以监测Activity的内存泄漏,Fragment的内存泄漏(虽然内部没有指定Fragment可用的API但内部关于类对象的检测机制在此处就有着异曲同工之妙的作用),SQL内存泄漏,是否正常关闭读写流以及可以指定某个类的最大对象数目。

StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectAll()//监测所以内容
                .penaltyLog()//违规对log日志
                .penaltyDeath()//违规Crash
                .build());
  • 1
  • 2
  • 3
  • 4
  • 5
API描述
detectAll 监测所以违规内容
detectActivityLeaks 监测Activity内存泄漏的情况
detectLeakedClosableObjects() 和 detectLeakedSqlLiteObjects() 当使用的资源没有被正确关闭时会触发
detectLeakedRegistrationObjects 监测BroadcastReceiver 或者 ServiceConnection 注册类对象是否被正确释放
setClassInstanceLimit 设定某个类在内存中实例的上限
penaltyDialog 一旦触发违规操作,就弹出违规信息对话框

当然,以上列出的是常用的API,也还有其他的一些API没有列出来,在开发中以上的API基本够用了,要是还有其他需求的小伙伴,就自行去查找一下吧。

二:项目或者模块开发完成后,检测内存泄漏

在项目或者模块开发完成后,所需要做的就是各种测试,在交给测试人员之前,为了展现我们自身编码的高逼格和高质量,通常我们程序猿自己先会进行一部分测试,其中很重要的一部分测试就是本章内容讲的,内存泄漏的测试。

首先,我们可以使用android Studio中AndroidMonitor自带的一个工具—>memory,这个工具也可以说是非常的好用,先简单的介绍一下,memory虽然不可以分析出哪部分存在泄漏等情况,但可以很直观的看到内存的占用情况,看到内存的动态变化。

memory的使用
1. 我们打开AndroidMonitor,点击打开Memory,打开的界面,可能是这样的什么都没有。

这时我们需要需要注意两个点: 
2. 可能是没有选择相关的进程,点击下三角,选择我们需要查看相关的进行。

  1. 如果还是什么都没有,我们点击 “Tools->Android->Enable ADBIntergration(勾选)”

memory2

操作好以上步骤之后,我们就可以看到上面这张图片的状态,可以很直观的看到,应用在内存中占用的情况。

使用Memory,并不能够分析出具体的是哪一部分存在内存泄漏,主要是用来查看内存的占用的动态情况

关于左侧上方四个按钮功能和用法的描述:

功能描述
image Memory的开关
row 2 col 1(Initial GC) 用于手动GC,通常在抓取HPROF文件前,手动GC回收掉那些无用对象
image(Dump Java Heap) 用于生成HPROF文件,HPROF文件中的的数据是点击这个生成按钮这一瞬java VM执行时共享Heap中的数据,共享Heap中的数据主要包含了类的实例和数组对象,通常用于内存泄漏分析,在点击之后就能够看到Memory recorder在转动,在稍许延迟之后就会生成一个HPROF文件。
image(Starg Allocation Tracking) 用于动态追踪内存情况,可以记录下一段时间区间内各个线程中各个方法在内存中的分配情况,使用方式:点击Starg Allocation Tracking按钮,开始分配追踪,可以看到Memroy recorder在转动,在合适的时间再点击一下就完成了对一时间区间内内存动态的记录,在稍许延迟之后就会生成一个.alloc文件

Dump Java Heap生成的.HPROF文件

在我们点击 Dump Java Heap之后,就会生成一个.alloc文件,这个文件是分析内存泄漏非常重要的一部分。

image

当打开HPROF文件的时候是这样的,什么都没有,为了方便查找,我们点击”Class List View”选择–>”Package Tree View”当前展示就是变为包的视图树。

image

那么到了这一部分,我们应该查看哪儿是存在泄漏的呢?分为两种情况:

1. 其实如果到了这一步,基本上可以断定是存在内存泄漏了的,通过之前的LeakCanary、StrictMode已经可以判断出内存泄漏了,而这部分是为了通过LeakCanary、StrictMode给出的信息更确切的确定内存泄漏的部分在内存中运作的情况和更详细的数据。

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

给出的上图,是Fragmnet反复打开关闭产生实例的泄漏,实际情况中只应该存在一个,而此处有14个,有人会问,那你怎么就知道就要查找这一个Fragmnet实例呢?在此,需要说明的一点是,不管是通过上面所说的第一种情况还是第二种,都可以排查到这类泄漏的;要问:那要我没有排查到呢?那只有说:眼瞎,就先去医院治病~

在给出的上图中,存在多个Fragment内存泄漏,我们就关注图中标注好的“1”部分,我们可以通过Total Count(总实例数)和Heap Count(堆内存中实例数)可以看到有14个,点击这个类,然后在旁边的Instance框中我们可以看到这14个具体的实例,也就是图中标注的“2”部分,我们点击第一个;然后在下方的Reference Tree中我们可以看到当前这个实例对象持有的具体的对象,那么在这一部分我们怎么排查呢?我们主要需要关注的是 Dominating Size(当前指向的这个一条,在内存中占有的大小)值最大的前面几条,为什么呢?因为泄漏导致内存无法被释放值越大,存在泄漏的可能性越大。 所以看上图中的“3”部分,我们针对这一条不断的向下展开引用,在“4”部分,我们可以很清晰的看到是由EventBus导致的内存泄漏,而在我给出的这个示例里,也就是由于Fragment在销毁时未EventBus导致的。

另外值得一说的标注的“5”,在Analyzer中有一个功能就是 Detect Leaked Activities,点击绿色三角按钮运行后,可以帮我们分析出当前可能存在泄漏的Activity对象。

到目前为止,我们通过HPROF文件在Studio中的使用可以完成内存泄漏的分析,不过还有更为强大的一款工具,更方便我们使用就是下要说的MAT

MAT(Memory Analyzer Tool)工具的使用

【还没有这个工具的小伙伴可以戳这里–>Eclipse Memory Analyzer Open Source Project

首先我们需要打开在MAT中打开.hprof文件,在打开MAT工具后,点击右上角的 “File”-> “Open Heap Dump”,然后选择需要打开的.hprof文件,在打开的过程中可能会遇到这样的问题:

image

关于这个问题,有两种方式解决,比较建议的是第一种,第一种更方便,更容易操作:

一:

image

  1. 在Android Studio工具的右侧,点击上图红框标注的“1” CapTures;

  2. 选择红框标注的“2”Heap SanPshot选择需要导出的.hprof;

  3. 选择好需要导出的.hprof文件后,单机右键选择“Export to standard”(导出标准的.hprof文件); 
    4.然后选择导出保存到的位置,就可以了。

二: 
Windows操作系统下,打开CMD黑窗口(win键+R键,输入cmd)

image

按照上图的操作写入输入输入路径,然后回车,就可以完成转换了。

接下来就是关于在MAT工具中的操作,MAT的功能很多,但就不一一介绍了,就简单说明关于在检测内存泄漏用到的部分。

image

选择上图红框的“Histogram”,然后我们可以看到下图界面

image

在上图中,我们看到的是一系列的数组对象和类的实例等数据,我们可以通过上图红框中的部分,输入需要查找的路径或者类的类名等进行过滤

image

在这一部分中,所需要观察的点,就是Objects列,是存在的实例对象数,我们选择好了需要排查的实例对象点击右键,选择“merge shortest Path to GC Roots” -> “exclude all phantom/weak/soft etc. references”,而为什么要进行这一步过滤呢,因为导致内存泄漏的只会是硬引用,所以我们可以把虚引用,弱引用,软引用全部进行过滤掉,可以减少误判。

image

在进行过滤之后,我们可以看到唯一持有的硬引用只有EventBus,那么这时,我们回到项目中去具体观察EventBus部分的代码就好了;而这,只是在实际情况中的一种,另外在过滤掉那三种引用之后,还存在有多种引用的情况,这个时候,需要观察的点就是“Objcets”(对象数量)和Showll Heap(在堆内存中占据的内存大小)数量越多和占据内存越高的,存在泄露的可能性越大,因为反复创建却不能够被回收,数量和占据的内存越高。

原文地址:https://www.cnblogs.com/xgjblog/p/9134594.html