Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Android 关于内存泄露,你必须了解的东西

Android 关于内存泄露,你必须了解的东西

作者头像
developerHaoz
发布于 2018-08-20 07:33:01
发布于 2018-08-20 07:33:01
1.2K00
代码可运行
举报
运行总次数:0
代码可运行

前言

内存管理的目的就是让我们在开发过程中有效避免我们的应用程序出现内存泄露的问题。内存泄露相信大家都不陌生,我们可以这样理解:「没有用的对象无法回收的现象就是内存泄露」。

如果程序发生了内存泄露,则会带来以下这些问题

  • 应用可用的内存减少,增加了堆内存的压力
  • 降低了应用的性能,比如会触发更频繁的 GC
  • 严重的时候可能会导致内存溢出错误,即 OOM Error

OOM 发生在,当我们尝试进行创建对象,但是堆内存无法通过 GC 释放足够的空间,堆内存也无法再继续增长,从而完成对象创建请求的时候,OOM 发生很有可能是内存泄露导致的,但并非所有的 OOM 都是由内存泄露引起的,内存泄露也并不一定引起 OOM。

一、基础准备


如果真的想比较清楚的了解内存泄露的话,对于 Java 的内存管理以及引用类型有一个清晰的认识是必不可少的。

  • 理解 Java 的内存管理能让我们更深一层地了解 Java 虚拟机是怎样使用内存的,一旦出现内存泄露,我们也能更加从容地排查问题。
  • 了解 Java 的引用类型,能让我们更加理解内存泄露出现的原因,以及常见的解决方法。

具体的内容,可以看下这篇文章 你真的懂 Java 的内存管理和引用类型吗?

二、Android 中内存泄露的常见场景 & 解决方案


1、单例造成的内存泄露

单例模式是非常常用的设计模式,使用单例模式的类,只会产生一个对象,这个对象看起来像是一直占用着内存,但这并不意味着就是浪费了内存,内存本来就是拿来装东西的,只要这个对象一直都被高效的利用就不能叫做泄露。

但是过多的单例会让内存占用过多,而且单例模式由于其 静态特性,其生命周期 = 应用程序的生命周期,不正确地使用单例模式也会造成内存泄露。

举个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SingleInstanceTest {

    private static SingleInstanceTest sInstance;
    private Context mContext;

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

    public static SingleInstanceTest newInstance(Context context){
        if(sInstance == null){
            sInstance = new SingleInstanceTest(context);
        }
        return sInstance;
    }
}

上面是一个比较简单的单例模式用法,需要外部传入一个 Context 来获取该类的实例,如果此时传入的 Context 是 Activity 的话,此时单例就有持有该 Activity 的强引用(直到整个应用生命周期结束)。这样的话,即使该 Activity 退出,该 Activity 的内存也不会被回收,这样就造成了内存泄露,特别是一些比较大的 Activity,甚至还会导致 OOM(Out Of Memory)。

解决方法:单例模式引用的对象的生命周期 = 应用生命周期

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SingleInstanceTest {

    private static SingleInstanceTest sInstance;
    private Context mContext;

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

    public static SingleInstanceTest newInstance(Context context){
        if(sInstance == null){
            sInstance = new SingleInstanceTest(context);
        }
        return sInstance;
    }
}

可以看到在 SingleInstanceTest 的构造函数中,将 context.getApplicationContext() 赋值给 mContext,此时单例引用的对象是 Application,而 Application 的生命周期本来就跟应用程序是一样的,也就不存在内存泄露。

这里再拓展一点,很多时候我们在需要用到 Activity 或者 Context 的地方,会直接将 Activity 的实例作为参数传给对应的类,就像这样:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Sample {
    
    private Context mContext;
    
    public Sample(Context context){
        this.mContext = context;
    }

    public Context getContext() {
        return mContext;
    }
}

// 外部调用
Sample sample = new Sample(MainActivity.this);

这种情况如果不注意的话,很容易就会造成内存泄露,比较好的写法是使用弱引用(WeakReference)来进行改进。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Sample {

    private WeakReference<Context> mWeakReference;

    public Sample(Context context){
        this.mWeakReference = new WeakReference<>(context);
    }

    public Context getContext() {
        if(mWeakReference.get() != null){
            return mWeakReference.get();
        }
        return null;
    }
}

// 外部调用
Sample sample = new Sample(MainActivity.this);

被弱引用关联的对象只能存活到下一次垃圾回收之前,也就是说即使 Sample 持有 Activity 的引用,但由于 GC 会帮我们回收相关的引用,被销毁的 Activity 也会被回收内存,这样我们就不用担心会发生内存泄露了。

2、非静态内部类 / 匿名类

我们先来看看非静态内部类(non static inner class)和 静态内部类(static inner class)之间的区别。

class 对比

static inner class

non static inner class

与外部 class 引用关系

如果没有传入参数,就没有引用关系

自动获得强引用

被调用时需要外部实例

不需要

需要

能否调用外部 class 中的变量和方法

不能

生命周期

自主的生命周期

依赖于外部类,甚至比外部类更长

可以看到非静态内部类自动获得外部类的强引用,而且它的生命周期甚至比外部类更长,这便埋下了内存泄露的隐患。如果一个 Activity 的非静态内部类的生命周期比 Activity 更长,那么 Activity 的内存便无法被回收,也就是发生了内存泄露,而且还有可能发生难以预防的空指针问题。

举个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MainActivity extends AppCompatActivity {

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

    class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}

可以看到我们在 Activity 中继承 AsyncTask 自定义了一个非静态内部类,在 doInbackground() 方法中做了耗时的操作,然后在 onCreate() 中启动 MyAsyncTask。如果在耗时操作结束之前,Activity 被销毁了,这时候因为 MyAsyncTask 持有 Activity 的强引用,便会导致 Activity 的内存无法被回收,这时候便会产生内存泄露。

解决方法:将 MyAsyncTask 变成非静态内部类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MainActivity extends AppCompatActivity {

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

    static class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(50000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}

这时候 MyAsyncTask 不再持有 Activity 的强引用,即使 AsyncTask 的耗时操作还在继续,Activity 的内存也能顺利地被回收。

匿名类和非静态内部类最大的共同点就是 都持有外部类的引用,因此,匿名类造成内存泄露的原因也跟静态内部类基本是一样的,下面举个几个比较常见的例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MainActivity extends AppCompatActivity {

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // ① 匿名线程持有 Activity 的引用,进行耗时操作
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(50000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // ② 使用匿名 Handler 发送耗时消息
        Message message = Message.obtain();
        mHandler.sendMessageDelayed(message, 60000);
    }

上面举出了两个比较常见的例子

  • new 出一个匿名的 Thread,进行耗时的操作,如果 MainActivity 被销毁而 Thread 中的耗时操作没有结束的话,便会产生内存泄露
  • new 出一个匿名的 Handler,这里我采用了 sendMessageDelayed() 方法来发送消息,这时如果 MainActivity 被销毁,而 Handler 里面的消息还没发送完毕的话,Activity 的内存也不会被回收

解决方法:

  • 继承 Thread 实现静态内部类
  • 继承 Handler 实现静态内部类,以及在 Activity 的 onDestroy() 方法中,移除所有的消息 mHandler.removeCallbacksAndMessages(null);
3、集合类

集合类添加元素后,仍引用着集合元素对象,导致该集合中的元素对象无法被回收,从而导致内存泄露,举个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
   static List<Object> objectList = new ArrayList<>();
   for (int i = 0; i < 10; i++) {
       Object obj = new Object();
       objectList.add(obj);
       obj = null;
    }

在这个例子中,循环多次将 new 出来的对象放入一个静态的集合中,因为静态变量的生命周期和应用程序一致,而且他们所引用的对象 Object 也不能释放,这样便造成了内存泄露。

解决方法:在集合元素使用之后从集合中删除,等所有元素都使用完之后,将集合置空。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    objectList.clear();
    objectList = null;
4、其他的情况

除了上述 3 种常见情况外,还有其他的一些情况

  • 1、需要手动关闭的对象没有关闭
代码语言:txt
AI代码解释
复制
- 网络、文件等流忘记关闭
- 手动注册广播时,退出时忘记 unregisterReceiver()
- Service 执行完后忘记 stopSelf()
- EventBus 等观察者模式的框架忘记手动解除注册2、static 关键字修饰的成员变量3、ListView 的 Item 泄露

三、利用工具进行内存泄露的排查


除了必须了解常见的内存泄露场景以及相应的解决方法之外,掌握一些好用的工具,能让我们更有效率地解决内存泄露的问题。

1、Android Lint

Lint 是 Android Studio 提供的 代码扫描分析工具,它可以帮助我们发现代码机构 / 质量问题,同时提供一些解决方案,检测内存泄露当然也不在话下,使用也是非常的简单,可以参考下这篇文章:Android 性能优化:使用 Lint 优化代码、去除多余资源

2、leakcanary

LeakCanary 是 Square 公司开源的「Android 和 Java 的内存泄漏检测库」,Square 出品,必属精品,功能很强大,使用也很简单。建议直接看 Github 上的说明:leakcanary,也可以参考这篇文章:Android内存优化(六)LeakCanary使用详解


参考资料

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018.01.27 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Android中常见的内存泄露
内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory。 Android中常见的内存泄露如下: 1.集合类泄漏 集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。 2.单例造成的内存泄漏 由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。比如下面一个典型的例子
老马的编程之旅
2022/06/22
6320
Android中常见的内存泄露
最常见的Android内存优化方式及防止泄漏造成OOM总结篇
内存优化目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,就是该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用导致 GC 不能回收。既然说到内存泄漏和优化,就不得不先简单了解一下内存分配策略,然后再举常见泄漏例子和解决方法,最后做一下总结,这样更直观全面了解Android内存方面处理。
Android技术干货分享
2019/10/08
1.3K0
Android性能优化之内存泄漏,你想要的这里都有~
在Android中,内存泄露的现象十分常见;而内存泄露导致的后果会使得应用Crash 本文 全面介绍了内存泄露的本质、原因 & 解决方案,最终提供一些常见的内存泄露分析工具,希望你们会喜欢。
用户9239674
2022/02/09
8610
Android内存泄漏分析
强引用:类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
用户1205080
2019/03/06
1.6K0
Android内存泄漏分析
Android开发:详解Handler的内存泄露
上面提到,在Java里,非静态内部类和匿名类都会潜在的引用它们所属的外部类。 但是,静态内部类不会。
Carson.Ho
2019/02/22
1.1K1
Android开发中应该避免的内存泄露
一、背景和目的: 目前许多开发人员在Android开发过程中,较少关注实现细节和内存使用,容易会造成内存泄露,导致程序OOM。 本文会通过代码向大家介绍在Android开发过程中常见的内存泄露。 二、常见的内存泄露代码 1、使用Handler****造成的内存问题 在Android开发过程中,Handler是比较常用的,通过Handler发送Message与主线程进行通信,Message发送之后是存储在MessageQueue中的,有些Message并不是马上被处理的,在Message中存在一个Target
陈仁松
2018/03/20
1.2K0
内存泄漏三问—vivo真题
说到性能优化,就不得不提下内存泄漏了,内存泄漏发生的原因以及解决办法你是否都已了解呢?看看今天的三问:
码上积木
2020/09/27
5910
Android开发之漫漫长途 番外篇——内存泄漏分析与解决
该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索》以及《深入理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相关知识,另外也借鉴了其他的优质博客,在此向各位大神表示感谢,膜拜!!!另外,本系列文章知识可能需要有一定Android开发基础和项目经验的同学才能更好理解,也就是说该系列文章面向的是Android中高级开发工程师。
LoveWFan
2018/08/07
4440
Android开发之漫漫长途 番外篇——内存泄漏分析与解决
避免Android中Context引起的内存泄露
Context是我们在编写Android程序经常使用到的对象,意思为上下文对象。 常用的有Activity的Context还是有Application的Context。Activity用来展示活动界面,包含了很多的视图,而视图又含有图片,文字等资源。在Android中内存泄露很容易出现,而持有很多对象内存占用的Activity更加容易出现内存泄露,开发者需要特别注意这个问题。
技术小黑屋
2018/09/05
1.4K0
Android 性能优化最佳实践
快,稳,省,小,这四点很形象的代表了性能的四个方面,同时也让我们知道我们 App 现在是否是款性能良好的 APP,如果有一项不达标,那么说明我们的应用有待优化。
李林LiLin
2020/11/22
1.4K0
Android | App内存优化 之 内存泄漏 要点概述 以及 解决实战
1.Bitmap优化 Bitmap非常消耗内存, 而且在Android中,读取bitmap时, 一般分配给虚拟机的图片堆栈只有8M,所以经常造成OOM问题。 所以有必要针对Bitmap的使用作出优化: 1.1. 图片显示:加载合适尺寸的图片,比如显示缩略图的地方不要加载大图。 1.2. 图片回收:使用完bitmap,及时使用Bitmap.recycle()回收。 问题:Android不是自身具备垃圾回收机制吗?此处为何要手动回收。 Bitmap对象不是new生成的,而是通过BitmapFactory生产的。 通过源码可发现是通过调用JNI生成Bitmap对象(nativeDecodeStream()等方法)。 所以, 加载bitmap到内存里包括两部分, Dalvik(ART)内存和Linux kernel内存。 前者会被虚拟机自动回收。 而后者必须通过recycle()方法, 内部调用nativeRecycle()让linux kernel回收。 1.3. 捕获OOM异常:程序中设定如果发生OOM的应急处理方式。 1.4. 图片缓存:内存缓存、硬盘缓存等 1.5. 图片压缩:直接使用ImageView显示Bitmap时会占很多资源, 尤其当图片较大时容易发生OOM。 可以使用BitMapFactory.Options对图片进行压缩。 1.6. 图片像素(质量):android默认颜色模式为ARGB_8888, 显示质量最高,占用内存最大。 若要求不高时可采用RGB_565等模式。 还可以使用WebP; 图片大小:图片长度 * 宽度 * 单位像素 所占据字节数 ARGB_4444:每个像素占用2byte内存 ARGB_8888:每个像素占用4byte内存 (默认) RGB_565:每个像素占用2byte内存 1.7. 考虑使用inBitmap;图片优化之inBitmap 2. 巧用对象引用类型
凌川江雪
2019/12/16
1.3K0
Android | App内存优化 之 内存泄漏 要点概述 以及 解决实战
Android中Handler引起的内存泄露
在Android常用编程中,Handler在进行异步操作并处理返回结果时经常被使用。通常我们的代码会这样实现。
技术小黑屋
2018/09/05
5760
Android 内存泄露简介、典型情景及检测解决
什么是内存泄露? Android虚拟机的垃圾回收采用的是根搜索算法。GC会从根节点(GC Roots)开始对heap进行遍历。到最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉。内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到gc roots导致无法被GC回收。无用的对象占据着内存空间,导致不能及时回收这个对象所占用的内存。内存泄露积累超过Dalvik堆大小,就会发生OOM(OutOfMemory)。 内存泄露的经典场景 非静态
非著名程序员
2018/02/02
8020
Android 内存泄露简介、典型情景及检测解决
四年Android面试遇到的问题整理
以下问题的答案均是个人四年来学习实践中整理的,如有不同意见,欢迎斧正。 1.自定义Handler时如何避免内存泄漏
Android技术干货分享
2019/03/26
5850
Android异步通信:你了解Handler内存泄露吗?
在Android开发中,内存泄露十分常见。本文将详细讲解内存泄露的其中一种情况:在Handler中发生的内存泄露
Carson.Ho
2022/03/25
6840
Android异步通信:你了解Handler内存泄露吗?
Android context(Application/Activity)与内存泄露
android中的context可以做很多操作,但是最主要的功能是加载和访问资源。
阳光岛主
2019/02/19
1.4K0
Android面试每日一题: Handler 如何有效地避免内存泄漏问题
在Android系统中,Handler是一个消息发送和处理机制的核心组件之一,与之配套的其他主要组件还有Looper和Message,MessageQueue。 Message和Runnable类是消息的载体。MessageQueue是消息等待的队列。Looper则负责从队列中取消息。
老马的编程之旅
2022/09/21
8301
Android面试每日一题: Handler 如何有效地避免内存泄漏问题
小题大做 | Handler内存泄露全面分析
"内部类持有了外部类的引用,也就是Hanlder持有了Activity的引用,从而导致无法被回收呗。"
码上积木
2021/01/11
9240
小题大做 | Handler内存泄露全面分析
Android开发从GC root分析内存泄漏
我们常说的垃圾回收机制中会提到GC Roots这个词,也就是Java虚拟机中所有引用的根对象。我们都知道,垃圾回收器不会回收GC Roots以及那些被它们间接引用的对象。但是,对于GC Roots的定义却不是很清楚。它们都包括哪些对象呢?
yuxiaofei93
2018/09/11
2.2K0
Android 进阶解密笔记-热修复
内存泄漏(Memory Leak):是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
Yif
2019/12/26
4090
相关推荐
Android中常见的内存泄露
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验