引言
内存溢出与内存泄漏问题已经是一个老生常谈的话题了,本文并不是打算谈什么是内存溢出或者内存泄漏,而是打算结合这两年来安卓开发过程中遇到的一些问题进行总结,看看我在一路上遇到的内存泄漏,不得不说,从走上工作岗位后,现在作为开发人员的我的技术相对于在学校里面作为非计算机专业学生的我,的确提高了很多,所以干一行,爱一行,当初选择编程,是因为觉得这和自己喜欢思考和喜欢专研的性格相关,好了,谁关心你呢,逼逼啥呢,赶快进入正题吧。
虽然不讨论,还是先写一下内存泄漏和内存溢出的定义吧,我自己的白话文版,凑点字数:
内存泄漏:泄漏泄漏,就是只是船破了个口子,但是大船还没沉,开发过程中,给对象分配了内存,但是不用了的条件下,忘记去释放,在Java里面体现为这个对象的强引用仍然被持有,于是乎,就算是具有自动回收机制的JVM也傻眼了,好吧,这就是内存泄漏,记住,是漏,不一定会沉船。
内存溢出:千里之堤,毁于蚁穴,当然还有个英文版,A small leak sink a giant ship,Anyway,都是讲的一个意思,既然船有了小口子,那么慢慢的,沉船只是时间问题,并且,如果我们的程序中,有很多的这种小口子,那么沉的速度就更快了,所以,在我们的安卓编程过程中,如果不去注意的话,OutOfMemoryException应该就很快出现了。
再来个稍微我认为解释的较好的,Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收。也就是说,一个对象不被任何引用所指向,则该对象会在被GC发现的时候被回收;另外,如果一组对象中只包含互相的引用,而没有来自它们外部的引用(例如有两个对象A和B互相持有引用,但没有任何外部对象持有指向A或B的引用),这仍然属于不可到达,同样会被GC回收。
方法论
常见内存分析方法
OK,你说泄漏就泄漏,你说溢出就溢出啊,有什么证据说我溢出啦,没证据你可别诬陷我啊!这句话熟悉吧,好吧,问题来了的时候,同事间推脱常常就会发生,那么,就跟git blame一样,咱们来追责吧,只是这次并不是看谁最后提交,是去检测我们的安卓页面到底有没有泄漏。
安卓开发里面,大概有这么些方法都可以去检测内存泄漏:
1). MAT好像用的人很多,但是大多都是由Eclipse切换过来的老手们在用,当然我也用过哈~.
2). 然后么就是裸奔党,其实我也是一个,如果系统自身就有,而且还凑合可以用的话,干嘛还要去用别的呢,嗯,照这个思路,那我猜你一定会喜欢Android Studio自带的Dump heap功能,里面可以通过在你出问题的时候,点击Dump heap按钮,帮你分析堆内存,而且很体贴的,有一个专门的检测Activity内存泄漏的选项,勾选上,那么如果出现Activity内存泄漏,可以看得到迹象。
3). LeakCanary,好吧,个人是Jake Wharton的粉丝,当然是它们Square那群人做的产品的粉丝,所以,对于它们的产品,怎么能够不用呢,最关键的是,它们的LeakCanary使用起来简单,分析后结果的展示就在App上,而且该App设计的还不错,好吧,就是它了,本文就基于LeakCanary,分析一下常见的内存溢出场景,并且反思以前自己在写页面上遇到的内存问题的原因,还有Fragment中拿到getActivity()问题。
LeakCanary设置
当然,首先要引入LeakCanary这个库,在build.gradle中加入:
我们只想在debug阶段去判断内存泄漏,线上的并没有这个打算,所以进行了上面的设置,然后,我们需要在自定义的Application中去初始化,初始化的代码也异常的简单,假设我们的Application的类名为LeakCanaryApplication,别忘了到Manifest中去声明,然后我们的配置就需要如下代码:
OK,everything is done,进行如上配置,我们就能够得到在LeakCanaryApplication下的每个Activity的内存泄漏跟踪结果了。在debug build中,如果检测到某个 activity 有内存泄露,LeakCanary 就是自动地显示一个通知,点击这个通知,就会查看到泄漏的整个LeakTree,帮我们定位到泄漏的地方。
以上的代码还是需要解释一下:
使用RefWatcher是为了监控那些本该被回收的对象。LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个ActivityRefWatcher,用于自动监控调用Activity.onDestroy()之后泄露的activity,但是,其实我们能做的,当然不止这些,比如我还想监控某个Fragment,那么如下去定义:
这样,我们每个继承与BaseFragment的Framgent的内存泄漏跟踪结果就可以得到分析。
来看看工作机制:
RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
然后在后台线程检查引用是否被清除,如果没有,调用GC。
如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。
在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。
得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。
HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。
好了,LeakCanary就讲到这吧,开始我们的泄漏场景分析之旅吧。
安卓内存溢出常见场景
在安卓平台,泄漏 Context 对象问题尤其严重。这是因为像 Activity 这样的 Context 对象会引用大量很占用内存的对象,例如 View 层级,以及其他的资源。如果 Context 对象发生了内存泄漏,那它引用的所有对象都被泄漏了。安卓设备大多内存有限,如果发生了大量这样的内存泄漏,那内存将很快耗尽。
如果一个对象的合理生命周期没有清晰的定义,那判断逻辑上的内存泄漏将是一个见仁见智的问题。幸运的是,activity 有清晰的生命周期定义,使得我们可以很明确地判断 activity 对象是否被内存泄漏。onDestroy() 函数将在 activity 被销毁时调用,无论是程序员主动销毁 activity,还是系统为了回收内存而将其销毁。如果 onDestroy 执行完毕之后,activity 对象仍被 heap root 强引用,那垃圾回收器就无法将其回收。所以我们可以把生命周期结束之后仍被引用的 activity 定义为被泄漏的 activity。Activity 是非常重量级的对象,所以我们应该极力避免妨碍系统对其进行回收。然而有多种方式会让我们无意间就泄露了 activity 对象。
静态Activity(最傻的)
之所以认为最傻,是因为这是最明显的错误,使用这种方法的人肯定对安卓开发不熟悉,当然也不排除老手有特殊需求自己控制的场景。
如果Activity中定义了一个static的变量,并且将其指向一个Activitiy实例,如果Activity走到生命周期末尾并准备销毁自己的时候,没有清楚这个引用的话,就会导致该Activity对象无法得到回收。由于这个对象是静态的,那么声明这个静态对象的类一旦加载,就会在App中一直常驻内存,如果该类不卸载,静态成员就得不到回收:
好了,以上代码仅做示范,一般情况应该没人会这么去写。
静态的View
写这种代码的场景也不多,发生内存泄漏的原因其本质和上一个原因是差不多的,由于构建View的时候,View是需要持有Context的强引用的,也就是对应的Activity,于我们通过一个静态成员引用了这个 view,所以我们也就引用了 activity,因此 activity 就发生了泄漏。所以一定不要把加载的 view 赋值给静态变量,如果你真的需要,那一定要确保在 activity 销毁之前将其从 view 层级中移除。
内部类
写一个内部类的原因也有很多,可以增加代码的封装度和可读性,比如我的一些定义一些只属于某个Activity下的辅助类,那么使用内部类最适合不多了。但是如果我们在Activity的内部生命了一个静态的内部类实例,由于内部类会间接持有外部类的引用,那么如果这个静态的内部类的实例没有清楚或销毁,会导致Activity的强引用一直被持有,那么无法回收。
Handler的不恰当使用
这个内存泄漏的发生是有概率的,如果我们在程序中定义的Handler是一个非静态的,那么会得到IDE的一个提示,This Handler class should be static or leaks might occur,相信大家一定都见过这句话,其实,这是Android Studio开发团队在提醒大家应当注意Handler的这种不正确的定义可能会引发内存泄漏。
当一个Android应用启动的时候,会自动创建一个供应用主线程使用的Looper实例。Looper的主要工作就是一个一个处理消息队列中的消息对象。在Android中,所有Android框架的事件(比如Activity的生命周期方法调用和按钮点击等)都是放入到消息中,然后加入到Looper要处理的消息队列中,由Looper负责一条一条地进行处理。主线程中的Looper生命周期和当前应用一样长。
当一个Handler在主线程进行了初始化之后,我们发送一个target为这个Handler的消息到Looper处理的消息队列时,实际上已经发送的消息已经包含了一个Handler实例的引用,只有这样Looper在处理到这条消息时才可以调用Handler#handleMessage(Message)完成消息的正确处理。
在Java中,非静态的内部类和匿名内部类都会隐式地持有其外部类的引用。静态的内部类不会持有外部类的引用。
来分析一段典型的代码:
分析一下上面的代码,当我们执行了Activity的finish方法,被延迟的消息会在被处理之前存在于主线程消息队列中10分钟,而这个消息中又包含了Handler的引用,而Handler是一个匿名内部类的实例,其持有外面的LeakActivity的引用,所以这导致了LeakActivity无法回收,进行导致LeakActivity持有的很多资源都无法回收,这就是我们常说的内存泄露。
注意上面的new Runnable这里也是匿名内部类实现的,同样也会持有SampleActivity的引用,也会阻止SampleActivity被回收。在这里提出了这个问题,那么解决办法就放在后面吧。
Thread的不恰当使用
来看一下下面这段代码:
以上代码中,线程被初始化为线程内部类,使得每一个线程都持有一个外部Activity实例的隐式引用,由于线程是在不断的循环使用中,这样就使得Activity不会被Java的垃圾回收机制回收,最重导致内存泄漏。
TimerTask的不恰当使用
|
|
只要它们是通过匿名类创建的,尽管它们在单独的线程被执行,它们也会持有对 activity 的强引用,进而导致内存泄漏。
系统服务Manager注册接口不及时解注册
系统的很多服务可以通过context.getSystemService 获取,它们负责执行某些后台任务,或者为硬件访问提供接口。其生命周期可以比调用的Activity长,如果 context 对象想要在服务内部的事件发生时被通知,那就需要把自己注册到服务的监听器中。然而,这会让服务持有 activity 的引用,如果程序员忘记在 activity 销毁时取消注册,那就会导致 activity 泄漏了。
见招拆招
好了,上面列举了我们在开发过程中常见的可能会导致内存泄漏的场景,那么我们来各个击破吧。
----未完待续----