Android内存泄露详解

作者: alighters | 来源:发表于2015-12-22 10:37 被阅读1048次

    内存泄露

    在开发应用的过程中,我们总会遇到内存泄露的问题。现在通过代码列出一些常见的内存泄露的情况以及解决方案。

    在安卓中内存泄露常常出现的情况是指组件生命周期已经结束,但是其引用被其他对象持有,得不到释放引起的。常见的内存泄露的情况,主要是有两种:内部类和静态引用的问题。

    内部类

    内部类的种类

    • 成员内部类
    • 局部内部类
    • 匿名内部类
    • 静态内部类

    非静态内部类的问题

    问题:非静态内部类会持有其外部类的引用。而外部类则不会有这个问题,所以我们在使用内部类的时候,要慎重考虑,尽量使用静态内部类。

    PS:在EffectiveJava一书中(第四章第22条:优先考虑静态成员类,P94),作者将这四种情况统一称为嵌套类,非静态内部类(除静态内部类之外的三种)统称为内部类

    代码准备

    • LeakCanary Square出的用来记录内存泄露的工具,非常好用
    • Logger 可以详细地打印出Log信息,包括线程信息,方便查看

    常见耗时类泄露

    public class ThreadActivity extends BaseActivity {
    
        @Override
        protected void onCreate (Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            showThreadInfo();
        }
    
        int i = 0;
    
        private void showThreadInfo() {
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        while (true) {
                            Thread.sleep(1000);
                            Logger.d(i++ + "");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                }
            }).start();
        }
    }
    

    在上面的代码中,当TreadActivity的生命周期结束,就会出现内存泄露。这里,主要匿名内部类new Runnable的使用,这是一个耗时的线程操作,它会持有外部类ThreadActivity的使用。当退出TreadActivity时,因Runnable持有其引用,导致其不能释放,造成内存泄露。注意,这里的new Thread并不是一个匿名内部类,仅仅是一个变量而已,Thread类并没有在TreadActivity中定义。

    现在,针对这个ThreadActivity做一些改进。代码如下:

    public class ThreadActivity extends BaseActivity {
        
        @Override
        protected void onCreate (Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            showWeakThreadInfo();
        }
    
        int i = 0;
    
        private void showWeakThreadInfo() {
    
            new MyThread(new Runnable() {
                @Override
                public void run() {
                    try {
                        while (true) {
                            Thread.sleep(1000);
                            Logger.d(i++ + "");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    
        static class MyThread extends Thread {
            WeakReference<Runnable> mWeakReference;
    
            public MyThread(Runnable runnable) {
                mWeakReference = new WeakReference<>(runnable);
            }
    
            @Override
            public void run() {
                super.run();
                if (mWeakReference.get() != null) {
                    mWeakReference.get().run();
                }
            }
        }
    }
    

    这里,主要做的一个改进是,在MyThread中,持有一个Runable的弱引用,这样,MyThread的执行,就不会对Activity的执行产生什么影响了。但是,在使用Demo的过程中,还是会出现内存泄露的问题。为什么呢?原因在new MyThread的过程中,穿进去的参数new Runable还是持有ThreadActivity的引用,这种写法真的是然并卵

    Weak CallBack解决弱引用问题

    主要思想:就是把上面的耗时操作Thread相关代码,提取放置到一个新的类中,进一步弱化跟Activity的引用。代码如下:

    public class WeakTask {
    
        public void excute(CallBack callback) {
            new WeakThread(callback).start();
        }
    
        private static class WeakThread extends Thread {
            public WeakThread(CallBack callback) {
                this.call = new WeakReference<>(callback);
            }
    
    
            private WeakReference<CallBack> call;
    
    
            @Override
            public void run() {
                super.run();
                try {
                    int i = 0;
                    if (call.get() != null) {
                        call.get().onStart(i + "");
                    }
                    while (true) {
                        Thread.sleep(1000);
    
                        if (call.get() != null) {
                            call.get().onExcute(i + "");
                        } else {
                            Logger.d("Thread内部:" + i);
                        }
                        i++;
    
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    其中,使用到的CallBack仅仅做一个回调,代码如下:

    public interface CallBack {
    
        void onStart(String msg);
    
        void onExcute(String msg);
    
        void onSucces(String msg);
    
    }
    

    然后,调用实现的代码就比较简单了:

    public class WeakCallbackActivity extends BaseActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setTitle(R.string.leak_weak_callback);
            execute();
        }
    
        private void execute() {
            Logger.d("execute");
            new WeakTask().excute(new CallBack() {
    
                @Override
                public void onStart(String msg) {
                    Logger.d(msg);
                }
    
                @Override
                public void onExcute(String msg) {
                    Logger.d(msg);
                }
    
                @Override
                public void onSucces(String msg) {
                    Logger.d(msg);
                }
            });
        }
    }
    

    以上代码所做的主要思想就是在耗时操作类中,持有的回调仅仅是一个弱引用,这样在Activity界面类中传入的CallBack匿名类因为弱引用是可以及时被回收掉的,就避免了Activity的泄露。

    Handler引起的内存泄露

    提到Handler,我们知道在通过Handler的post以及postDelayed方法会往对应的Looper的MessageQueue中插入一条Message,而Message中的target就是指向当前的handler。这样若是handler使用的是内部类,则会持有外部类的引用。而当这条Message还没被执行,则会导致咱们的外部类不能得到释放。所以,我们在使用Handler的时候,要谨慎一些,尽量使用静态Handler,然后持有Context的弱引用。具体详解可见:Android中Handler引起的内存泄露

    静态引用

    另一种,常常出现内存泄露的问题就是静态应用了。若是当前的Activity,被一个静态变量持有,会导致当前的Activity不能及时的释放,这样就会导致内存泄露。代码如下:

    public class StaticReferenceActivity extends BaseActivity {
    
        private static Context mContext;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setTitle(R.string.leak_static_reference);
            mContext = this;
        }
    }
    

    这里举得例子是比较简单地,当前的StaticReferenceActivity被静态变量mContext所持有,这就要求我们在当前组件生命周期结束时(OnDestory回调)及时释放静态变量所持有的对象。这种常见的情况就是:在做一些静态初始化的过程中,会持有context的引用,譬如好多第三库都要求我们持有的是ApplicationContext的引用。还有就是我们给第三方库或者不知道其内部实现的代码,需要我们对其传入Context对象,这里我们就得小心一二了。

    最后,这里使用到得弱引用并不是万能的,它也有自己的问题,若是当一次GC之后,它就会被回收掉,会导致我们的回调收不到。所以我们需要对使用到的耗时操作时间长短,确定使用WeakReference还是SoftWeakRefernce或者强引用做一些筛选判定。

    PS:若是还有疑问,则可以针对代码进行一些修改来验证自己的猜想。

    相关文章

      网友评论

      • ca63366e0798:方法 2,3 的区别仅仅是将类的定义置于 外部,这样做的原因是什么,从哪个方面说 弱化了
      • krmao:问题来了
        setOnClickLisener(new View.OnClickListener(){
        )
        等诸如此类的 回调函数,也属于匿名内部类,也会包含强引用,会造成泄漏吗?
        alighters:@mkrcpp 匿名内部类是没问题的。出现问题的时候是因为匿名内部类中做了耗时的操作,导致其对外部的引用不能及时被回收,而导致内存泄露。
        krmao:@alighters 是啊,我查了很久,显有人提及这个,如果这个都可能存在泄漏,代码实在太难看了,
        不是很清你说的轻量级与重量级的区别,重点是他持有Activity的强引用吧?这个问了不少人,还没有一个满意的答案 :)
        是否不推荐用匿名类了?
        alighters:@mkrcpp 是匿名内部类,但一般情况下,这些Listener都是轻量级的操作,但若是耗时操作,则就会有问题了。。
      • ca63366e0798:方式2 同样也是持有runnable的弱引用,为什么会造成内存泄露
        陌痕丶:@alighters 第二种, 直接把runnable 写成static 不就行了.runnable不持有activity引用. 那么自然就没泄露什么事了..
        alighters:@nimda_ 这里的弱引用是Thread持有runnable的弱引用,而在Activity中初始化这个线程的时候,传给Thread的参数匿名Runnable类则会持有Activity的引用,而这个Runnable是一直在运行的,所以还是导致activity的泄露
      • 我是Asha:感觉还是别依靠弱引用,比如第一个例子,关闭activity了,理应先关掉thread再关闭activity
        alighters:@我是Asha 嗯,明白。这里主要用来演示内存泄露。方便leakcanary的查看

      本文标题:Android内存泄露详解

      本文链接:https://www.haomeiwen.com/subject/mebmhttx.html