Android性能优化(内存泄露第一篇)
首先我们关注一个内存泄露的场景,相信大家都知道在Android中非静态的内部类或匿名内部类都很有可能造成Context泄露。主要原因就是在某些情况下,Context的生命周期已经走完,但是这些类的生命还未到尽头,而他们又持有Context的引用,导致GC时无法回收该回收的内存空间从而导致类存泄露。
上面这段话应该不难理解,下面就用一些简单的例子说明这个问题。
一、普通内部类或匿名类造成内存泄露
|
|
上面的代码中,有一个匿名的Runnable类让其所在线程sleep 100秒,在这个Activity中有一个ImageView并为其设置了一张图片。我们连续的进行打开->关闭Activity这项操作,发现越到后面卡顿越严重。看下面两张图,这是某两个时刻的内存使用情况(一前一后):
可以发现,在连续进行上述同一操作的时候,程序内存增大了很多!再看看Dalvikvm(4.4以上系统可能是ART)打印的日志:
GC操作显示当前活动对象占用的内存越来越多,最后直至程序崩溃!这里可以肯定,我们上面写的代码确实造成了内存泄露。就是这个匿名内部类,它持有外部Activity的引用,当我们点击Button开启了线程的同时结束了当前Activvity,此时GC正要回收此Activity占用的内存空间,发现还有对象持有它的引用所以无法进行内存回收;当我们多次进行打开->关闭Activity操作的时候,就导致了内存泄露,最后程序也崩了。
问题来了,如何避免。其实这里相信大家都知道,将其声明为静态的就行,如下:
|
|
使用
|
|
修改后Dalvikvm打印日志如下图:
程序的内存不在一直飙升,而是稳定在一个范围内。这里的主要原因就在于内部类和静态内部类的区别:
- 静态内部类不同于普通内部类,它不会持有外部类的引用;而普通内部类或匿名类则相反
- 普通内部类或匿名类因为持有外部类的引用,所以可以访问外部类的资源属性成员变量等;静态内部类不行
- 因为普通内部类或匿名类依赖外部类,所以必须先创建外部类,再创建普通内部类或匿名类;而静态内部类随时都可以在其他外部类中随时创建
所以上面的代码中,由于使用的是静态内部类,当外部类Activity需要被GC回收内存时,Activity的引用数为0,所以能被正常回收。
二、Handler造成Context泄露
先看代码:
|
|
当我们写下这段代码的时候,IDE会提示一个警告如下:
提示Handler类应该是静态的,否则可能会发生泄露。
其实这里发生泄露和上面说的普通/匿名内部类是类似的。根据Android的消息机制,每个Message对象都保存着处理其Handler的引用,而在Activity中实例化一个非静态的Handler类,此类又会持有Activity的引用;当消息没处理完或者需要延迟处理就结束了当前Activity,此时Activity引用数不为0,就会造成Context泄露。问题就是这样,对策是不是也同样出来了,将Handler类声明为静态内部类,代码如下:
|
|
警告确实没有了,但是问题又来了。一般情况下,我们使用Handler就是为了配合Thread进行耗时操作然后更新UI,但是这里的Handler类是静态内部类,不能访问外部类的成员变量,怎么破!接下来,就该 WeakReference 派上用场了!
Google对WeakReference介绍不多,下面是官方文档中的介绍(以下"入队"指将该引用加入引用队列(Reference Queen)):
弱引用(WeakReference)
弱引用(WeakReference)是三种引用中间的一种。一旦GC判定一个对象时弱引用可到达,会发生以下情况:
有一组引用ref,这组引用包含以下元素:
- 指向该对象的所有弱引用
- 所有弱引用指向的软引用/强引用可到达对象
所有在这组ref中的引用会被自动清除
所以之前被ref引用的对象都可以被析构(回收)
在未来的某个时候,ref中所有的引用会根据自己的相应的引用队列(如果有)入队
弱引用在Map中很有用,如果一个弱引用没有被外部任何地方引用,它就会自动被移除。
SoftReference
和 WeakReference
的区别就在于对象被回收、引用入队的时间点不同:
- 如果一个对象是软引用可到达,那么这个对象会尽可能晚的被回收,这个引用同样会尽可能晚的入队。比如当VM内存不足时这种情形。
- 如果一个对象被判定是弱引用可到达,那么这个对象会尽快被回收,这个引用也会尽快入队。
- 弱引用不能阻挡GC对对象进行回收,由GC决定引用的对象何时回收并且将对象从内存移除
- 使用get()方法获取其引用的对象
介绍完了弱引用,看看我们修改后的代码:
|
|
这样我们就可以在静态内部类中使用操作Activity。
除了弱引用(WeakReference)和上面稍微提到的软引用(SoftReference),还有强引用(StrongReference)和虚引用 (PhantomReference)。
软引用(SoftReference)
一旦GC判定一个对象时弱引用可到达,会发生以下情况:
- 有一组引用ref,这组引用包含以下元素
- 指向该对象的所有弱引用
- 所有软引用指向的强引用可到达的对象
- 所有在这组ref中的引用会被自动清除
- 在同一时间或是未来的某一时间,ref中所有的引用会根据自己的相应的引用队列(如果有)入队
- 系统会延迟清除软引用指向的对象,该软引用也会延迟入队,但是再系统抛出 OutOfMemoryError 异常的时候所有的软引用可到达的对象会被回收。当系统需要回收内存来满足分配,软引用可到达的对象会才会被回收,软引用入队。简单来说就是软引用阻止GC回收其指向的对象的能力相对弱引用强。
软引用上面说到了当内存不足时才会回收这些软引用指向的对象,所以挺适合做缓存用。但是Google可不推荐这么做,因为很多原因限制了它灵活的处理缓存相关的事情。所以关于SoftReference官方文档提到这样一句:
Most applications should use an
android.util.LruCache
instead of soft references. LruCache has an effective eviction policy and lets the user tune how much memory is allotted.
所以要做缓存还是得用LruCache。
强引用(StrongReference)
我们使用的最多的就是强引用,比如一句简单的赋值代码:
|
|
创建一个Button对象,并将这个对象的引用存到button中。
虚引用 (PhantomReference)
虚引用是几类引用中最弱的一种,当一个对象被判定是虚引用可到达时,该引用就会被加入到引用队列(也就是当一个对象被回收之后),但是它的指向不会被清除。虚引用适合在一个对象回收前做一些清理操作,因为它比 finalize()
方法更灵活。
关于Java中的弱引用,这篇文章(译文)关于WeakReference写的很好,推荐。