Android---内存泄漏检测核心原理

news2024/9/23 11:26:57

目录

LeakCanary 核心原理

LeakCanary 检测对象的类型

ReferenceQueue 与 WeakReference

LeakCanary 里的监控列表与保留列表

常见内存泄漏案例

1. 单例导致内存泄漏

2. 静态变量导致内存泄漏

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

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

5. Timer 和 TimerTask 导致内存泄漏

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

7. 资源未关闭或释放导致内存泄漏

8. 属性动画造成内存泄漏

9. WebView 造成内存泄漏


LeakCanary

LeakCanary 是 Square 公司的一个开源库。通过它可以在 App 运行过程中检测内存泄漏,当内存泄漏发生时会生成发生泄漏对象的引用链,并通知程序开发人员。

LeakCanary 执行流程

 \bullet 检测保留的对象

 \bullet 生成堆转储文件(heap dump)

 \bullet 分析堆转储文件 

 \bullet 对泄漏进行分类

LeakCanary 核心原理

LeakCanary 通过 hook Android 的生命周期来自动检测 Activity 和 Fragment 何时被销毁,何时应该被垃圾回收,这些被 destory 的对象被传递给 ObjectWatcher, ObjectWatcher 持有对它们的弱引用

LeakCanary 检测对象的类型

 \bullet 已销毁的 Activity 实例

 \bullet 已销毁的 Fragment 实例

 \bullet 已销毁的 Fragment View 实例

 \bullet 已清除的 ViewModel 实例

ReferenceQueue 与 WeakReference

弱引用(WeakReference)可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收器回收,虚拟机就会把这个弱引用加入到与之关联的引用队列中,我们可以用此特性来检测一个对象是否被垃圾回收器回收成功。 

 (在上图中,首先创建弱引用WeakReference,给它关联一个ReferenceQueue,对于一个弱引用如果要引用它需要一个强引用obj来引用这个对象Object。当我们把 obj 置为空,GC 就来回收。当弱引用所引用的对象被回收后,这个弱引用就被加入到与之关联的 ReferenceQueue 里)

LeakCanary 里的监控列表与保留列表

 弱引用的流转过程

 

常见内存泄漏案例

1. 单例导致内存泄漏

单例模式在 Android 开发中会经常用到,但是如果使用不当就会导致内存泄漏。因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么整个应用程序的生命周期都不能正常被回收,从而导致内存泄漏。

public class AppSettings {

    private static volatile AppSettings mInstance;
    private Context context;

    public AppSettings(Context context) {
        this.context = context;
    }

    public static AppSettings getInstance(Context context) {
        if (mInstance == null) {
            synchronized (AppSettings.class){
                if (mInstance == null) {
                    mInstance = new AppSettings(context);
                }
            }
        }
        return mInstance;
    }
}

如上述代码中的单例写法,就会存在内存泄漏的可能。如果我们在调用 getInstance(Context context) 方法的时候传入的 context 参数是 Activity、Service 等上下文时,就会导致内存泄漏。以 Activity 为例,当我们启动一个 Activity,并调用 getInstance(Context context) 方法去获取 AppSettings 的单例,传入 Activity.this 作为 context,这样 AppSettings 类的单例 mInstance 就持有了 Activity 的引用,当我们退出 Activity 时,该 Activity 就没有用了,但是因为 mInstance 作为静态单例(在应用程序的整个生命周期中存在)会继续持有这个 Activity 的引用,导致这个 Activity 对象无法被回收释放,这就造成了内存泄漏。

为了避免这种单例导致内存泄漏,我们可以将 context 参数改为全局上下文:

    private AppSettings(Context context){
        this.context = context.getApplicationContext(); // 获取 Application 的上下文
    }

2. 静态变量导致内存泄漏

静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。比如下面这样的情况,在 Activity 中为了避免重复的创建 info,将 sInfo 作为静态变量。

public class InfoActivity extends AppCompatActivity {

    public static Info sInfo;

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

        sInfo = new Info(this);
    }

    class Info{
        private Context context;

        public Info(Context context){
            this.context = context;
        }
    }
}

Info 作为 Activity 的静态成员,并且持有 Activity 的引用,但是 sInfo 作为静态变量,生命周期肯定比 Activity 长。所以当 Activity 退出后,sInfo 仍然引用了 Activity,Activity 不能被回收,这就导致了内存泄漏。

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

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

非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄漏。非静态内部类导致的内存泄漏在 Android 开发中有一种典型的场景就是使用 Handler(是一个匿名内部类),很多开发者在使用 Handler 时是这样写的:

public class InfoActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_info);

        start();
    }

    private void start(){
        Message message = Message.obtain();
        message.what = 1;
        mHandler .sendMessage(message);
    }
    
    //TODO 存在内存泄漏
    private final Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            if (msg.what == 1) {
                //功能代码
            }
        }
    };
}

也许有人会说,mHandler 并未作为静态变量持有 Activity 引用,生命周期可能不会比 Activity 长,应该不一定会导致内存泄漏。显然不是这样的,熟悉 Handler 消息机制的都知道, mHandler 会作为成员变量保存在发送的消息 msg 中,即 msg 持有 mHandler 的引用,而 mHandler 是 Activity 的非静态内部类实例,即 mHandler 持有 Activity 的引用,那么我们就可以理解为 msg 间接持有 Activity 的引用。msg 被发送后先放到消息队列 MessageQueue 中,然后等到 Looper 的轮询处理(MessageQueue 和 Looper 都是与线程相关的,MessageQueue 是 Looper 引用的成员变量,而 Looper 是保存在 ThreadLocal 中的)。那么当 Activity 退出后,msg 可能仍然存在于消息队列 MessageQueue 中未处理或正在处理,那么这样就会导致 Activity 无法被回收,以至发送 Activity 的内存泄漏。

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

    MyHandler mHandler;

    public static class MyHandler extends Handler{
        private WeakReference<Activity> mActivityWeakReference;

        public MyHandler(Activity activity){
            // TODO 持有 Activity 的弱引用
            mActivityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            // 处理逻辑
        }
    }

mHandler 通过弱引用的方式持有 Activity,当 GC 执行垃圾回收时,遇到 Activity 就会回收并释放所占据的内存单元。这样就不会发生内存泄漏了。上面的做法确实避免了 Activity 导致的内存泄漏,发送的 msg 不再持有 Activity 的引用了,但是 msg 还是有可能存在消息队列 MessageQueue 中,所以更好的是在 Activity 销毁时就将 mHandler 的回调和发送的消息给移除掉

 @Override
    protected void onDestroy() {
        super.onDestroy();
        // TODO 把所有的回调都移除
        mHandler.removeCallbacksAndMessages(null);
    }

非静态内部类造成内存泄漏还有一种情况就是使用 Thread 或者 AsyncTask。要避免内存泄漏的话还是需要像上面 Handler 一样使用静态内部类 + 弱引用的方式。 

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

比如我们在 Activity 中注册广播,如果在 Activity 销毁后不取消注册,那么这个广播会一直存在系统中,同上面所说的静态内部类一样持有 Activity 引用,导致内存泄漏。因此注册广播后在 Activity 销毁后一定要取消注册。在注册观察者模式的时候,如果不及时取消也会造成内存泄漏。比如使用 Retrofit + RxJava 注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,所以需要记得在不用或者销毁的时候取消注册。

5. Timer 和 TimerTask 导致内存泄漏

Timer 和 TimerTask 在 Android 中通常会被用来做一些计时或循环任务,比如实现无限轮播的 ViewPager:

private void stopTimer(){
        if (mTimer != null) {
            mTimer.cancel();
            mTimer.purge();
            mTimer = null;
        }
        if (mTimerTask != null) {
            mTimerTask.cancel();
            mTimerTask = null;
        }
    }

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

当我们 Activity 销毁时,有可能 Timer 还在继续等待执行 TimerTask,它持有 Activity 的引用不能被回收,因此当我们 Activity 销毁的时候要立即 cancel 掉 Timer 和 TimerTask,以避免发生内存泄漏。

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

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

7. 资源未关闭或释放导致内存泄漏

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

8. 属性动画造成内存泄漏

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

9. WebView 造成内存泄漏

关于 WebView 的内存泄漏,因为 WebView 在加载网页后会长期占用内存而不能被释放,因此我们在 Activity 销毁后要调用它的 destory() 方法来销毁它以释放内存。另外在查阅 WebView 内存泄漏相关资料时看到这种情况:WebView 下面的 Callback 持有 Activity 引用,造成 WebView 内存无法释放,即使是调用了 Webview.destory() 等方法都无法解决问题(Android5.1 之后)。最终的解决方案是:在销毁 WebView 之前需要先将 WebView 从父容器中移除,然后再销毁 WebView

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/431302.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

paddleocr 实操笔记 (前向后梳理)

要点&#xff1a; 参考&#xff1a; 基于PaddleOCR的数字显示器字符识别 工业仪表数值识别 前言 问题分析 要处理电表中的数据&#xff0c;可以分为步骤&#xff0c;拆解为以下问题&#xff1a; 感兴趣区域定位问题OCR读数问题 针对问题1,经过实验与探索&#xff0c;也找到…

黑马---Redis入门到实战【基础篇】

一、初识Redis 认识NoSql redis是键值数据库&#xff0c;没有表、没有约束&#xff0c;存的都是键值对&#xff0c;称为NoSql数据 NoSQL VS SQL 认识Redis Redis诞生于2009年&#xff0c;全称Remote Dictionary Server&#xff0c;远程词典服务器&#xff0c;是一个基于内存…

开篇:为什么学习 Go 语言

简介 Go 语言又称 Golang&#xff0c;由 Google 公司于 2009 年发布&#xff0c;近几年伴随着云计算、微服务、分布式的发展而迅速崛起&#xff0c;跻身主流编程语言之列&#xff0c;和 Java 类似&#xff0c;它是一门静态的、强类型的、编译型编程语言&#xff0c;为并发而生…

高等数学笔记(上下)

目录 不定积分定积分微分方程线性微分方程解的结构常系数齐次线性微分方程常系数齐次线性微分方程特解的求法 计算机解法 不定积分 第一类换元积分法&#xff1a;灵感来自于复合函数的求导&#xff0c;利用中间变量替换得到复合函数的积分法&#xff1a;设 f ( u ) f(u) f(u)具…

【Redis7】Redis7 事务管道发布订阅

【大家好&#xff0c;我是爱干饭的猿&#xff0c;本文重点介绍Redis7 事务、管道和发布订阅。 后续会继续分享Redis7和其他重要知识点总结&#xff0c;如果喜欢这篇文章&#xff0c;点个赞&#x1f44d;&#xff0c;关注一下吧】 上一篇文章&#xff1a;《【Redis7】Redis7 持…

Qt扫盲-QXYSeries理论总结

QXYSeries理论总结 一、概述二、常用函数介绍1. 维护点2. 绘图相关3. 绘制标签 三、信号说明1. Point 增删相关2. Point 鼠标相关3. 图变化相关 一、概述 QXYSeries 类是折线图、曲线图、散点图的基类。这个类其实就是维护的是图线的相关信息&#xff0c;就比如是这个线条的颜…

5、cmake的简单认识及CMakeLists.txt的编写语法

文章目录 1、cmake是什么&#xff08;1&#xff09;cmake的两大功能 2、CMakeLists.txt常用命令&#xff08;即如何编写CMakeLists.txt文件&#xff09;&#xff08;1&#xff09;cmake常见预定义1&#xff09;常见的预定义变量2&#xff09;系统信息预定义变量3&#xff09;开…

开发者笑疯了! LLaMa惊天泄露引爆ChatGPT平替狂潮,开源LLM领域变天

来源: 新智源 微信号&#xff1a;AI-era Meta的LLaMA模型开源&#xff0c;让文本大模型迎来了Stable Diffustion时刻。谁都没想 谁能想到&#xff0c;一次意外的LLaMA泄漏&#xff0c;竟点燃了开源LLM领域最大的创新火花。 一系列表现出色的ChatGPT开源替代品——「羊驼家族」…

Linux系统网络传输之端口详解

Linux系统网络传输之端口详解 1、端口的概念2、安装namp3、查看端口占用情况4、安装net-tools5、查看指定端口占用情况 1、端口的概念 计算机程序之间的通讯&#xff0c;通过IP只能锁定计算机&#xff0c;但是无法锁定具体的程序。通过端口可以锁定计算机上具体的程序&#xf…

《花雕学AI》深度测试ChatGPT国内镜像站:超简单提示词的猫娘角色扮演,真的好神奇啊!

偶然看过一篇讲解如何使用ChatGPT调教猫娘的文章&#xff0c;有六个步骤&#xff0c;许许多多的调教提示语&#xff0c;让我感觉这是一件非常复杂与专业的事情。今天有空&#xff0c;于是就想先从简单的开始尝试一下。我使用了最简单的提示词&#xff1a;”ChatGPT角色扮演猫娘…

分布式事务Seata实践入门

1 前言 现在应用基本上都是分布式部署&#xff0c;那么针对分布式事务问题&#xff0c;也有对应的解决方案。经过简单的调研&#xff0c;最后选择了阿里的 Seata 组件&#xff0c;来实现分布式事务。 Seata是2019年1月份&#xff0c;蚂蚁金服和阿里巴巴共同开源的分布式事务解…

(TinkSystem SR650)安装服务器操作系统(Windows Server 2022)步骤和相关概念

&#xff08;TinkSystem SR650&#xff09;安装服务器操作系统&#xff08;Windows Server 2022&#xff09;步骤和相关概念 服务器操作系统安装步骤 记录一下服务器操作系统安装过程&#xff0c;虽然简单但还是有一些坑需要注意&#xff0c;本次使用的是联想服务器ThinkSyst…

VS2019中Ctrl+左键不起作用和控制台不驻留(cmd窗口闪退)

1、关于Ctrl左键不起作用 解决&#xff1a;在线装个插件就行。 工具>>扩展和更新 联机>>VS库>>所搜插件“GO TO Definition” 安装&#xff0c;重启VS即可。 2、关于控制台不驻留 当然这个问题解决方案有很多&#xff0c;这个是首选吧

[Java·算法·中等]LeetCode105. 从前序与中序遍历序列构造二叉树

每天一题&#xff0c;防止痴呆 前言题目示例分析思路1题解1分析思路2题解2 &#x1f449;️ 力扣原文 前言 二叉树前序遍历的顺序为&#xff1a; 先遍历根节点&#xff1b; 随后递归地遍历左子树&#xff1b; 最后递归地遍历右子树。 二叉树中序遍历的顺序为&#xff1a; 先递…

HCIP-6.9BGP路由反射器原理与配置

路由反射器原理与配置 1、路由反射器概念1.1、路由反射器原理&#xff1a;1.2、多集群路由反射器1.3、备份路由反射器2、路由反射器配置3、路由反射器防环机制 1、路由反射器概念 IBGP的水平分割&#xff0c;IBGP 1只能update一跳&#xff0c;就是说在IBGP 2 设备收到IBGP 1设…

【RocketMQ】事务的实现原理

事务的使用 RocketMQ事务的使用场景 单体架构下的事务 在单体系统的开发过程中&#xff0c;假如某个场景下需要对数据库的多张表进行操作&#xff0c;为了保证数据的一致性&#xff0c;一般会使用事务&#xff0c;将所有的操作全部提交或者在出错的时候全部回滚。以创建订单…

12-RabbitMQ

一 RabbitMQ概念 1 MQ 消息队列 MQ全称Message Queue&#xff08;消息队列&#xff09;&#xff0c;是在消息的传输过程中保存消息的容器。多用于系统之间的异步通信。 同步通信相当于两个人当面对话&#xff0c;你一言我一语。必须及时回复 异步通信相当于通过第三方转述对…

ObjectBox一种基于中心点的无锚点目标检测方法

ObjectBox: From Centers to Boxes for Anchor-Free Object Detection 论文地址&#xff1a;https://arxiv.org/pdf/2207.06985.pdf 官方代码&#xff1a;https://github.com/MohsenZand/ObjectBox 基于中心点的无锚点目标检测方法是一种目标检测方法&#xff0c;其思路是将目…

DJ编曲用什么软件,DJ编曲教需要哪些步骤

随着现在人们的生活水平不断提高&#xff0c;我们的精神生活也越来越丰富&#xff0c;对于现在的年轻人来说&#xff0c;DJ舞曲是一个较受欢迎的领域&#xff0c;有许多年轻人对DJ这个职业感兴趣&#xff0c;想要深入了解DJ编曲这份工作&#xff0c;那么今天我们就来说一说DJ编…

300元买什么蓝牙耳机性价比高?300左右性价比高的蓝牙耳机推荐

TWS耳机已经成了很多人生活的必需品&#xff0c;如今的耳机在设计、功能、体验等方面都非常完善&#xff0c;拥有一副TWS耳机似乎已经成为时尚的标志&#xff0c;尤其是年轻群体&#xff0c;耳机既是听歌、娱乐的主力设备&#xff0c;也是穿搭风格、个性的体现&#xff0c;下面…