Andorid性能优化(二) 之 内存泄漏场景介绍

1 相关概念

1.1 内存泄漏

内存泄漏是指程序在向系统申请分配内存空间后,也就是说new了对象后,在使用完毕后没有对其进行释放。结果导致一直占据该内存单元。简单的说,在C/C++语言中,如果向堆中分配了内存(new了对象)后,没有对其进行释放掉(没有delete对象),那就是内存泄漏。在Java中由于有了垃圾回收机制,不再需要开发者手动去delete对象了,所以在Java中内存泄漏是指内存对象明明已经不需要的时候,但还仍然保留着这块内存和它的访问方式。

1.2 内存溢出

Android系统为每个应用进程都分配一个有封顶的堆内存值,当应用内存占用过高到没有足够的内存来提供给新对象分配并且垃圾回收机制也已经没有空间可回收时就会内存溢出或者叫OOM(Out Of Memory)。大量的内存泄漏就有可能会导致OOM。

1.3 JVM的内存

Java是在JVM所虚拟出的内存环境中运行的,JVM的内存分三个区:栈区(Stack)、堆区(Heap)和 方法区(Method)。

栈区:栈区中只保存基本数据类型的对象和自定义对象的引用。每个线程包含一个栈,每个栈中的数据都是私有的,其他栈不能访问。栈最显著的特征是:LIFO(Last In,First Out,后进先出)。

堆区:堆区用于存放由new创建的对象和数组。JVM只有一个堆区被所有线程共享。在堆中分配的内存,由Java虚拟机自动垃圾回收器来管理。

方法区:方法区又叫静态区,它包含整个程序中永远唯一的元素,如class和static变量。它跟堆一样也是被所有的线程共享

1.4 内存回收机制

在Java中JVM的栈记录了方法的调用,每个线程包含一个。在线程的运行过程当中,执行到一个新的方法调用,就在栈中增加一个内存单元。内存单元中保存有该方法调用的参数、局部变量和返回地址。当调用的方法结束时,该方法对应的内存单元就会从栈中自动删除,其所占用的内存空间也随之释放。而内存是存放着被创建出来的对象,它是不会随着方法的结束而清空。也就是说,方法结束后栈中内存得到了释放,但方法内创建出来的局部变量在方法结束后是依然存活在堆内存中的。所以Java引入了垃圾回收(Garbage Collection, 简称GC)机制来处理堆内存的回收。该机制可以自动清空堆中不再使用的对象,也就是没有引用指向的对象。但是如果一个明明已经没有使用价值的对象一直被引用着就会变成无法被回收,这就造成了内存的浪费,这就是Java的内存泄漏。

实现思想:

我们将栈定义为root,所以在GC root时就会遍历栈中的所有对象,如果这个对象有引用指向就会标记起来,然后对栈再次遍历,如果发现没有被标记上的对象,这些对象就是要进行垃圾回收的对象,就要将其清除。

GC触发时机:

GC又分为 Minor GC 和 Full GC (也称为 Major GC )。堆内存分为新生代和老年代,新生代有一个Eden区,两个Survivor区。新生成的对象直接放在Eden区,Eden区满了就放进Survivor1,当Survivor1满了就会触发一次Minor GC:将存活的对象放入Survivor2,然后清空Eden和Survivor1,再将Survivor区的交换,保证Survivor2为空。当Survivor2不足以存放Eden和Survivor1的存活对象时,就会放入老年区。较大的对象和长期存活的对象直接进入老年区。当即将进入老年区的对象超过老年区剩余大小时,触发一次Full GC。这个逻辑过程具体到什么时刻执行,这个是由系统来进行决定,是无法预测的。

1.5 对象引用

从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

强引用(Strong reference):

强引用是实际开发中最常见的一种引用类型,如:String str=”abc”中变量str就是字符串对象”abc”的一个强引用。如果持有对象的强引用,垃圾回收器是无法在内存中回收这个对象。

软引用(Soft Reference):

软引用通过SoftReference类来实现,当系统内存空间足够时,它不会被系统回收,但当系统内存不足时,而其指示的对象没有任何强引用对象指向时,系统将会回收它,软引用通常用于对内存敏感的程序中,使用如:

A a = new A();
SoftReference<A> srA = new SoftReference<A>(a);

弱引用(Weak Reference)

弱引用通过WeakReference类来实现,当系统垃圾回收机制运行时,不管系统内存是否足够,如果其指示的对象没有任何强引用对象指向时,系统就会回收它,使用如:

String str = "abc";
SoftReference srStr = new SoftReference(str);
str = null;
// 打印结果:abc
System.out.println(srStr.get());
//强制进行垃圾回收
System.gc();
System.runFinalization();
// 打印结果:null
System.out.println(srStr.get());

虚引用(Phantom Reference):

虚引用通过PhantomReference类实现,如果一个对象只有一个虚引用时。那它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列(ReferenceQueue)联合使用,如:

String str = "abc";
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference prStr = new PhantomReference(str, referenceQueue);
str = null;
// 打印结果:null
System.out.println(prStr.get());
//强制进行垃圾回收
System.gc();
System.runFinalization();
// 打印结果:true
System.out.println(referenceQueue.poll() == prStr);

2 导致内存泄漏的场景

在Android开发中,最容易引发内存泄漏是Activity该退出时没有正常退出。Activity是重量级对象,如若泄漏了Activity,也意味着泄漏它指向的所有对象。Android机器内存有限,太多的内存泄漏容易导致OOM。下面我们来列举一些比较常见的内存泄漏场景。

2.1 静态引用大对象

有时候,我们出现一些目的和开发上的便利,会定义一些全局的静态变量,但如果此类对象越来越多、越来越大时,就会产生对应的内存一直被占用着。此类内存是无法被释放的,因为static变量是惯穿整个App的生命周期。同时一定要避免使用static Activity和static View之类的代码的存在。

2.2 Activity或Service作为Context被传入单例中

有时候,往往在一个单例逻辑中要传入一个Context且该Context被赋值给了类中的成员变量,这本来也没什么大问题,但使用者有时不注意会在Activity或Service中传入了this。这样问题就来了,单例的生命周期有时候也跟static差不多,也可能是惯穿整个App的生命周期,但是Activity或Service通常的可能性就是存在一小段时间,只要我们不需要就会将其释放掉,但是如果被单例引用住,即使我们退出了Activity或Service,但它也不会被释放。所以如果仅需要Context时应该尽量使用ApplicationContext。

2.3 BroadcastReceiver没有反注册

有时候,我们使用BroadcastReceiver在Activity中的onCreate中动态注册,而这个BroadcastReceiver的生命周期本来是跟Activity一样的,但是在Activity的onDestroy中忘记或异常导致没有反注册。这样的话,这个广播就会一直存在于应用中,而这个广播会一直持有着Activity的引用,从而也导致了Activity无法释放。动态广播最好在Activity 的 onResume()注册、onPause()注销。因为当系统因为内存不足要回收Activity占用的资源时,Activity在执行完onPause()方法后就会被销毁,有些生命周期方法onStop(),onDestory()就不会执行。当再回到此Activity时,是从onCreate方法开始执行。 假设我们将广播的注销放在onStop(),onDestory()方法里的话,有可能在Activity被销毁后还未执行onStop(),onDestory()方法,即广播仍还未注销,从而导致内存泄露。

2.4 没有正确调用close

有时候,我们在使用IO流、Socket或Cursor时,忘记调用其close或没有正确调用close,也会导致内存泄漏。所以在一般情况下,都是建议使用try catch时,将需要释放的代码写在finally中。

2.5 内部类导致外部无法被释放

在Java中,非静态内部类和匿名内部类会持有外部类的隐式引用,而静态内部类则不会。所以要避免在外部类该结束时被内部类占用着引用,从而导致外部类不能被释放的情况,请看下面一个错误的实例:

public class TestActivity extends Activity {
    private  final static int MSG_TEST = 1;
    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == MSG_TEST) {
                // TODO...
            }
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        mHandler.sendEmptyMessageDelayed(MSG_TEST, 1000 * 60 * 10);
        finish();
    }
}

代码中,当Activity生命周期本该结束时,执行了一个延时10分钟的Message,该Message持有了所在Activity的Handler的引用,而Handler持有外部类TestActivity的隐式引用。该引用会继续存在直到Message被处理完毕。所以这里就阻止了本该结束的Activity的回收,从而导致内存泄漏。解决这种情况的问题,可以将代码修改成这样:

public class TestActivity extends Activity {
    private  final static int MSG_TEST = 1;
    private static class MyHandler extends Handler {
        private final WeakReference<TestActivity> mActivity;
        public MyHandler(TestActivity activity) {
            mActivity = new WeakReference<TestActivity>(activity);
        }
        @Override
        public void handleMessage(Message msg) {
            TestActivity activity = mActivity.get();
            if (activity == null) {
                return;
            }
            if (msg.what == MSG_TEST) {
                // TODO...
            }
        }
    }

    private final MyHandler mHandler = new MyHandler(this);
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        mHandler.sendEmptyMessageDelayed(MSG_TEST, 1000 * 60 * 10);
        finish();
    }
}

修改后的代码中,新建一个Handler的静态子类。因为静态内部类不会持有外部类的隐式引用,所以就不会导致内存泄漏。此刻若是内部类需要调用外部类的方法,可以让Handler持有一个Activity的弱引用对象。弱引用的特征是,当系统垃圾回收机制运行时,如果其指示的对象没有任何强引用对象指向时,系统就会回收它,所以这里很好地解决延迟执行还强占Activity的问题。

内部类的使用看似是正常不过的逻辑,但实质上隐藏着玄机,一不小心就会踩中坑了,所以我们在日常开发中都应该注意这种情况,要避免在Activity中使用非静态内部类,如果该内部类的实例会存在于Activity的生命周期之外,那必须要使用静态内部类持有一个外部类的弱引用替代。

2.6 WebView大坑

WebView是Android提供的比较重量级的控件,我们在平时使用WebView时,一定要记得调用其destory()方法来释放内部持有的数据对象。否则它内部的一些资源是不会被释放的从而导致内存泄漏。WebView本身也是算在Android开发中比较多坑的控件。一般情况下,都是建议将其放置在一个单独的进程中来做事,然后待事情做完了后将此进程杀掉一了百了,不用担心使用后内存出什么大问题。因为WebView可能会存在不同的内核和不同的厂商也可能对其进行过修改,所以它本身算是一个比较不稳定的东西。

2.7 属性动画没有停止

如果在Activity中播放属性动画没有进行过停止动画,那么动画就会一直播放下去,尽管已经无法在界面上看到动画效果,并且这时Activity的View会被动画持有,而View又持有了Activity,最终Activity无法释放。解决方法可以在Activity的onDestroy中调用animator.cancel()来停止动画,或者建议一般处理动画时在View的onAttachedToWindow、onDetachedFromWindow和 onWindowVisibilityChanged这三个方法中,代表在显示时、初始化时便执行开始动画,在隐藏时、销毁时便执行停止动画。代码如:

@Override
protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    stopAnimation();
}

@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    startAnimation();
}

@Override
protected void onWindowVisibilityChanged(int visibility) {
    super.onWindowVisibilityChanged(visibility);
    if (visibility == VISIBLE) {
        startAnimation();
    } else {
        stopAnimation();
    }
}

 

更多场景有待补充!!

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值