优化之内存泄露的总结

优化之内存泄露

一.常见的内存泄漏和解决方案

什么是内存泄漏?
内存泄漏是当程序不再使用到的内存时,释放内存失败而产生了无用的内存消耗。
内存泄漏并不是指物理上的内存消失,这里的内存泄漏是指由程序分配的内存但是由于程序逻辑错误而导致程序失去了对该内存的控制,使得内存浪费。

怎样会导致内存泄漏?
对象被生命周期长的对象引用,如activity被静态集合引用导致activity不能释放;
资源对象没关闭造成的内存泄漏,如查询数据库后没有关闭游标cursor;
Bitmap对象不在使用时调用recycle()释放内存;
构造Adapter时,没有使用 convertView 重用


内存泄漏对于app没有直接的危害,即使app有发生内存泄漏的情况,也不一定会引起app崩溃,但是会增加app内存的占用。
内存得不到释放,慢慢的会造成app内存溢出。所以我们解决内存泄漏的目的就是防止app发生内存溢出。

内存泄漏检测工具是: LeakCanary   github地址:https://github.com/square/leakcanary

Android常见的内存泄漏的场景以及解决方式?
1)单例造成的内存泄漏(传递activity的引用);
2)非静态内部类创建"静态实例"造成的内存泄漏;
3)Handler匿名内部类造成的内存泄漏; 新建线程引起的Activity内存泄漏(Timer,AsyncTask)
4)资源没有释放导致的内存泄露:WebView未释放造成的泄漏;和 资源未关闭造成的内存泄漏;
5)集合中的对象没清空造成内存泄漏;
    使用ListView时造成的内存泄漏

1、单例引起的内存泄漏
由于单例的静态特性导致它的生命周期和整个应用的生命周期一样长,如果一个对象已经不再使用了,但又却被单例持有引用,那么会导致这个对象不能被正常回收,从而导致内存泄漏。

// 使用了单例模式
public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

问题所在:
从上面的代码我们可以看出,在创建单例对象的时候,引入了一个Context上下文对象,如果我们把Activity注入进来,会导致这个Activity一直被单例对象持有引用,当这个Activity销毁的时候,对象也是没有办法被回收的。

解决方案:
 让这个上下文对象指向应用的上下文即可(this.context=context.getApplicationContext()),因为应用的上下文对象的生命周期和整个应用一样长。

2、非静态内部类创建静态实例引起的内存泄漏
由于非静态内部类会默认持有外部类的引用,如果在外部类中去创建这个内部类对象,当频繁打开关闭Activity,会导致重复创建对象,造成资源的浪费。

为了避免这个问题一般会把这个实例设置为静态,这样虽然解决了重复创建实例,但是会引发出另一个问题,就是静态成员变量它的生命周期是和应用的生命周期一样长的,然而这个静态成员变量又持有该Activity的引用,所以导致这个Activity销毁的时候,对象也是无法被回收的。

问题所在:
其实和上面单例对象的内容泄漏问题是一样的,由于静态对象持有Activity的引用,导致Activity没办法被回收。

解决方案:
把非静态内部类改成"静态内部类"即可(static class TestResource 或将该内部类抽取出来封装成一个单例(如果需要使用Context,就使用Application的Context)。

有时我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现如下写法:
这样在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据。
虽然避免了资源的重复创建,但却会造成内存泄漏:
因为非静态内部类默认会持有外部类的引用,而非静态的内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这导致了该静态实例一直持有该Actitity的引用,从而导致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 { //... }
//改进方式:
private TestResource mResource = null;
   static class TestResource{
//.....
}
}

3、Handler引起的内存泄漏

3.1、Handler造成的内存泄漏
示例:创建匿名内部类的静态对象
1、从Android的角度
当Android应用程序启动时,该应用程序的主线程会自动创建一个Looper对象和与之关联的MessageQueue关联起来。
所有发送到MessageQueue的Message都会持有Handler的引用,所以Looper会据此回调Handler的handleMessage()方法会处理消息。
只要MessageQueue中有未处理的Message,Looper就会不断的从中取出并交给Handler处理。
另外,主线程的Looper对象会伴随该应用程序的整个生命周期。

2、Java角度
在Java中,非静态内部类和匿名内部类都会潜在持有它们所属的外部类的引用,但是静态内部类不会。
对上述示例分析,当MainActivity结束时,未处理的消息持有handler的引用,而handler又持有它所属的外部类也就是mainActivity的引用。
这条引用关系会一直保持直到消息得到处理,这样阻止了MainActivity被垃圾回收器回收,从而造成了内存泄漏。
解决方法:使用"静态内部类" 或者 将Handler类独立出来 

问题所在:

首先,程序启动时在主线程中会创建一个Looper对象,这个Looper里维护着一个MessageQueue消息队列,这个消息队列里会按时间顺序存放着Message。

文章《从源码的角度彻底理解Android的消息处理机制》,然后上面的Handler是通过内部类来创建的,内部类会持有外部类的引用,也就是Handler持有Activity的引用,而消息队列中的消息target是指向Handler的,也就等同消息持有Handler的引用,也就是说当消息队列中的消息如果还没有处理完,这些未处理的消息(也可以理解成延迟操作)是持有Activity的引用的,此时如果关闭Activity,是没办法回收的,从而导致内存泄露。

解决方案:
把非静态内部类改成静态内部类(如果是Runnable类也需要改成静态),然后在Activity的onDestroy中移除对应的消息,同时需在Handler内部用弱引用持有Activity,因为让内部类不再持有外部类的引用时,程序也就不允许Handler操作Activity对象了。

    Handler mHandler=new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case 0:
                    //to do something..
                    break;
            }    
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new Thread(new Runnable() {
            @Override
            public void run() {
                //to do something..
                mHandler.sendEmptyMessage(0);
            }
        }).start();
    }
   private MyHandler myHandler = new MyHandler(this);
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
         new Thread(new Runnable() {
            @Override
            public void run() {
                myHandler.sendMessage(Message.obtain());
            }
        }).start();
    }
    @Override
    protected void onDestroy() {
        //移除对应的Runnable或者是Message
        //mHandler.removeCallbacks(runnable);
        //mHandler.removeMessages(what);
        mHandler.removeCallbacksAndMessages(null);
    }

    private static class MyHandler extends Handler {
        private WeakReference<Activity> mActivity;
        public MyHandler(Activity activity) {
            mActivity = new WeakReference<Activity>(activity);
        }
        @Override
        public void handleMessage(Message msg) {
            if (mActivity.get() == null) {
                return;
            }
             //to do something..
        }
    };

3.2、Handler匿名内部类造成内存溢出? 

这段代码运行后,立即点击 finish按钮,通过检测发现HandlerActivity 出现了内存泄漏。
当Activity finish后,延时消息会继续存在主线程消息队列中8秒钟,然后处理消息。
而该消息引用了Activity的Handler对象,然后这个Handler又引用了这个Activity。这些引用对象会保持到该消息被处理完,这样就导致该Activity对象无法被回收,从而导致了上面说的Activity泄露。
Handler是个很常用也很有用的类,异步,线程安全等等。如果有下面这样的代码,会发生什么呢?
handler.postDeslayed ,假设delay时间是几个小时… 这意味着什么?
意味着只要handler的消息还没有被处理结束,它就一直存活着,包含它的Activity就跟着活着。
修复的方案是WeakReference,也就是所谓的弱引用。垃圾回收器在回收的时候,是会忽视掉弱引用的,所以包含它的 Activity会被正常清理掉。
如何避免?
使用静态内部类+弱引用。

public class HandlerActivity extends AppCompatActivity {
    private final static int MESSAGECODE = 1 ;
    private final Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            Log.d("mmmmmmmm" , "handler " + msg.what ) ;
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        findViewById(R.id.finish).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
        new Thread(new Runnable() {
            @Override
            public void run() {
                handler.sendEmptyMessage(MESSAGECODE) ;
                try {
                    Thread.sleep( 8000 );
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                handler.sendEmptyMessage(MESSAGECODE) ;
            }
        }).start();
        //fixData();//改进的方式
    }
     
    // 改进措施:使用静态内部类+弱引用。
    private static Handler handler ;
    private void fixData(){
        handler = new MyHandler(this) ;
        new Thread(new Runnable() {
            @Override
            public void run() {
                handler.sendEmptyMessage(MESSAGECODE) ;
                try {
                    Thread.sleep( 8000 );
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                handler.sendEmptyMessage(MESSAGECODE) ;
            }
        }).start() ;
    }
    private static class MyHandler extends Handler {
        WeakReference<HandlerActivity> weakReference ;
        public MyHandler(HandlerActivity activity ){
            weakReference  = new WeakReference<HandlerActivity>( activity) ;
        }
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if ( weakReference.get() != null ){
                // update android ui
                Log.d("mmmmmmmm" , "handler " + msg.what ) ;
            }
        }
    }
}

这个Handler已经使用了静态内部类,并且使用了弱引用。但是这个并没有完全解决HandlerActivity内存泄漏的问题,
罪魁祸首是线程创建的方式出了问题,就像本文的第一个例子一样。改进的方式,是把Runnable类写成静态内部类。
最终完整的代码如下:等等,还没完呢?
上面这个代码已经有效的解决了Handler,Runnable引用Activity实例从而导致内存泄漏的问题,但是这不够。
因为内存泄漏的核心原因就是这个某个对象应该被系统回收内存的时候,却被其他对象引用,造成该内存无法回收。
再回到上面这个问题,当当前Activity调用finish销毁的时候,在这个Activity里面所有线程是不是应该在OnDestory()方法里取消线程。
当然是否取消异步任务,要看项目具体的需求,比如在Activity销毁的时候,启动一个线程,异步写log日志到本地磁盘,
针对这个需求却需要在OnDestory()方法里开启线程。所以根据当前环境做出选择才是正解。
所以还可以修改代码为: 在onDestroy()里面移除所有的callback和Message 。

public class HandlerActivity extends AppCompatActivity {
    private final static int MESSAGECODE = 1 ;
    private static Handler handler ;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        findViewById( R.id.finish ).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
        handler = new MyHandler( this ) ;
        new Thread(new MyRunnable()).start();
    }
 
    private static class MyHandler extends Handler {
        WeakReference<HandlerActivity> weakReference ;
        public MyHandler(HandlerActivity activity ){
            weakReference  = new WeakReference<HandlerActivity>( activity) ;
        }
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if ( weakReference.get() != null ){
                // update android ui
                Log.d("mmmmmmmm" , "handler " + msg.what ) ;
            }
        }
    }
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            handler.sendEmptyMessage( MESSAGECODE ) ;
            try {
                Thread.sleep( 8000 );
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            handler.sendEmptyMessage( MESSAGECODE ) ;
        }
    }
    //补充: 
    @Override
    protected void onDestroy() {
        super.onDestroy();
       //如果参数为null的话,会将所有的Callbacks和Messages全部清除掉。
        handler.removeCallbacksAndMessages( null );
    }
}

4、新建的线程引起的Activity内存泄漏:

示例:AsyncTask和 Runnable
AsyncTask和Runnable都使用了匿名内部类,那么他们将持有其所在Activity的隐式引用。
如果任务在Activity销毁之前还未完成,那么将导致Activity的内存资源无法被回收,从而造成内存泄漏。
解决方法:将Asynctask和Runnable类独立出来 或者 使用"静态内部类"
4.1、Asynctask引起的内存泄露
  这部分和Handler比较像,其实也是因为内部类持有外部类引用,一样的改成静态内部类,然后在onDestory方法中取消任务即可。

运行下面的代码后,点击finish按钮,过一会儿发生了内存泄漏的问题。
为什么Activity6会发生内存泄漏?
进入Activity6界面,然后点击finish按钮,Activity6销毁。
但是Activity6里面的线程还在运行,匿名内部类Runnable对象引用了Activity6的实例,导致Activity6所占用的内存不能被GC及时回收。
如何改进? Runnable改为静态非匿名内部类即可。

public class Activity6 extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_6);
        findViewById(R.id.finish).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {       
                    Thread.sleep( 15000 );
                } 
            }
        }).start();
        // 改进措施
        //new Thread( new MyRunnable()).start();
    } 
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep( 15000 );
            } 
        }}
}

4.1、AsyncTask造成内存泄漏
为什么?
上面代码在activity中创建了一个匿名类AsyncTask,匿名类和非静态内部类相同,会持有外部类对象,这里也就是activity,
因此,如果你在Activity里声明且实例化一个匿名的AsyncTask对象,则可能会发生内存泄漏,
如果这个线程在Activity销毁后还一直在后台执行,那这个线程会继续持有这个Activity的引用从而不会被GC回收,直到线程执行完成。

怎么解决?
自定义静态AsyncTask类,AsyncTask的周期和Activity周期保持一致。也就是在Activity生命周期结束时要将AsyncTask cancel掉。

public class Activity2 extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_2);
        findViewById( R.id.finish2).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });

        new AsyncTask<String,Integer,String>(){
            @Override
            protected String doInBackground(String... params) {
                try {
                    Thread.sleep( 6000 );
                } catch (InterruptedException e) {
                }
                return "ssss";
            }
 
            @Override
            protected void onPostExecute(String s) {
                super.onPostExecute(s);
                Log.d( "mmmmmm activity2 " , "" + s ) ;
            }
 
        }.executeOnExecutor( AsyncTask.THREAD_POOL_EXECUTOR , "" ) ;

        //改进措施;
        myTask = new MyTask() ;
        myTask.executeOnExecutor( AsyncTask.THREAD_POOL_EXECUTOR , "") ;
    }
    // 改进方案:
    private static MyTask myTask ;
    private static class MyTask extends AsyncTask{
        @Override
        protected Object doInBackground(Object[] params) {
            try {
                //模拟耗时操作
                Thread.sleep( 15000 );
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (myTask != null ){
            myTask.cancel(true ) ;
        }
    }
} 

4.2、TimerTasks 造成内存泄漏
为什么? 这里内存泄漏在于Timer和TimerTask没有进行Cancel,从而导致Timer和TimerTask一直引用外部类Activity。
怎么解决? 在适当的时机进行Cancel。加上TimerTask用静态内部类
注意:在网上看到一些资料说,解决TimerTask内存泄漏可以使用在适当的时机进行Cancel。
经过测试,证明单单使用在适当的时机进行Cancel,还是有内存泄漏的问题,所以一定要用静态内部类配合使用。

public class TimerActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_2);
        findViewById( R.id.finish2).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
        //开始定时任务
        startTimer();
    }
 
    void startTimer(){
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() { 
                while(true);
            }
        },1000 );  //1秒后启动一个任务
    }

    // 改进的方式:静态内部类 + 销毁时候取消任务
   private TimerTask timerTask ;
    void startTimer2(){
        timerTask = new MyTimerTask() ;
        new Timer().schedule(timerTask ,1000 );  // 1秒后启动一个任务
    }
private static class MyTimerTask extends TimerTask{ @Override public void run() { while(true){ Log.d( "ttttttttt" , "timerTask" ) ; } } } @Override protected void onDestroy() { super.onDestroy(); if (timerTask != null ){ timerTask.cancel() ; } } }

5、WebView引起的内存泄露
当我们不要使用WebView对象时,应该调用它的deatory()来销毁它,并释放其占用的内存,否则其长期占用内存也不能被回收,从而造成内存泄漏。
关于WebView的内存泄漏,绝对大坑!不同版本都存在着不同版本的问题,这里我只能给出我平时的处理方法,可能不同机型上存在差异,只能靠积累了。
方法一:
首先不要在xml去定义<WebView/>,定义一个ViewGroup就行,然后动态在代码中new WebView(Context context)(传入的Context采取弱引用),再通过addView添加到ViewGroup中,最后在页面销毁执行onDestroy()的时候把WebView移除。

方法二:
简单粗暴,直接为WebView新开辟一个进程,在结束操作的时候直接System.exit(0)结束掉进程,这里需注意进程间的通讯,可以采取Aidl,Messager,
Content Provider,Broadcast等方式。
解决方法:为WebView另外开启一个进程,通过AIDL与主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

6、资源对象未关闭引起的内存泄露
如常使用的广播接收者,数据库的游标,多媒体,文档,套接字等。
对于使用了BroadcastReceiver,ContentObserver,Cursor,Stream,Bitmap,File等资源,应该在Activity销毁时及时关闭或者注销,
否则这些资源将不会被回收,从而造成内存泄漏。
1)、比如在Activity中register了一个BroadcastReceiver,但在Activity结束后没有unregister该BroadcastReceiver。
2)、资源性对象比如Cursor,Stream、File文件等往往都用了一些缓冲,在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。
      它们的缓冲不仅存在于java虚拟机内,还存在于java虚拟机外。如果我们仅仅把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。
3)、对于资源性对象在不使用的时候,应该调用它的close()将其关闭掉,然后再设置为null。在程序退出的时候一定要确保我们的资源性对象已经关闭。
4)、Bitmap对象不再使用时,调用recycle()释放内存。2.3以后bitmap应该是不需要手动recycle了,内存已经在java层。

7、集合容器中的内存泄漏
一些对象的引用加入到集合(如ArrayList)中,当我们不需要该对象时,并没有把他的引用从集合中清理掉,这样这个集合就会越来越大。
如果这个集合是static的话,那情况就更严重了。
解决方法:在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。

8、使用ListView时造成的内存泄漏
初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View缓存起来。
当向上滚动时,原先位于最上面的Item的View对象被回收,然后被用来构造新出现在下面的Item。这个构造过程就是由getView()方法完成的,getView()的第二个形参convertView就是被缓存起来的Item的View对象
(初始化是缓存中没有View对象则convertView是null)。
构造Adapter时,没有使用缓存的convertView。
解决方法:在构造Adapter时,使用缓存的convertView。

9、其他一些
还有一些需要注意的,比如注册了EventBus没注销,添加Activity到栈中,销毁的时候没移除等。

 

posted on 2022-12-31 15:49  左手指月  阅读(329)  评论(0编辑  收藏  举报