Android性能优化(内存泄露第一篇)

首先我们关注一个内存泄露的场景,相信大家都知道在Android中非静态的内部类或匿名内部类都很有可能造成Context泄露。主要原因就是在某些情况下,Context的生命周期已经走完,但是这些类的生命还未到尽头,而他们又持有Context的引用,导致GC时无法回收该回收的内存空间从而导致类存泄露。

上面这段话应该不难理解,下面就用一些简单的例子说明这个问题。

一、普通内部类或匿名类造成内存泄露

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
public class SecondActivity extends Activity {
private static final String TAG = "WeakReferenceTest";
private ImageView ivTest;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_2);
ivTest = (ImageView) findViewById(R.id.image);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
ivTest.setImageBitmap(bitmap);
// 匿名内部类会持有外部类的引用
final Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000 * 100);
Log.i(TAG, "This log is from SecondActivity!");
}catch (InterruptedException e){
}
}
});
Button button = (Button) findViewById(R.id.btn_2);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
thread.start();
finish();
}
});
}
}

上面的代码中,有一个匿名的Runnable类让其所在线程sleep 100秒,在这个Activity中有一个ImageView并为其设置了一张图片。我们连续的进行打开->关闭Activity这项操作,发现越到后面卡顿越严重。看下面两张图,这是某两个时刻的内存使用情况(一前一后):

可以发现,在连续进行上述同一操作的时候,程序内存增大了很多!再看看Dalvikvm(4.4以上系统可能是ART)打印的日志:

GC操作显示当前活动对象占用的内存越来越多,最后直至程序崩溃!这里可以肯定,我们上面写的代码确实造成了内存泄露。就是这个匿名内部类,它持有外部Activity的引用,当我们点击Button开启了线程的同时结束了当前Activvity,此时GC正要回收此Activity占用的内存空间,发现还有对象持有它的引用所以无法进行内存回收;当我们多次进行打开->关闭Activity操作的时候,就导致了内存泄露,最后程序也崩了。

问题来了,如何避免。其实这里相信大家都知道,将其声明为静态的就行,如下:

1
2
3
4
5
6
7
8
9
10
11
12
private static class MyRunnable implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000 * 100);
Log.i(TAG, "This log is from SecondActivity!");
}catch (InterruptedException e){
}
}
}

使用

1
2
3
4
5
6
7
8
9
10
final Thread thread = new Thread(new MyRunnable());
Button button = (Button) findViewById(R.id.btn_2);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
thread.start();
finish();
}
});

修改后Dalvikvm打印日志如下图:

程序的内存不在一直飙升,而是稳定在一个范围内。这里的主要原因就在于内部类和静态内部类的区别:

  • 静态内部类不同于普通内部类,它不会持有外部类的引用;而普通内部类或匿名类则相反
  • 普通内部类或匿名类因为持有外部类的引用,所以可以访问外部类的资源属性成员变量等;静态内部类不行
  • 因为普通内部类或匿名类依赖外部类,所以必须先创建外部类,再创建普通内部类或匿名类;而静态内部类随时都可以在其他外部类中随时创建

所以上面的代码中,由于使用的是静态内部类,当外部类Activity需要被GC回收内存时,Activity的引用数为0,所以能被正常回收。

二、Handler造成Context泄露

先看代码:

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
public class SecondActivity extends Activity {
private static final String TAG = "WeakReferenceTest";
private ImageView ivTest;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Log.i(TAG, msg.obj.toString());
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_2);
ivTest = (ImageView) findViewById(R.id.image);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
ivTest.setImageBitmap(bitmap);
Message msg = mHandler.obtainMessage();
msg.obj = "This is a message!";
mHandler.sendMessageDelayed(msg, 1000 * 10);
finish();
}
}

当我们写下这段代码的时候,IDE会提示一个警告如下:

提示Handler类应该是静态的,否则可能会发生泄露。

其实这里发生泄露和上面说的普通/匿名内部类是类似的。根据Android的消息机制,每个Message对象都保存着处理其Handler的引用,而在Activity中实例化一个非静态的Handler类,此类又会持有Activity的引用;当消息没处理完或者需要延迟处理就结束了当前Activity,此时Activity引用数不为0,就会造成Context泄露。问题就是这样,对策是不是也同样出来了,将Handler类声明为静态内部类,代码如下:

1
2
3
4
5
6
7
static class MyHandler extends Handler{
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
}

警告确实没有了,但是问题又来了。一般情况下,我们使用Handler就是为了配合Thread进行耗时操作然后更新UI,但是这里的Handler类是静态内部类,不能访问外部类的成员变量,怎么破!接下来,就该 WeakReference 派上用场了!

Google对WeakReference介绍不多,下面是官方文档中的介绍(以下"入队"指将该引用加入引用队列(Reference Queen)):

弱引用(WeakReference)
弱引用(WeakReference)是三种引用中间的一种。一旦GC判定一个对象时弱引用可到达,会发生以下情况:

有一组引用ref,这组引用包含以下元素:

  • 指向该对象的所有弱引用
  • 所有弱引用指向的软引用/强引用可到达对象

所有在这组ref中的引用会被自动清除

所以之前被ref引用的对象都可以被析构(回收)

在未来的某个时候,ref中所有的引用会根据自己的相应的引用队列(如果有)入队

弱引用在Map中很有用,如果一个弱引用没有被外部任何地方引用,它就会自动被移除。

SoftReferenceWeakReference 的区别就在于对象被回收、引用入队的时间点不同:
  • 如果一个对象是软引用可到达,那么这个对象会尽可能晚的被回收,这个引用同样会尽可能晚的入队。比如当VM内存不足时这种情形。
  • 如果一个对象被判定是弱引用可到达,那么这个对象会尽快被回收,这个引用也会尽快入队。
  • 弱引用不能阻挡GC对对象进行回收,由GC决定引用的对象何时回收并且将对象从内存移除
  • 使用get()方法获取其引用的对象

介绍完了弱引用,看看我们修改后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static class MyHandler extends Handler{
private final WeakReference<Context> mWeakReference;
public MyHandler(Context context){
mWeakReference = new WeakReference<Context>(context);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Activity mActivity;
if ((mActivity = (Activity)mWeakReference.get()) != null){
// Activity operation
// ...
}
}
}

这样我们就可以在静态内部类中使用操作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)

我们使用的最多的就是强引用,比如一句简单的赋值代码:

1
Button button = new Button(this);

创建一个Button对象,并将这个对象的引用存到button中。

虚引用 (PhantomReference)

虚引用是几类引用中最弱的一种,当一个对象被判定是虚引用可到达时,该引用就会被加入到引用队列(也就是当一个对象被回收之后),但是它的指向不会被清除。虚引用适合在一个对象回收前做一些清理操作,因为它比 finalize() 方法更灵活。

关于Java中的弱引用,这篇文章(译文)关于WeakReference写的很好,推荐。

参考: