做了较长时间的Android开发了,发现其实android应用开发入门容易,但是进阶或者成为高级工程师,需要具备的基础能力还是非常高的:性能优化、内存泄露、apk瘦身、热修复等等,这些都非常的考验一个人的能力。android成长之路还很长,自己会持续的走下去。本文主要介绍android内存泄露方面的知识。其实要真的理解内存泄露,需要对JVM、Java语言有一定的了解,在这个基础上就比较容易理解本文了。
一.内存泄露概念
在java中,如果一个对象没有可用价值了,但又被其他引用所指向,那么这个对象对于gc来说就不是一个垃圾, 所以不会对其进行回收,但是我们认为这应该是个垃圾,应该被gc回收的。这个对象得不到gc的回收, 就会一直存活在堆内存中,占用内存,就跟我们说的霸着茅坑不拉屎的道理是一样的。这样就导致了内存的泄露。
为什么会内存泄露呢,根本原因就是一个永远不会被使用的对象,因为一些引用没有断开,没有满足GC条件,导致不会被回收,这就造成了内存泄露。比如在Activity中注册了一个广播接收器,但是在页面关闭的时候进行unRegister,就会出现内存溢出的现象。如果我们的java运行很久,而这种内存泄露不断的发生,最后就没内存可用了,最终就是我们看到的OOM错误。虽然android的内存泄露做到了应用程序级别的泄露(android中的每个应用程序都是独立运行在单独进程中的,每个应用进程都由虚拟机指定了一个内存上限值,一旦内存占用值超过这个上限值,就会发生oom错误,进程被强制kill掉,kill掉的进程内存会被系统回收),但是对于一名开发工程师,绝对不能放过任何的内存泄露。
二.出现内存泄露的场合及解决方案
1.activity中handler间断的post消息,会造成activity泄露
1.1出现场合:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
1.2原因分析:
当这个Activity被finished后,延时发送的消息会继续在主线程的消息队列中存活10分钟,直到他们被处理。这个消息持有这个Activity的Handler引用,这个Handler有隐式地持有他的外部类(在这个例子中是SampleActivity)。直到消息被处理前,这个引用都不会被释放。因此Activity不会被垃圾回收机制回收,泄露他所持有的应用程序资源。注意,第15行的匿名Runnable类也一样。匿名类的非静态实例持有一个隐式的外部类引用,因此context将被泄露。
具体的引用顺序:
MessageQueue->Message->Runnable->Handler->Activity。所以这就导致当前activity与MessageQueue一直有关联,导致LeakActivity的对象不能被gc回收,从而导致内存泄露。
1.3解决方案:
为了解决这个问题,Handler的子类应该定义在一个新文件中或使用静态内部类。静态内部类不会隐式持有外部类的引用。所以不会导致它的Activity泄露。如果你需要在Handle内部调用外部Activity的方法,那么让Handler持有一个Activity的弱引用(WeakReference)以便你不会意外导致context泄露。为了解决我们实例化匿名Runnable类可能导致的内存泄露,我们将用一个静态变量来引用他(因为匿名类的静态实例不会隐式持有他们外部类的引用)。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
不在一个Activity中使用非静态内部类, 以防它的生命周期比Activity长。相反,尽量使用持有Activity弱引用的静态内部类。
或者:
我们也可以这样做,在activity的onDestroy方法中干掉handler的所有callback和message:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
2.非静态匿名内部类造成内存泄露
在activity中开启的线程也是一样,如果activity结束了而线程还在跑,一样会导致activity内存泄露,因为”非静态内部类对象都会持有一个外部类对象的引用”,你创建的线程就是activity中的一个内部类,持有activity对象的引用,当activity结束了,但线程还在跑,就会导致activity内存泄露。
2.1出现场合:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
2.2原因分析:
非静态的内部类会持有外部类的一个隐式引用,有需要的朋友可以参考下。
只要非静态的匿名类对象没有被回收,MainActivity就不会被回收,MainActivity所关联的资源和视图都不会被回收,发生比较严重的内存泄漏。
2.3解决方案:
要解决MainActivity的内存泄漏问题,只需把非静态的Thread匿名类定义成静态的内部类就行了(静态的内部类不会持有外部类的一个隐式引用)。
3.Thread造成的内存泄露
3.1出现场合:具体的场合可见2.
3.2原因分析:
2中,一旦一个新的Activity创建,那么就有一个Thread永远得不到清理回收,发生内存泄漏。Threads在Java中是GC roots;意味着Dalvik。Virtual Machine (DVM)会为所有活跃的threads在运行时系统中保持一个硬引用,这会导致threads一直处于运行状态,垃圾收集器将永远不可能回收它。
3.3解决方案:
出于这个原因,我们应当为threads添加一个结束的逻辑,如下代码所示:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
在上述的代码中,当Activity结束销毁时在onDestroy()方法中结束了新创建的线程,保证了thread不会发生泄漏。
4.单例+依赖注入
4.1出现场合:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
4.2原因分析:
TestManager——>listener——>activity,TestManager是单例。
因为TestManager中有static对象,static跟类的生命周期是一样的,类一加载,static就加载了,类一被销毁,static才会跟着销毁(static是存在方法区),这时候jvm会在方法区中存储变量INSTANCE,然后在堆内存开辟空间存放INSTANCE对象,然后把地址值付给INSTANCE变量,使INSTANCE变量就指向这个对象(类似C语言的指针),activity类也是这样的一种执行关系。
因为这是一个单例,当app进程被干掉的时候,堆内存中的INSTANCE对象才会被释放,所以INSTANCE对象的生命周期是很长的,LeakActivity1中,listener持有当前activity的对象,然后testManager.registerListener(listener);执行完,TestManager中的listener就持有activity中listener的对象,而TestManager中的INSTANCE是static的,生命周期长,activity销毁的时候INSTANCE依然还在,INSTANCE还在,那么TestManager类中的全局变量也还是存在的,所以TestManager中的listener变量还在,还一直持有LeakActivity1中的listener对象引用,所以最终是INSTANCE导致LeakActivity1内存泄露。
4.3解决方案:
所以,要解决这个问题,可以这样做,在activity的onDestroy方法中注销注册的listener.
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
这样做后TestManager中的listener不再持有LeakActivity1中的listener对象引用,所以LeakActivity1被销毁后listener对象也可被回收了。最终,问题又解决了,当然你也可以直接把INSTANCE置null。
三.总结
其实对于内存泄露的分析,用逆向思维来分析更好,从根源一直分析到无法被回收的对象。
出现内存泄露的主要原因是生命周期的不一致造成的:在Android中,长时间运行的任务和Acyivity生命周期进行协调会有点困难,如果你不加以小心的话会导致内存泄漏。
内存泄漏的主要原因在于一个生命周期长的东西间接引用了一个生命周期短的东西,会造成生命周期短的东西无法被回收。反过来,如果是一个生命周期短的东西引用了一个生命周期长的东西,是不会影响生命周期短的东西被回收的。
对象都是有生命周期的,对象的生命周期有的是进程级别的,有的是Activity所在的生命周期,随Activity消亡;有的是Service所在的生命周期,随Service消亡。很多情况下判断对象是否合理存在的一个很重要的理由就是它实际的生命周期是否符合它本来的生命周期。很多Memory Leak的发生,很大程度上都是生命周期的错配,本来在随Activity销毁的对象变成了进程级别的对象,Memory Leak就无法避免了。
四.避免内存泄露的一些技巧
- 使用静态内部类/匿名类,不要使用非静态内部类/匿名类.非静态内部类/匿名类会隐式的持有外部类的引用,外部类就有可能发生泄漏。而静态内部类/匿名类不会隐式的持有外部类引用,外部类会以正常的方式回收,如果你想在静态内部类/匿名类中使用外部类的属性或方法时,可以显示的持有一个弱引用。
- 不要以为Java永远会帮你清理回收正在运行的threads.在上面的代码中,我们很容易误以为当Activity结束销毁时会帮我们把正在运行的thread也结束回收掉,但事情永远不是这样的!Java threads会一直存在,只有当线程运行完成或被杀死掉,线程才会被回收。所以我们应该养成为thread设置退出逻辑条件的习惯。
- 适当的考虑下是否应该使用线程.Android应用框架设计了许多的类来简化执行后台任务,我们可以使用与Activity生命周期相关联的Loaders来执行简短的后台查询任务。如果一个线程不依赖与Activity,我们还可以使用Service来执行后台任务,然后用BroadcastReceiver来向Activity报告结果。另外需要注意的是本文讨论的thread同样使用于AsyncTasks,AsyncTask同样也是由线程来实现,只不过使用了Java5.0新增并发包中的功能,但同时需要注意的是根据官方文档所说,AsyncTask适用于执行一些简短的后台任务。
- 频繁的使用static关键字修饰
很多初学者非常喜欢用static类static变量,声明赋值调用都简单方便。由于static声明变量的生命周期其实是和APP的生命周期一样的(进程级别)。大量的使用的话,就会占据内存空间不释放,积少成多也会造成内存的不断开销,直至挂掉。static的合理使用一般用来修饰基本数据类型或者轻量级对象,尽量避免修复集合或者大对象,常用作修饰全局配置项、工具类方法、内部类。 - BitMap隐患
Bitmap的不当处理极可能造成OOM,绝大多数情况应用程序OOM都是因这个原因出现的。Bitamp位图是Android中当之无愧的胖子,所以在操作的时候必须小心。
及时释放recycle。由于Dalivk并不会主动的去回收,需要开发者在Bitmap不被使用的时候recycle掉。
设置一定的压缩率。需求允许的话,应该去对BItmap进行一定的缩放,通过BitmapFactory.Options的inSampleSize属性进行控制。如果仅仅只想获得Bitmap的属性,其实并不需要根据BItmap的像素去分配内存,只需在解析读取Bmp的时候使用BitmapFactory.Options的inJustDecodeBounds属性。
最后建议大家在加载网络图片的时候,使用软引用或者弱引用并进行本地缓存,推荐使用android-universal-imageloader或者xUtils。 - 页面背景图
在布局和代码中设置背景和图片的时候,如果是纯色,尽量使用color;如果是规则图形,尽量使用shape画图;如果稍微复杂点,可以使用9patch图;如果不能使用9patch的情况下,针对几种主流分辨率的机型进行切图。 - 引用地狱
Activity中生成的对象原则上是应该在Activity生命周期结束之后就释放的。Activity对象本身也是,所以应该尽量避免有appliction进程级别的对象来引用Activity级别的对象,如果有的话也应该在Activity结束的时候解引用。如不应用applicationContext在Activity中获取资源。Service也一样。
有的时候我们也会为了程序的效率性能把本来是Activity级里才用的资源提升到进程级别,比如ImageCache,或者其它DataManager等。
我只能说,空间和时间是相对的,有的时候需要牺牲时间换取空间,有的时候需要牺牲空间换取时间。内存是空间的存在,性能是时间的存在。完美的程序是在一定条件下的完美。 - BroadCastReceiver、Service 解绑
绑定广播和服务,一定要记得在不需要的时候给解绑。 - handler 清理
在Activity的onDestroy方法中调用
handler.removeCallbacksAndMessages(null);
取消所有的消息的处理,包括待处理的消息; - Cursor及时关闭
在查询SQLite数据库时,会返回一个Cursor,当查询完毕后,及时关闭,这样就可以把查询的结果集及时给回收掉。 - I/O流
I/O流操作完毕,读写结束,记得关闭。 - 线程
线程不再需要继续执行的时候要记得及时关闭,开启线程数量不易过多,一般和自己机器内核数一样最好,推荐开启线程的时候,使用线程池。线程生命周期要跟activity同步。 - String/StringBuffer
当有较多的字符创需要拼接的时候,推荐使用StringBuffer。 - 网络请求也是线程操作的,也应该与activity生命周期同步,在onDestroy的时候cancle掉请求。
- 尽量使用application代替activity和context: Context context = activity.getApplication();这样就使得context不是指向Activity了,指向全局的application,这样就没内存泄露可说了。
五.android中内存泄露检测工具
MAT
LeakCanary
六.一些问题的解答
1.内部类如何不影响外部类的回收呢?
非静态的内部类会持有外部类的一个隐式引用。
在Java里面,非静态的匿名内部类保持了一个对outer class的引用。如果你不够小心,持有了这个引用,那么将导致这个Activity一直保留(本应该被垃圾回收器回收)。Activity对象保持了对整个View结构和所有这些Resources的引用,所以你LeakActivity,也就Leak了很多的内存。
2.activity onbestrory的时候并没有被垃圾回收掉
不要认为Java会为你清理runningthread。在上面的例子中,我们会很容易的就想象当用户离开这个Activity的时候,Activity实例会被垃圾回收器回收,所有在这个Activity中开启的running thread也会被清理掉。事实上不是这样的。Java线程将会一直存在,直到他们被显示的关闭或者处理结束,或者被杀掉整个进程。所以,你需要完成可被取消的线程机制,在Activity的生命周期的某个地方做适当的处理。