Android内存泄漏(一)——常见内存泄漏以及解决

转载请标明出处: http://www.weyye.me/detail/android-memory-leak-explaination/
本文出自:【Wey Ye的博客】

前言

内存泄漏也算是一个老生常谈的一个话题,在面试的时候也会经常被问到。这几天正在研究性能优化,所以我打算先从内存泄漏开始讲起

什么是内存泄漏

引用一句网上用烂的一句话,也是面试装逼必备~

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

谁管理着我们的内存?又是什么时候回收的?

做过C/C++同学都知道,都是自己去分配内存和释放内存,一切都是我们手动管理。而java则是自动管理的内存-即有自己的一套垃圾回收机制(Garbage Collection),简称gc.

Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:

  • 发现无用信息对象
  • 回收被无用对象占用的内存空间,使该空间可被程序再次使用

GC内存回收时机

当某对象不再有任何的引用的时候才会进行回收。

内存分配策略

要想搞清楚为什么会内存泄漏,我们首先要了解内存是如何进行分配的

静态存储区

内存在程序编译的时候就已经分配好,这块的内存在程序整个运行期间都一直存在。它主要存放静态数据、全局的static数据和一些常量。

栈区

当执行方法里面的代码时,方法里面的局部变量都是创建在栈上面的,
比如说基础数据类型int i=5,此时就会在栈上开辟一块区域名字是i,它的值就是5,当创建对象时候的Person p=new Person(),此时就会在堆中创建一个Person的实例,而在栈中就会开辟一块区域p,而值就是在堆中实例对应的引用地址(指针)。当函数执行结束后这些存储单元就会自动被释放掉栈内存包括分配的运算速度很快,因为内置在处理器的里面的。当然容量有限。

堆区

堆内存用来存放由new创建的对象和数组。 如new Person()。在堆中创建一个数组或者对象后,还会在栈中定义一个特殊的变量,该变量存储着这个数组或对象的内存地址(指针),也就成了数组或对象的引用变量。在堆中申请的内存全部有gc管理

堆和栈的区别

  • 堆是不连续的内存区域,堆空间比较灵活也特别大。
  • 栈式一块连续的内存区域,大小是有操作系统觉决定的。

堆管理很麻烦,频繁地new/remove会造成大量的内存碎片,这样就会慢慢导致效率低下。对于栈的话,他先进后出,进出完全不会产生碎片,运行效率高且稳定。

public class Main{
    int a = 1;//注意:此成员变量在堆区
    Person s = new Person();
    public void XXX(){
        int b = 1;//栈区
        Student s2 = new Student();
    }

}
  • 成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体)—因为他们属于类,类对象最终还是要被new出来的。
  • 局部变量的基本数据类型和引用存储于栈当中,引用的对象实体存储在堆中。—–因为他们属于方法当中的变量,生命周期会随着方法一起结束。

其实说了这么多就像说明一个问题

其实堆区它存放的就是栈中内存地址(指针)所指向的对象实例。

内存抖动

其实内存抖动就是在短时间内创建了大量的对象或者被回收的对象,gc为了内存合理使用就会进行频繁回收内存,这样就造成了内存抖动。其实gc内存回收机制也是一段代码的执行,它也会消耗cpu和内存。所以呢我们要避免内存抖动,你想它要在短短的时间内进行高工作的回收工作,这样就阻碍的性能,也是app卡顿的主要原因之一。

内存泄漏的好处 or 坏处?

好处

很遗憾的说没有好处!如果硬要说有,那就是我们不用逐一排查引起内存泄漏的原因啦~

坏处

Android系统在我们运行app的时候其实就已经规定我们使用的内存大小,如果我们的内存泄漏了,内存无法进行回收,那么大量无用内存就会一直停留在里面。运行时间一长,就会超过给我们限制的内存大小最终出现内存溢出直接Crash

内存泄露的栗子

单例模式

在我们时候单例模式的时候有时候需要传入一个Context,比如这样:

public class CommonManager {
    private static CommonManager instance;
    private  Context mContext;

    public CommonManager(Context context) {
        mContext=context;
    }

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

然而在Activity调用的时候我们习惯这样

public
 class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        CommonManager manager = CommonManager.getInstance(this);
        manager.xxx();
    }
}

如上,如果你也传入了this,那么恭喜你。。内存泄漏了!

First blood!(这是一个有声音的文字)

为什么捏??

因为现在CommonManager持有了Activity的引入,如果现在Activity的整个生命周期执行完了(调用onDestroy()后),这个Activity本该被gc回收了,可是却被CommonManager持有了,然后这个Activity就会一直存在内存中,就像路边的垃圾的一样无人问津。。。

如何解决这个内存泄漏呢?

我们之所以要用单例,很大部分原因是因为我们在整个app的运行中,能够轻松的取到里面的值或者调用极其非静态的方法。由于我们的静态类的生命周期和整个app的生命周期一样长,而Application类正好也是,而Application也继承Context,所以我们可以改用Application

public class MainActivity extends AppCompatActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        CommonManager manager = CommonManager.getInstance(getApplicationContext());
    }
}

因为Application跟静态的生命周期一样长,所以就不会产生内存泄漏啦!

静态变量

慎用静态变量

为什么说慎用呢?因为静态变量的生命周期在于从赋值开始一直到置空(null)或者app进程结束。也就是说你app所有的界面关闭了其实静态变量中的值还是存在的

那么如何进程会结束呢?

  • 如果运行内存足够,Android不会杀掉任何进程。在内存不足的时候就会杀掉或者重启进程
  • 用户手机手动清理内存

静态变量使用错误之四大组件

有时候我们会有这样的需求比如:MainActivity跳转到DetailActivity,此时我需要在DetailActivity调用MainActivity里面的方法来刷新ui。这个时候你也许会这样做:

public class MainActivity extends AppCompatActivity {
    public static MainActivity mIntance;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mIntance = this;
    }
    public void doSomething()
    {
        //...
    }
}

public class DetailActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);
        findViewById(R.id.btnCommit).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MainActivity.mIntance.doSomething();
            }
        });
    }
}

MainActivity中定义一个自己的静态变量实例mIntance,这样在DetailActivity以及任何界面都可以直接调用MainActivity中的方法。

如果你是这样做的,那么恭喜你。。内存泄漏了!

Double kill

为什么捏?

如上面所说,静态变量的生命周期从赋值开始一直到置空(null)或者app进程结束,也就是说这个MainActivity的实例一直存在得不到回收,所以产生了泄漏

怎么解决呢?

public class MainActivity extends AppCompatActivity {
    public static MainActivity mIntance;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mIntance = this;
    }
    public void doSomething()
    {
        //...
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mIntance = null;
    }
}

onDestroy置空即可。

当然我个人是不推荐这样做的。我现在是在按钮的点击事件里面去调用MainActivity的方法。那假如用户一直不点呢?那么这个MainActivity就会一直存在内存中。而Activity又是四大组件,可想而知它是个重量级的家伙。

那么有什么好的解决方法呢?

我们可以用第三方框架来解决这个问题,比如EventBus,如果你项目引入RxJava,那么你可以使用RxBus。在MainActivity注册订阅,然后在DetailActivity发消息通知MainActivity然后执行对应的方法,当然一定要在onDestroy的时候取消注册,不然还是会内存泄漏的!

静态变量使用错误之View

同样的问题,我需要在DetailActivity界面去更新MainActiviybutton的文字。

public class MainActivity extends AppCompatActivity {
    public static Button mBtn;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBtn=findViewById(R.id.btn);
    }

}

public class DetailActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);
        findViewById(R.id.btnCommit).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MainActivity.mBtn.setText("我被改变了");
            }
        });
    }
}

如果你是这样做的 Trible Kill!

同理我们只需要在onDestory置空就可以了

那么我们有了上面的经验,同样直接就可以用EventBus/RxBus搞定!

Handler泄漏

说起内存泄漏,那么一定跑不了Handler,因为我们日常写代码的时候经常会去这样写:

public class MainActivity extends AppCompatActivity {
    Handler mHandler=new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //handle message...
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mHandler.sendEmptyMessage(0);
            }
        }, 120000);
    }

}

在代码中可以看到延时2分钟给handler发送了一个空消息。看似没问题,那么如果此时用户点了返回退出了这个Activity,那么Activity2分钟内一直在内存中不能被回收,这个那么这个时候就产生泄漏

如果你代码类似这样。。 Ultra Kill!

在我的Android走进Framework之app是如何被启动的里面说过当我们app启动时候ActivityThread会创建主线程的Looper对象,来进行消息队列的循环取出消息然后处理消息。发送消息的时候Message会持有mHandler的引用而mHandler又是一个匿名Handler类,匿名类是特殊内部类,而内部类是持有外部类实例的引用的。所以此时这个Handler持有了外部Activity的引用。这样Looper对象才可以取到Message然后交给对应的Handler去处理

Handler.java

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

当我发送消息的时候最终会调用enqueueMessage,在这里讲this(Handler)赋值给了Message里面的target;此时就持有了Handler的引用了

好了 搞清楚为什么会内存泄漏后,那我们怎么解决呢?

     static  Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //handle message...
        }
    };

mHandler改成静态的,因为静态内部类是不持有外部类的引用的

那就引入了另一问题:静态类怎么调用非静态的方法呢?当然加入构造方法传进来。比如这样?

    static class MyHandler extends Handler {
        MainActivity mActivity;

        public MyHandler(MainActivity activity) {
            mActivity = activity;
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            mActivity.doSomething();
        }
    }

错!虽然变成了静态的了,但是这样做跟之前做是一样的,传入进来的都是强引用,还是一样回收不了。

那么到底该怎么做呢? 应该传入一个弱引用的Activity,如下

    static class MyHandler extends Handler {
        private final WeakReference<MainActivity> mActivity;

        public MyHandler(MainActivity activity) {
            mActivity = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            mActivity.get().doSomething();
        }
    }

这样做就可以了。但是为什么要使用WeakReference呢?

来一段4大引用的解释

StrongReference 强引用

回收时机:从不回收
使用:对象的一般保存
生命周期:JVM停止的时候才会终止
(new出来的对应都是强引用)

SoftReference 软引用
回收时机:当内存不足的时候
使用:SoftReference结合ReferenceQueue构造有效期短
生命周期:内存不足时终止

WeakReference 弱引用
回收时机:在垃圾回收的时候
使用:同软引用
生命周期:GC后终止

PhatomReference 虚引用
回收时机:在垃圾回收的时候
使用:合ReferenceQueue来跟踪对象呗垃圾回收期回收的活动
生命周期:GC后终止

当我们使用WeakReference,gc回收后那个MainActivity的引用也就不存在,那么MainActivity就可以了正常被回收了

当然这样做就完美了嘛?

No! 我们最好在onDestory移除消息,因为当你Activity退出后Message还在排队等待处理,而我们的Activity都关闭了,所以也就没必要处理了,最终如下


public class MainActivity extends AppCompatActivity {
    private MyHandler mHandler = new MyHandler(this);
    static class MyHandler extends Handler {
        private final WeakReference<MainActivity> mActivity;

        public MyHandler(MainActivity activity) {
            mActivity = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            mActivity.get().doSomething();
        }
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mHandler.sendEmptyMessage(0);//What建议定义常量,为了演示直接传0
            }
        }, 120000);
    }
    public void doSomething() {
        //todo
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeMessages(0);
    }
}

完美!

总结

看到上面几个内存泄漏的例子,在我们日常编码中也要好好注意了,例如:

  • 当Application的Context能搞定的情况下,并且生命周期长的对象,优先使用Application的Context
  • 不要生命周期长的对象引用Activity
  • 尽量避免使用静态变量
  • 内部类是持有外部类的的引用的,而静态内部类不持有
  • 如果使用静态内部类,一定要将外部实例对象作为弱引用持有
  • Handler正确使用
  • 4大引用的解释

当然,这些例子都是比较常见的内存泄漏的地方,还有不常见的泄漏,这个时候就需要我们借助工具来查看逐一排除解决。在下一篇我会讲解如何去去使用工具来查看内存泄漏

参考

Java垃圾回收机制(GC)详解

Java中堆内存和栈内存详解

博主整理不易,转载请注明出处:
http://www.weyye.me/detail/android-memory-leak-explaination/