Android内存泄漏分享
内容概述
- 内存泄漏和内存管理相关基础。
- Android中的内存使用。
- 内存分析工具和实践。
以下内容不考虑非引用类型的数据,或者将其等同为对应的引用类型看待——一切皆对象。
内存泄漏概念
不再使用的对象常驻内存,如静态变量,或被其它还在使用的对象(生命周期更长)所引用的对象,对应内存无法回收利用。
为了避免对象无法正确、及时被释放,需要理解:
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。另外,若对象包含内部类对象,那么内部对象的引用也要被释放。
不同的变量它们的默认生命周期是不一样的。
- 非静态成员变量随对象的释放而释放
- 局部变量随方法结束释放
- 静态成员变量随进程结束而释放。
都可以“手动”设置为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实时监测。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人