Android内存泄漏分享

内容概述

  1. 内存泄漏和内存管理相关基础。
  2. Android中的内存使用。
  3. 内存分析工具和实践。

以下内容不考虑非引用类型的数据,或者将其等同为对应的引用类型看待——一切皆对象

内存泄漏概念

不再使用的对象常驻内存,如静态变量,或被其它还在使用的对象(生命周期更长)所引用的对象,对应内存无法回收利用。

为了避免对象无法正确、及时被释放,需要理解: GC如何回收对象,如何释放对象?

对象的引用

对象的使用是通过指向它的引用被访问的,引用被保存在引用类型变量中。

这里变量指:

类变量:静态成员变量,成员变量也叫字段。 实例变量:非静态成员变量。 局部变量:在方法中定义,赋值和使用。 不考虑:参数、返回值、常量。

在new一个对象后,其强引用被构造方法返回。 对象的内部类对象,也拥有this$0这样的强引用指向它。

Java有四种引用,分别对应不同性质的引用可达性(reachable)——可达指通过此引用访问到对应的对象。

引用的分类

强引用使用引用对应的类型变量保存,需要手动释放——设置引用变量为null。 java的有四种引用,其它三种引用由对应的引用包裹类实现——可以认为是特殊类型的引用变量,GC在对待这些引用变量时有不同的策略:

  • 强引用(StrongReference) 正常声明的变量都是强引用,即便抛出OutOfMemoryError异常,也不会被回收。需要手动设置变量为null来释放引用。
  • 软引用(SoftReference) 仅在内存不足时GC才会回收软引用对象。
  • 弱引用(WeakReference) 一旦扫描到对象仅拥有弱引用,就回收。GC运行在一个优先级很低的线程,不会那么“及时”发现。

在通过引用包裹对象get获得实际对象时,有可能为null。可以使用一个ReferenceQueue来关联软引用和弱引用对象,它们在回收时其引用包裹对象被添加至此队列。

  • 虚引用(PhantomReference) 不对GC产生任何影响,必须有关联的ReferenceQueue,在对象仅剩虚引用时,GC在回收它时把对应的引用包裹对象放入ReferenceQueue——通常就是用来跟踪对象的回收,做一些清理操作。不含任何“持有”对象的概念,get永远返回null。

GC何时回收对象[简要]

Java有自动的内存回收机制,在合适的时候,运行时会执行GC来清理掉那些不再被使用的对象。根据内存需要,程序运行时会不定期多次执行GC。

Java判断对象是否不再使用有多种策略,最终都是和对象的引用相关。

如果对象的引用数量为0,那么它显然是垃圾对象。 此外,Java使用“根对象可达性”来判定对象是否有效。

在虚拟机中,有一类GC相关的对象被称作“GC root”。 GC root通过引用变量一级级来找到堆中的每一个对象。很显然,不同类型的引用变量,GC对待它们有不同的发现(使用其中的引用)策略。 那些最终不能从根对象引用得到的对象被认为是不可达对象,也就是可回收对象。

可见,只有强引用需要我们自己来考虑其释放的问题。在分析内存泄漏问题时,我们主要关注对象的强引用。

对象的释放

对象的释放,就是对其强引用的释放——将保存此引用的变量设置为null。另外,若对象包含内部类对象,那么内部对象的引用也要被释放。

不同的变量它们的默认生命周期是不一样的。

  1. 非静态成员变量随对象的释放而释放
  2. 局部变量随方法结束释放
  3. 静态成员变量随进程结束而释放。

都可以“手动”设置为null来释放。

方法未返回前,执行域的变量都不会释放。需要注意一些方法中的变量的及时释放。

void releaseObject() {
    Person p = new Person();
    p = new Person(); // 释放
    p = null;         // 释放
    // more code...
}

void uglyMethod() {
    Task task;
    while(!stop) {
        task = mBlockingQueue.take(); // 阻塞
        
        //一些针对task的操作。
    }
}
上面,在take()获得下一个对象赋给task之前,task一直引用着上一个从队列中获得的Task对象——它无法被释放。

引用的方向

引用指向某个对象。 A持有B的引用,那么此引用的方向从A到B。 A不可释放,A引用B,那么B也不可释放。反之,B引用到了不可释放的A,对B的释放没有影响。

Outgoing Reference: 对于一个对象,查看它拥有的引用变量,可以知道它所引用的其它对象。

Incoming Reference: 其它对象持有的指向当前对象的引用变量。

环引用

若A和B互相引用,这两个对象则形成一个环形引用,但不是根对象可达,环形引用是可以被正常回收的。

Android中的内存使用

  • Android程序有内存限制。
  • 频繁的GC容易造成程序响应问题。
  • 进程自动回收:运行在后台的程序,拥有的内存越大,越容易被回收——任务栈和进程的关系——做好数据持久化、程序状态连续性和恢复。

对象使用的建议

Android程序偏向更轻量级的对象,更少的内存占用时间(除去必要的内存缓存),重用避免重复创建。

  • 避免使用枚举 使用final static int。
  • 多使用final修饰 除非业务需要,首选final修饰,编译器会优化。
  • 图片 成熟的库(Android-Universal-Image-Loader),用多少取多少,及时释放,缓存。
  • 软引用和弱引用 能满足需要的话,代替强引用。
  • 池和对象复用 避免对象创建,引起内存抖动。例如知道一个集合是固定大小的话,那么每次网络请求结束后更新对象字段值,而不是clear又创建一批新对象。 线程池——好处不多说。使用时注意因为run持久不结束,线程对象对应的字段和局部变量注意泄漏。 Adapter中数据对象和View的复用。
  • UI操作的去噪 快速滑动、输入等。
  • 避免不必要的getter、setter 仅仅是简单的POJO,完全没必要访问控制器。
  • 合并handler handler不要离开Activity,最好的一个Activity使用一个就够了。不要使用Handler代替回调来通信,使用第三方库,如EventBus来解耦,handler传递数据很低效(不及时-它不是同步的,对象序列化)。 handler是用来完成跨线程的通信的。
  • 及时释放引用 能使用局部变量的,就不要使用字段。方法中,释放那些不使用又继续占有的对象引用。 四大组件对象不是由我们new的,有其明确的生命周期,在“销毁”动作时从对象引用层面释放该释放的。
  • 内部类 优先使用静态内部类。 匿名内部类总是默认持有外部类对象的引用。
  • 在保证速度的前提下使用文件缓存 一些情况下甚至是必须的,如登录状态。
  • 使用ApplicationContext 仅在必要的时候——如dialog——使用Activity,而且注意Activity的Context的及时释放。
  • 使用具体类而不是接口 例如,HashMap,变量不需要声明为Map,这会有更好的执行速度。 没必要为“不存在”的扩展性做牺牲。
  • 在onDestroy中做好清理 主要是引用的释放,广播的取消注册,回调/监听对象的解除,handler的取消投递的消息、网络请求的取消、动画的停止,线程、其它异步任务和处理等。

“最佳实践”平时多收集,原则上对于泄漏问题,只有一点,不使用就及时把保持引用的成员变量和局部变量设置为null。重点注意回调和静态字段。

常见的泄漏

典型大对象

  • Activity
  • 图片、音频、视频文件
  • Json数据

可以从Activity开始,依次排查占用内存较大的对象的泄漏。通常,一个包含更多其它对象的大对象的释放,顺带解决了很多对象的泄漏。

匿名内部类

网络,语音,线程,其它异步操作,如果使用到callBack/Listener对象,应该注意这些对象的释放。 场景: AudioManager是全局的语音管理对象。 假设播放需要传递语音文件路径并提供回调来控制UI: 在Activity中:

void onCreate() {
    AudioManager.addListener(new AudioPlayCallBack() {
        @Override
        public void startPlay() {
        }
        
        @Override
        public void stopPlay() {
        }
    });
}

void onPlayButtonClick() {
    AudioManager.startPlay(mAudioPath);
}

void onDestroy() {
    AudioManager.clearListener();
    AudioManager.stopPlay();
}

监听(观察者模式),广播接收器

同回调一样,一般的,Activity中使用Receiver或Observer对象,在onCreate中开始注册,在onDestroy中需要解除注册。

Service

作为四大组件之一,对象本身创建和释放不是我们控制。使用startService和stopService、bingService和unBindService来控制组件对象的生命周期。 通常服务是一直运行在后台的,避免在服务中保存不使用的对象。 场景:

ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mCoreService = ((CoreService.CoreServiceBinder) service).getService();
            mCoreService.registerConnectionStatusCallback(new IConnectionStatusCallback() {
                @Override
                public void connectionStatusChanged(int connectedState, String reason) {
                    // xxx
                }
            });
            // xxx
        }

        /**
        在和Service的连接“意外”中断时执行,通常是运行Service的其它进程崩溃后引起。
        同一进程中几乎不会发生(Service死掉了,而此处代码还在执行...):此方法几乎不会被执行。
        不会移除此连接。必须主动调用unbindService来解除连接。
        */
        @Override
        public void onServiceDisconnected(ComponentName name) {
            mCoreService.unRegisterConnectionStatusCallback(); // 不执行
            mCoreService = null;// 不执行
        }
    };

上面应该在onDestroy中unbindService并移除Activity和Service对象的引用(回调匿名内部类)连接。

Handler

延迟消息被线程中的MessageQueue持有,在消息未处理前,Message对象引用handler,而handler引用Activity的事情很容易发生。

handler大多数时间也是写为匿名内部类——这本身没什么。 在onDestroy中:

void onDestroy() {
    handler.removeCallbacksAndMessages(null);
}

Context

Android中Context是“God Object”,它拥有很多运行时需要的全局信息。通常使用第三方库,系统API时,需要一个 Context时,优先使用Application。如果必须用到Activity的情况,记得它和匿名内部类是一样的,不要在三番五次的参数传递之后,忘记释放。

动画

属性动画必须手动stop,否则它会一直执行下去,持有Activity的mContext导致Activity对象的泄漏。

单例、全局对象

少用,注意意外的引用驻留。 简单的: ActivityManager管理Activity的集合,在onCreate和onDestroy时从ActivityManager中add和remove掉。 类变量如果是内部类这样的拥有对外部类的引用: 记得释放类变量,或者换用静态内部类,普通类,然后提供对外部类引用的设置和解除。

总而言之:对象是有生命周期的,需要在合适的时间释放对象的强引用。

内存分析工具

学习内存分析工具的使用,在实践中积累内存泄漏的问题,避免错误的代码。

Android Monitor

Android Studio 1.5以上版本有此功能。 可以快速查看对象个数,占用内存情况,“简单地”分析对象引用情况。

Memory Analysis Tool

Java的内存分析工具。

Shallow vs. Retained Heap

Dominator Tree

LeakCanary

产生和发现引用泄漏

检测目标类

运行程序,GC后dump生成hprof文件,使用MAT分析。

全面测试

在测试环境,使用LeakCanary实时监测。

讨论

列表+详情页 详情页 使用SingleInstance?不允许多个页面同时打开?

Volley Dispatcher引起的泄露。

收集踩过的坑。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏测试开发架构之路

堆和栈的区别

一、预备知识—程序的内存分配          一个由C/C++编译的程序占用的内存分为以下几个部分     1、栈区(stack)— 由编译器自动分配释放,存...

29380
来自专栏Java技术栈

详解 Java 中的三种代理模式

代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即...

8510
来自专栏自动化测试实战

RF自定义系统关键字

41770
来自专栏noteless

java for循环里面执行sql语句操作,有效结果只有一次,只执行了一次sql mybatis 循环执行update生效一次 实际只执行一次

java后台controller中,for循环执行数据库操作,但是发现实际仅仅执行了一次,或者说提交成功了一次,并没有实际的个数循环

37130
来自专栏Jimoer

jvm学习记录-对象的创建、对象的内存布局、对象的访问定位

简述 今天继续写《深入理解java虚拟机》的对象创建的理解。这次和上次隔的时间有些长,是因为有些东西确实不好理解,就查阅各种资料,然后弄明白了才来做记录。 (此...

28470
来自专栏编程

30个基本的Python技巧和窍门程序员

1.就地交换两个数字。 Python提供了一种直观的方式来分配和交换一行。请参考下面的例子。 x,y = 10,20print(x,y) x,y = y,xpr...

22970
来自专栏欧阳大哥的轮子

深入解构objc_msgSend函数的实现

熟悉OC语言的Runtime(运行时)机制以及对象方法调用机制的开发者都知道,所有OC方法调用在编译时都会转化为对C函数objc_msgSend的调用。

12520
来自专栏架构师小秘圈

shell脚本极简教程

一,shell题记 不懂shell的程序员不是好程序员,学习shell是为了自动化,使用自动化可以非常有效的提高工作效率。没有一个大公司不要求linux的基本技...

44860
来自专栏FreeBuf

Safari信息泄露漏洞分析

Javascript中的数组和数组对象一直都是编程人员优化的主要目标,一般来说,数组只会包含一些基本类型数据,比如说32位整数或字符等等。因此,每个引擎都会对这...

10220
来自专栏IT笔记

聊聊Servlet、Struts1、Struts2以及SpringMvc中的线程安全

很多初学者,甚至是工作1-3年的小伙伴们都可能弄不明白?servlet Struts1 Struts2 springmvc 哪些是单例,哪些是多例,哪些是线程安...

38760

扫码关注云+社区

领取腾讯云代金券