内存泄漏分析

本文详细介绍了内存泄漏的原因,包括单例、静态变量、非静态内部类(如Handler)、未取消注册的回调、Timer和Task、未清理的集合对象、未关闭的资源、属性动画以及WebView等场景。同时,提供了相应的解决策略,帮助开发者避免和解决内存泄漏问题,实现更高效的内存管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是内存泄漏?

一个无用的对象,仍然被其他对象持有引用,造成该对象无法被系统回收,导致该对象在堆中所占有的内存单元无法被释放而造成内存空间浪费,这就是内存泄漏.

什么是堆?

https://blog.youkuaiyun.com/weixin_46269963/article/details/120010419?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2defaultCTRLISTRate-1-120010419-blog-77833808.pc_relevant_multi_platform_whitelistv4&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2defaultCTRLISTRate-1-120010419-blog-77833808.pc_relevant_multi_platform_whitelistv4&utm_relevant_index=2

堆(Heap):是应用程序在运行的时候请求操作系统分配给自己的内存,一般是申请/给予的过程,C/C++分别用malloc/New请求分配Heap,用free/delete销毁内存。由于从操作系统管理的内存分配所以在分配和销毁时都要占用时间,所以用堆的效率低的多!但是堆的好处是可以做的很大,C/C++对分配的Heap是不初始化的。

什么是栈?

栈(Stack):是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有FILO的特性,在编译的时候可以指定需要的Stack的大小。

堆栈(heap and stack)的概念在计算机领域中不容忽视。虽然堆栈常被连着念,堆与栈是两个相对的概念:堆指先进先出(first in first out),栈指先进后出(first in last out) ;

引起内存泄漏的几种场景以及解决方式

1.单例导致内存泄漏

单例的静态特性使得ta的生命周期和应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有ta的引用,那么在整个应用的生命周期中ta都不能被正常的回收,从而导致内存泄漏.

public class AppSettings {

    private static AppSettings sInstance;
    private Context mContext;

    private AppSettings(Context context) {
        this.mContext = context;
    }

    public static AppSettings getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new AppSettings(context);
        }
        return sInstance;
    }
}

像上面的单例,在调用getInstance 方法时,如果传入的是Activity、Service这类上下文,就会导致内存泄漏!
例如传入的上下文是Activity,那么上述单例类就持有了Activity的引用,当我们退出Activity页面时,这个Activity就不在使用了,但是因为单例类作为静态单例,它还是继续持有这个Activity的引用,导致这个Activity无法被系统回收释放,这就造成了内存泄漏;

修复方案

避免这样的单例导致内存泄漏,将上下文改为全局的上下文;

private AppSettings(Context context) {
    this.mContext = context.getApplicationContext();
}

全局的上下文和单例的生命周期一样长,这就避免了内存泄漏。

静态变量导致内存泄漏

静态变量储存在方法去,ta的生命周期从类加载就开始,到整个进程结束。一旦静态变量初始化后,ta持有的引用只能等到进程结束后才会被释放。

public class MainActivity extends AppCompatActivity {

    private static Info sInfo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (sInfo != null) {
            sInfo = new Info(this);
        }
    }
}

class Info {
    public Info(Activity activity) {
    }
}

sInfo作为静态成员变量,并且ta还持有了Activity的引用,但是作为静态变量,ta的生命周期肯定比Activity还长,但Activity退出后,sInfo还持有了Activity的引用,Activity不能被释放,这就导致了内存泄漏。

解决方案

在开发过程中,静态持有很多时候都有可能因为使用的生命周期不一致导致内存泄漏,所以我们在新建静态持有的变量时要多考虑各个成员之间的引用关系!尽量少的使用静态持有的变量;
在适当的时候将静态变量重置为null,使其不在持有,这也可以避免内存泄漏。

非静态内部类导致内存泄漏

要清晰的记住一点:非静态内部类包括匿名的内部类,默认是持有了外部的引用,单非静态内部类的生命周期比外部类对象的生命周期长时,就会导致内存泄漏。

案例1.Handler

常用到的场景是使用Handler,很多人是这样使用Handler的:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        start();
    }

    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 1) {
                // 做相应逻辑
            }
        }
    };
}

Handler 的消息机制是这样的,handler会作为成员变量保存到msg中,msg持有handler的引用;而handler是Activity的非静态内部类实例,handler持有了Activity的引用,也就是说msg间接持有了handler的引用。msg被发送后放在消息队列MessageQueue中,等待Looper轮询处理;
当Activity退出后,msg可能仍然存在于消息队列MessageQueue中未处理或者正在被处理,这样就会导致Activity无法被回收,导致发生Activity的内存泄漏。

解决方案

在Android开发中如果要使用内部类,但要避免内存泄漏,一般采用静态内部类+弱引用的方式。

public class MainActivity extends AppCompatActivity {

    private Handler mHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandler = new MyHandler(this);
        start();
    }

    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
    }

    private static class MyHandler extends Handler {

        private WeakReference<MainActivity> activityWeakReference;

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

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = activityWeakReference.get();
            if (activity != null) {
                if (msg.what == 1) {
                    // 做相应逻辑
                }
            }
        }
    }
}

hander 通过弱引用来持有Activity,但GC执行回收时,遇到Activity就会释放;这样就不会导致内存泄漏了。

上面的方式,存在一个问题,发送的msg不在持有activity的引用,但是msg可能还在消息队列中,所以更好的方式是在Activity销毁是就将handler的回调和发送的消息给移除掉。

@Override
protected void onDestroy() {
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);
}

案例2 Thread,AsyncTask

在Activity中直接new一个Thread

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 模拟相应耗时逻辑
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

或者直接开启一个AsyncTask

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                // 模拟相应耗时逻辑
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return null;
            }
        }.execute();
    }
}

上面新建的Thread和AsyncTask都是匿名的内部类对象,默认隐式的持有了外部Activity的引用,导致内存泄漏;
要避免内存泄漏,可以像Handler一样,静态内部类+弱引用的方式。

未取消注册或者回调导致内存泄漏

在Activity注册广播时,如果Activity销毁后不取消注册,这个广播会一直存在系统中,同上面的非静态内部类一样持有Activity的引用,导致内存泄漏。因此注册广播后在Activity销毁后一定要取消注册。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.registerReceiver(mReceiver, new IntentFilter());
    }

    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            // 接收到广播需要做的逻辑
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        this.unregisterReceiver(mReceiver);
    }
}

在注册观察者模式时,如果不及时取消也会造成内存泄漏。比如Retrofit+RxJava注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,需要记得在不用或者销毁的时候取消注册。

Timer和Task导致内存泄漏

比较典型的用法,存在匿名内部类持有外部引用的场景;

解决方案

在Activity销毁时一定要cancle掉Timer和TimerTask!

集中的对象未清理造成内存泄漏

这个比较好理解,如果一个对象放入到ArrayList、HashMap等集合中,这个集合就会持有该对象的引用。当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用(而此对象已经无用了),这个对象就造成了内存泄露。并且如果集合被静态引用的话,集合里面那些没有用的对象更会造成内存泄露了。所以在使用集合时要及时将不用的对象从集合remove,或者clear集合,以避免内存泄漏。

资源未关闭或释放导致内存泄露

在使用IO、File流或者Sqlite、Cursor等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果及时不关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。因此我们在不需要使用它们的时候就及时关闭,以便缓冲能及时得到释放,从而避免内存泄露。

属性动画造成内存泄露

动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏。

@Override
protected void onDestroy() {
    super.onDestroy();
    mAnimator.cancel();
}

WebView造成内存泄露

关于WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。

另外在查阅WebView内存泄露相关资料时看到这种情况:

Webview下面的Callback持有Activity引用,造成Webview内存无法释放,即使是调用了Webview.destory()等方法都无法解决问题(Android5.1之后)。

解决方案

最终的解决方案是:在销毁WebView之前需要先将WebView从父容器中移除,然后在销毁WebView。
具体可参考:

https://blog.youkuaiyun.com/xygy8860/article/details/53334476

@Override
protected void onDestroy() {
super.onDestroy();
// 先从父控件中移除WebView
mWebViewContainer.removeView(mWebView);
mWebView.stopLoading();
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.removeAllViews();
mWebView.destroy();
}

总结

内存泄露在Android内存优化是一个比较重要的一个方面,很多时候程序中发生了内存泄露我们不一定就能注意到,所有在编码的过程要养成良好的习惯。总结下来只要做到以下这几点就能避免大多数情况的内存泄漏:

1.构造单例的时候尽量别用Activity的引用;
2.静态引用时注意应用对象的置空或者少用静态引用;
3.使用静态内部类+软引用代替非静态内部类;
4.及时取消广播或者观察者注册;
5.耗时任务、属性动画在Activity销毁时记得cancel;文件流、Cursor等资源及时关闭;
6.Activity销毁时WebView的移除和销毁.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值