Android的内存泄漏分析

内存泄漏分析

为什么会产生内存泄漏?

当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

内存泄漏对程序的影响?

内存泄漏是造成应用程序OOM的主要原因之一!我们知道Android系统为每个应用程序分配的内存有限,而当一个应用中产生的内存泄漏比较多时,这就难免会导致应用所需要的内存超过这个系统分配的内存限额,这就造成了内存溢出而导致应用Crash。

1.单例造成的内存泄漏

单例模式非常受开发者的喜爱,不过使用的不恰当的话也会造成内存泄漏,由于单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。
如下这个典例:



  
  
public  class SingleMode {
    private static SingleMode instance;
    private Context context;

    private SingleMode(Context context) {
        this.context = context;
    }

    public static SingleMode getInstance(Context context) {
        if (instance != null) {
            instance = new SingleMode(context);
        }
        return instance;
    }
}

这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:

  1. 传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长

  2. 传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。 所以正确的单例应该修改为下面这种方式:


 public class SingleMode {
    private static SingleMode instance;
    private Context context;

    private SingleMode(Context context) {
        this.context = context.getApplicationContext();
    }

    public static SingleMode getInstance(Context context) {
        if (instance != null) {
            instance = new SingleMode(context);
        }
        return instance;
    }
}

这样不管传入什么Context最终将使用Application的Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏

2.非静态内部类创建静态实例造成的内存泄漏

有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:


public class MainActivity extends AppCompatActivity {
    private static TestResource mResource = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (mResource == null) {
            mResource = new TestResource();
        }
        //...
    }	

	class TestResource {
        //...
    }
}

这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。

正确的做法为:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请使用ApplicationContext

3.Handler造成的内存泄漏

Handler的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等api都应该会借助Handler来处理,对于Handler的使用代码编写一不规范即有可能造成内存泄漏,如下示例:


public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler() {
         @Override
         public void handleMessage(Message msg) {
                            //...
                        }
     };
     @Override
     protected void onCreate(Bundle savedInstanceState) {
                    super.onCreate(savedInstanceState);
                    setContentView(R.layout.activity_main);
                    loadData();
                }
    private void loadData(){
                    //...request
                    Message message = Message.obtain();
                    mHandler.sendMessage(message);
                }
}

这种创建Handler的方式会造成内存泄漏,由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏,所以另外一种做法如下:


	public class MainActivity extends AppCompatActivity {
	    private MyHandler mHandler = new MyHandler(this);
	    private TextView mTextView;
	    
	    private static class MyHandler extends Handler {
	        private WeakReference reference;
	        public MyHandler(Context context) {
	             reference = new WeakReference<>(context);
	            
	        }
	
	        @Override
	        public void handleMessage(Message msg) {
	             MainActivity activity = (MainActivity) reference.get();
	             if (activity != null) {
	                 activity.mTextView.setText("");
	            }
	        }
	    }
	    
	    @Override
	    protected void onCreate(Bundle savedInstanceState) {
	         super.onCreate(savedInstanceState);
	         setContentView(R.layout.activity_main);
	         mTextView = (TextView) findViewById(R.id.textview);
	         loadData();
	    }
	    
	    private void loadData() {
	                 //...request
	         Message message = Message.obtain();
	         mHandler.sendMessage(message);
	        
	    }
	
	     //在Activity的Destroy时或者Stop时应该移除消息队列中的消息
	    
	    @Override
	    protected void onDestroy() {
	         super.onDestroy();
	         mHandler.removeCallbacksAndMessages(null);    
	    }
 	}

创建一个静态Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象,这样就避免了Activity泄漏。

不过Looper线程的消息队列中还是可能会有待处理的消息,所以我们在Activity的Destroy时或者Stop时应该移除消息队列中的消息使用:mHandler.removeCallbacksAndMessages(null);是移除消息队列中所有消息和所有的Runnable。当然也可以使用mHandler.removeCallbacks();或mHandler.removeMessages();来移除指定的Runnable和Message。

4.线程造成的内存泄漏

对于线程造成的内存泄漏,也是平时比较常见的,如下这两个示例可能每个人都这样写过:


 new AsyncTask< Void, Void, Void>() {
    @Override
    protected Void doInBackground(Void... params) {
        SystemClock.sleep(10000);
        return null;
    }
 }.execute();
 

 new Thread(new Runnable() {
    @Override
    public void run() {
        SystemClock.sleep(10000);
    }
 }).start();

上面的异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。如果Activity在销毁之前,任务还未完成,
那么将导致Activity的内存资源无法回收,造成内存泄漏。正确的做法还是使用静态内部类的方式,如下:


static class MyAsyncTask extends AsyncTask< Void, Void, Void> {
    private WeakReference weakReference;

    public MyAsyncTask(Context context) {
        weakReference = new WeakReference<>(context);
    }

    @Override
    protected Void doInBackground(Void... params) {
        SystemClock.sleep(10000);
        return null;
    }

    @Override
    protected void onPostExecute(Void aVoid) {
        super.onPostExecute(aVoid);
        MainActivity activity = (MainActivity) weakReference.get();
        if (activity != null) {
            //...
        }
    }
}
static class MyRunnable implements Runnable{
    @Override
    public void run() {
        SystemClock.sleep(10000);
    }
}

这样就避免了Activity的内存资源泄漏,当然在Activity销毁时候也应该取消相应的任务AsyncTask:cancel(),避免任务在后台执行浪费资源。

5.使用WebView导致的内存泄漏

  1. 不要在布局文件中声明,改成在Activity中创建 如,WebView mWebView = new WebView(this);

  2. 在布局文件中用容器类布局,比如FrameLayout作为WebView的容器,在Activity中主动把WebView添加到容器中。

  3. 在OnDestory()中移除、销毁WebView。

举个例子吧:我们用FrameLayout作为WebView的父容器
1: 使用容器包裹WebView

2:在Activity中创建WebView,在OnDestroy()方法中从容器中移除、销毁WebView


public class TestActivity extends Activity {
    private FrameLayout mWebContainer;
    private WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.your_layout);

        mWebContainer = (FrameLayout) findViewById(R.id.web_container);
        mWebView = new WebView(getApplicationContext());
        mWebContainer.addView(mWebView);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mWebContainer.removeAllViews();
        mWebView.destroy();
    }
}

之所以这么做的原因是在XML文件中创建WebView,会把Activity作为Context传给WebView,而不是Application Context。所以在finishingActivity的时候,WebView任然持有Activity引用,导致Activity无法被回收。

参考资料:https://stackoverflow.com/questions/3130654/memory-leak-in-webview

6.资源未关闭造成的内存泄漏

对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

Java/Android引用类型及其使用分析
Java/Android中有四种引用类型,分别是:

  • Strong reference - 强引用
  • Soft Reference - 软引用
  • Weak Reference - 弱引用
  • Phantom Reference - 虚引用

不同的引用类型有着不同的特性,同时也对应着不同的使用场景。

  1. Strong reference - 强引用
    实际编码中最常见的一种引用类型。常见形式如:A a = new A();等。强引用本身存储在栈内存中,其存储指向对内存中对象的地址。一般情况下,当对内存中的对象不再有任何强引用指向它时,垃圾回收机器开始考虑可能要对此内存进行的垃圾回收。如当进行编码:a = null,此时,刚刚在堆中分配地址并新建的a对象没有其他的任何引用,当系统进行垃圾回收时,堆内存将被垃圾回收。

    SoftReference、WeakReference、PhantomReference都是类java.lang.ref.Reference的子类。Reference作为抽象基类,定义了其子类对象的基本操作。Reference子类都具有如下特点:

    1.Reference子类不能无参化直接创建,必须至少以强引用对象为构造参数,创建各自的子类对象;

    2.因为1中以强引用对象为构造参数创建对象,因此,使得原本强引用所指向的堆内存中的对象将不再只与强引用本身直接关联,与Reference的子类对象的引用也有一定联系。且此种联系将可能影响到对象的垃圾回收。
    根据不同的子类对象对其指示对象(强引用所指向的堆内存中的对象)的垃圾回收不同的影响特点,分别形成了三个子类,即SoftReference、WeakReference和PhantomReference。

  2. Soft Reference - 软引用
    软引用的一般使用形式如下:


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

通过对象的强引用为参数,创建了一个SoftReference对象,并使栈内存中的wrA指向此对象。
此时,进行如下编码:a = null,对于原本a所指向的A对象的垃圾回收有什么影响呢?
先直接看一下下面一段程序的输出结果:


import java.lang.ref.SoftReference;

        public class ReferenceTest {

            public static void main(String[] args) {

                A a = new A();

                SoftReference srA = new SoftReference(a);

                a = null;

                if (srA.get() == null) {
                    System.out.println("a对象进入垃圾回收流程");
                } else {
                    System.out.println("a对象尚未被回收" + srA.get());
                }

                // 垃圾回收
                System.gc();

                if (srA.get() == null) {
                    System.out.println("a对象进入垃圾回收流程");
                } else {
                    System.out.println("a对象尚未被回收" + srA.get());
                }

            }
        }

        class A {

    }

##输出结果为:
1 a对象尚未被回收A@4807ccf6

2 a对象尚未被回收A@4807ccf6

当 a = null后,堆内存中的A对象将不再有任何的强引用指向它,但此时尚存在srA引用的对象指向A对象。当第一次调用srA.get()方法返回此指示对象时,由于垃圾回收器很有可能尚未进行垃圾回收,此时get()是有结果的,这个很好理解。当程序执行System.gc();强制垃圾回收后,通过srA.get(),发现依然可以得到所指示的A对象,说明A对象并未被垃圾回收。那么,软引用所指示的对象什么时候才开始被垃圾回收呢?需要满足如下两个条件:

1.当其指示的对象没有任何强引用对象指向它;

2.当虚拟机内存不足时。

因此,SoftReference变相的延长了其指示对象占据堆内存的时间,直到虚拟机内存不足时垃圾回收器才回收此堆内存空

3.Weak Reference - 弱引用
同样的,软引用的一般使用形式如下:


	A a = new A();
	WeakReference wrA = new WeakReference(a);

当没有任何强引用指向此对象时, 其垃圾回收又具有什么特性呢?


import java.lang.ref.WeakReference;

     public class ReferenceTest {

         public static void main(String[] args) {

                A a = new A();

                WeakReference wrA = new WeakReference(a);

                a = null;

                if (wrA.get() == null) {
                        System.out.println("a对象进入垃圾回收流程");
                    } else {
                        System.out.println("a对象尚未被回收" + wrA.get());
                    }

                // 垃圾回收
                System.gc();

                if (wrA.get() == null) {
                        System.out.println("a对象进入垃圾回收流程");
                    } else {
                        System.out.println("a对象尚未被回收" + wrA.get());
                    }

            }
    }

    class A {

    }

##输出结果为:

  1. a对象尚未被回收A@52e5376a
  2. a对象进入垃圾回收流程
    输出的第一条结果解释同上。当进行垃圾回收后,wrA.get()将返回null,表明其指示对象进入到了垃圾回收过程中。因此,对弱引用特点总结为:
    WeakReference不改变原有强引用对象的垃圾回收时机,一旦其指示对象没有任何强引用对象时,此对象即进入正常的垃圾回收流程。
    那么,依据此特点,很可能有疑问:WeakReference存在又有什么意义呢?
    其主要使用场景见于:当前已有强引用指向强引用对象,此时由于业务需要,需要增加对此对象的引用,同时又不希望改变此引用的垃圾回收时机,此时WeakReference正好符合需求,常见于一些与生命周期的场景中。
    4.PhantomReference - 虚引用
    与SoftReference或WeakReference相比,PhantomReference主要差别体现在如下几点:
    1.PhantomReference只有一个构造函数PhantomReference(T referent, ReferenceQueue<? super T> q),因此,PhantomReference使用必须结合ReferenceQueue;
    2.不管有无强引用指向PhantomReference的指示对象,PhantomReference的get()方法返回结果都是null。

             public static void main(String[] args) {
           
                    A a = new A();
           
                    ReferenceQueue rq = new ReferenceQueue();
                    PhantomReference prA = new PhantomReference(a, rq);
           
                    System.out.println("prA.get():" + prA.get());
           
                    a = null;
           
                    System.gc();
           
                    try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
           
                    System.out.println("rq item:" + rq.poll());
           
                }
}

        class A {

        }

##输出结果为:
1 prA.get():null

2 rq item:java.lang.ref.PhantomReference@1da12fc0

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。


ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

总结

  1. 对于生命周期比Activity长的对象如果需要应该使用ApplicationContext
  2. 在涉及到Context时先考虑ApplicationContext,当然它并不是万能的,对于有些地方则必须使用Activity的Context,如Dialog的使用,只有在Activity中才能创建
  3. 对于需要在静态内部类中使用非静态外部成员变量(如:Context、View ),可以在静态内部类中使用弱引用来引用外部类的变量来避免内存泄漏
  4. 对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:
    将内部类改为静态内部类
    静态内部类中使用弱引用来引用外部类的成员变量
  5. 对于不再需要使用的对象,显示的将其赋值为null,比如使用完Bitmap后先调用recycle(),再赋为null
  6. 保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值