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

原文链接: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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容