zoukankan      html  css  js  c++  java
  • 优化之内存泄露

    什么是内存泄漏?

    简单点说,就是指一个对象不再使用,本应该被回收,但由于某些原因导致对象无法回收,仍然占用着内存,这就是内存泄漏。

    为什么会产生内存泄漏,内存泄漏会导致什么问题?

    相比C++需要手动去管理对象的创建和回收,Java有着自己的一套垃圾回收机制,它能够自动回收内存,但是它往往会因为某些原因而变得“不靠谱”。

    在Android开发中,一些不好的编码习惯就很可能会导致内存泄漏,而这些内存泄漏会导致应用内存越占越大,使得应用变得卡顿,甚至造成OOM(Out Of Memory)内存溢出问题,同时也使应用变得极其不稳定,因为当内存不足的时候,系统会优先回收那些“内存占比”大的应用。

    Java的内存分配机制

    首先我们先来了解下Java的内存分配机制,Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。

    静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

    栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

    堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。

    那什么样的对象会被回收呢?

     
    Java内存管理有向图

    为了更好理解 GC 的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被 GC 回收。

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

    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销毁的时候,对象也是无法被回收的。

    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没办法被回收。

    解决方案:
    在这里我们只需要把非静态内部类改成静态内部类即可(static class TestResource)。

    3、Handler引起的内存泄漏
    记得我们刚学习Handler的时候,网上资料甚至学校教材“教科书”式的写法都是这样的

        Handler mHandler=new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                //to do something..
                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();
        }
    

    问题所在:
    别看上面短短几行代码,其实涉及到了很多问题,首先我们知道程序启动时在主线程中会创建一个Looper对象,这个Looper里维护着一个MessageQueue消息队列,这个消息队列里会按时间顺序存放着Message,不清楚的朋友可以看下我之前写的这篇文章《从源码的角度彻底理解Android的消息处理机制》,然后上面的Handler是通过内部类来创建的,内部类会持有外部类的引用,也就是Handler持有Activity的引用,而消息队列中的消息target是指向Handler的,也就等同消息持有Handler的引用,也就是说当消息队列中的消息如果还没有处理完,这些未处理的消息(也可以理解成延迟操作)是持有Activity的引用的,此时如果关闭Activity,是没办法回收的,从而就会导致内存泄露。

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

       MyHandler myHandler = new MyHandler(this);
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
             new Thread(new Runnable() {
                @Override
                public void run() {
                    myHandler.sendMessage(Message.obtain());
                }
            }).start();
        }
    
        @Override
        protected void onDestroy() {
            super.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..
    
            }
        };
    

    4、WebView引起的内存泄露
    关于WebView的内存泄漏,这是个绝对的大大大大大坑!不同版本都存在着不同版本的问题,这里我只能给出我平时的处理方法,可能不同机型上存在的差异,只能靠积累了。
    方法一:
    首先不要在xml去定义<WebView/>,定义一个ViewGroup就行,然后动态在代码中new WebView(Context context)(传入的Context采取弱引用),再通过addView添加到ViewGroup中,最后在页面销毁执行onDestroy()的时候把WebView移除。
    方法二:
    简单粗暴,直接为WebView新开辟一个进程,在结束操作的时候直接System.exit(0)结束掉进程,这里需要注意进程间的通讯,可以采取Aidl,Messager,Content Provider,Broadcast等方式。

    5、Asynctask引起的内存泄露
    这部分和Handler比较像,其实也是因为内部类持有外部类引用,一样的改成静态内部类,然后在onDestory方法中取消任务即可。

    6、资源对象未关闭引起的内存泄露
    这块就比较简单了,比如我们经常使用的广播接收者,数据库的游标,多媒体,文档,套接字等。

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

    好了,以上就是比较常见的内存泄露原因和对应的解决方案,当然还有一些其他的,这里没有办法一一阐述,还是需要大家平时不断去积累,总结,这里提供一个可以检查内存泄露的工具LeakCanary,只需要几行代码就可以轻松在应用内集成内存监控功能了。


     =====

    Android 内存泄漏的场景以及解决方式

    最近在整理Android内存泄漏相关内容,目前整理出了以下八种情形,后期还会继续补充,请持续关注~

    单例造成的内存泄漏;
    非静态内部类创建静态实例造成的内存泄漏;
    Handler造成的内存泄漏;    线程造成的内存泄漏
    资源未关闭造成的内存泄漏; 集合容器中的内存泄漏;
    WebView造成的泄漏;

    使用ListView时造成的内存泄漏

    一、单例造成的内存泄漏

        由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。

    二、非静态内部类创建静态实例造成的内存

        例如,有时候我们可能会在启动频繁的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时都会使用该单例的数据。虽然这样避免了资源的重复重复创建,但是这种写法却会造成内存泄漏。因为非静态内部类默认会持有外部类的引用,而非静态的内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这样导致了该静态实例一直会持有该Actitity的引用,从而导致Activity的内存资源不能被正常回收。

    解决方法:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,就使用Application的Context。

    三、Handler造成的内存泄漏

    示例:创建匿名内部类的静态对象

    public class MainActivity extends AppCompatActivity {

    private final Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
    // ...
    }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    new Thread(new Runnable() {
    @Override
    public void run() {
    // ...
    handler.sendEmptyMessage(0x123);
    }
    });
    }
    }


    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类独立出来 或者 使用静态内部类,这样便可以避免内存泄漏。 

    四、线程造成的内存泄漏

    示例:AsyncTask和Runnable

    AsyncTask和Runnable都使用了匿名内部类,那么他们将持有其所在Activity的隐式引用。如果任务在Activity小会之前还未完成,那么将导致Activity的内存资源无法被回收,从而造成内存泄漏。

    解决方法:将Asynctask和Runnable类独立出来 或者 使用静态内部类,这样可以避免内存泄漏。

    五、资源未关闭造成的内存泄漏

        对于使用了BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源,应该在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层。

    六、集合容器中的内存泄漏

          一些对象的引用加入到集合(如ArrayList)中,当我们不需要该对象时,并没有把他的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。

    解决方法: 在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。

    七、WebView造成的泄漏

         当我们不要使用WebView对象时,应该调用它的deatory()函数来小会它,并释放其占用的内存,否则其长期占用内存也不能被回收,从而造成内存泄漏。

    解决方法:为WebView另外开启一个进程,通过AIDL与主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,

    从而达到内存的完整释放。
     

    八、使用ListView时造成的内存泄漏

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

        构造Adapter时,没有使用缓存的convertView。

    解决方法:在构造Adapter时,使用缓存的convertView。

  • 相关阅读:
    【Docker】11 私有仓库
    【Docker】10 容器存储
    【TypeScript】02 面向对象
    【TypeScript】01 基础入门
    【Spring】08 后续的学习补充 vol2
    【Vue】15 VueX
    【Vue】14 UI库
    【Vue】13 VueRouter Part3 路由守卫
    【Vue】12 VueRouter Part2 路由与传参
    【DataBase】SQL50 Training 50题训练
  • 原文地址:https://www.cnblogs.com/awkflf11/p/12595400.html
Copyright © 2011-2022 走看看