β

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

Harries Blog™ 31 阅读

什么是内存泄漏?

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

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

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

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

Java的内存分配机制

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

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

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

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

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

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

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.getAppli cat ionContext()),因为应用的上下文对象的生命周期和整个应用一样长。

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 mActivity;
        public MyHandler(Activity activity) {
            mActivity = new WeakReference(activity);
        }
        @Override
        public void handleMessage(Message msg) {
            if (mActivity.get() == null) {
                return;
            }
             //to do something..
        }
    };

4、WebView引起的内存泄露

关于WebView的内存泄漏,这是个绝对的大大大大大坑!不同版本都存在着不同版本的问题,这里我只能给出我平时的处理方法,可能不同机型上存在的差异,只能靠积累了。

方法一:

首先不要在xml去定义,定义一个ViewGroup就行,然后动态在代码中new WebView(Context context)(传入的Context采取弱引用),再通过addView添加到ViewGroup中,最后在页面销毁执行onDestroy()的时候把WebView移除。

方法二:

简单粗暴,直接为WebView新开辟一个进程,在结束操作的时候直接System.exit(0)结束掉进程,这里需要注意进程间的通讯,可以采取Aidl,Messager,Content Provider,Broadcast等方式。

5、Asynct ask 引起的内存泄露

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

6、资源对象未关闭引起的内存泄露

这块就比较简单了,比如我们经常使用的广播接收者, 数据库 游标 ,多媒体,文档,套接字等。

7、其他一些

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

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

作者:李晨玮

链接: https ://www.jianshu.com/p/b4f611d59ffc

PS:如果您想和业内技术大牛交流的话,请加qq群(527933790)或者关注微信公众 号(AskHarries),谢谢!

转载请注明原文出处: Harries Blog™ » Android中常见的内存泄漏和解决方案

作者:Harries Blog™
追心中的海,逐世界的梦
原文地址:Android中常见的内存泄漏和解决方案, 感谢原作者分享。

发表评论