安卓进阶利器Android开发经验谈Android技术知识

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

2015-12-23  本文已影响715人  whilu

原文链接:https://blog.lujun.co/2015/12/22/Android%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96(%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E7%AC%AC%E4%B8%80%E7%AF%87)/

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

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

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

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这项操作,发现越到后面卡顿越严重。看下面两张图,这是某两个时刻的内存使用情况(一前一后):


first_time_capture.png
second_time_capture.png

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


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

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

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){

        }
    }
}

// 使用
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打印日志如下图:


dalvikvm_log_2.png

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

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

二、Handler造成Context泄露

先看代码:

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会提示一个警告如下:

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

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

static class MyHandler extends Handler{

    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
    }
}

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

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

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

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

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

  • 所有在这组ref中的引用会被自动清除
  • 所以之前被ref引用的对象都可以被析构(回收)
  • 在未来的某个时候,ref中所有的引用会根据自己的相应的引用队列(如果有)入队
    弱引用在Map中很有用,如果一个弱引用没有被外部任何地方引用,它就会自动被移除。SoftReference和WeakReference的区别就在于对象被回收、引用入队的时间点不同:
  • 如果一个对象是软引用可到达,那么这个对象会尽可能晚的被回收,这个引用同样会尽可能晚的入队。比如当VM内存不足时这种情形。
  • 如果一个对象被判定是弱引用可到达,那么这个对象会尽快被回收,这个引用也会尽快入队。
  • 弱引用不能阻挡GC对对象进行回收,由GC决定引用的对象何时回收并且将对象从内存移除
  • 使用get()方法获取其引用的对象

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

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)

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

Button button = new Button(this); // 创建一个Button对象,并将这个对象的引用存到button中。
虚引用 (PhantomReference)

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

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

参考
[Android最佳性能实践][1]
[http://developer.android.com/reference][2]
[1]:http://blog.csdn.net/guolin_blog/article/details/42238633/
[2]:http://developer.android.com/reference

上一篇下一篇

猜你喜欢

热点阅读