Android中的内存泄漏
Android 中的内存泄漏(Memory Leak)本质上是:长生命周期的对象持有了短生命周期对象(如 Activity/Fragment)的引用,导致短生命周期对象无法被垃圾回收器(GC)回收。
以下是 Android 开发中常见的内存泄漏类型、原理、示例以及检测和解决方法的系统性整理。
一、 Android 常见内存泄漏类型
1. 静态变量导致的泄漏 (Static Variable)
- 原理:静态变量存储在方法区,其生命周期与 ClassLoader(通常等同于应用进程)一致。如果静态变量持有了
Activity或View的引用,该对象将永远无法释放。 例子:
public class MyUtils { // 错误:静态变量持有了 Activity 的 Context private static Context sContext; public static void setContext(Context context) { sContext = context; } } // 在 Activity 中调用 MyUtils.setContext(this); 导致 Activity 泄漏- 修正:尽量不存 Context。如果必须存,使用
context.getApplicationContext(),或使用WeakReference。
2. 单例模式导致的泄漏 (Singleton)
- 原理:单例的生命周期贯穿整个应用。如果单例初始化时传入了
Activity的Context并一直持有,该Activity就无法销毁。 例子:
public class AppManager { private static AppManager instance; private Context context; private AppManager(Context context) { this.context = context; // 如果传入 Activity,泄漏发生 } }- 修正:构造函数中强制转换:
this.context = context.getApplicationContext();。
3. 非静态内部类 / 匿名内部类 (Inner/Anonymous Class)
- 核心原理:在 Java 中,非静态内部类和匿名内部类会隐式持有外部类(如 Activity)的强引用。如果这些内部类对象的生命周期比外部类长,外部类就无法回收。
类型 A:Handler
例子:
// 匿名内部类 Handler 持有 Activity 引用 private final Handler mHandler = new Handler() { ... }; // 发送延时消息,消息未处理完,Activity 关闭也无法回收 mHandler.sendEmptyMessageDelayed(0, 10000);- 修正:使用
静态内部类+WeakReference,且在onDestroy中调用mHandler.removeCallbacksAndMessages(null)。
类型 B:Thread / AsyncTask
例子:
new Thread(new Runnable() { // 匿名内部类 Runnable 持有 Activity @Override public void run() { SystemClock.sleep(20000); } }).start();- 修正:定义为静态内部类,或在 Activity 销毁时中断线程。
类型 C:双括号初始化 (Double Brace Initialization)
- 原理:
new HashMap() {{ ... }}实际上创建了一个HashMap的匿名子类。这个子类会隐式持有外部类(Activity)的引用this$0。如果这个 Map 被传递到 Activity 外部(如静态缓存、单例配置),Activity 就会泄漏。 例子:
// 看起来是语法糖,实际是隐形炸弹 public void initData() { Map<String, Object> map = new HashMap<String, Object>() {{ put("key", "value"); // 这是一个匿名内部类的实例初始化块 }}; // 如果这个 map 被传给单例或静态变量,Activity 泄漏 GlobalCache.getInstance().save(map); }修正:
- 使用普通写法:
Map<String, Object> map = new HashMap<>(); map.put(...); - 使用 Kotlin:
val map = mapOf("key" to "value") - 使用静态工具方法:
Map.of(...)(Java 9+)
- 使用普通写法:
- 原理:
4. 资源未关闭 / 未注销 (Resource Not Closed)
- 原理:使用了系统资源或注册了监听器,使用完毕后未关闭或注销。
常见场景:
- BroadcastReceiver:忘记
unregisterReceiver。 - IO 流 / Cursor:数据库查询或文件读写后未
close()。 - EventBus / RxJava:
onDestroy中未取消订阅。 - 属性动画:无限循环动画未调用
cancel(),View 被持有,导致 Activity 无法释放。
- BroadcastReceiver:忘记
5. WebView 泄漏
- 原理:WebView 系统实现复杂,会在 native 层持有 Context,且往往不随 Activity 销毁而立即彻底释放。
修正:
- 不在 XML 中定义,改为代码动态
new WebView(getApplicationContext())。 - 销毁时:先从父容器
removeView,加载空内容loadUrl("about:blank"),最后destroy()。 - 多进程方案:将 WebView 放在独立进程,销毁时
System.exit(0)。
- 不在 XML 中定义,改为代码动态
二、 如何发现内存泄漏
1. LeakCanary (最推荐)
- 介绍:Square 开源的检测库。
- 原理:自动监测 Activity/Fragment 的销毁。若销毁后对象仍驻留堆内存,触发 GC 并分析引用链,通知开发者。
- 优点:集成简单,实时报警,无需手动分析。
2. Android Studio Profiler (Memory Profiler)
操作:
- 运行 App,打开 Profiler -> Memory。
- 操作 App 后手动触发 GC。
- 点击 "Dump Java Heap"。
- 在结果中筛选 Activity 类,查看 Count(实例数)。若 Activity 已关闭但 Count > 0 且有关联引用,即为泄漏。
3. Lint 静态检查
- Android Studio 自带的代码检查工具,能在编写代码时提示(例如:提示 "This Handler class should be static or leaks might occur")。
三、 总结:解决之道
解决的核心在于切断长生命周期对象对短生命周期对象的引用。
Context 敏感度:
- 原则:凡是跟 UI 渲染无关的(工具类、单例、全局配置),一律使用
Application Context。 - 警惕:严禁静态变量直接持有
Activity或View。
- 原则:凡是跟 UI 渲染无关的(工具类、单例、全局配置),一律使用
内部类规范:
- 强制:Activity 中定义的 Handler、Thread、Runnable 尽量使用 静态内部类 (static class)。
- 引用:如果静态内部类需要引用 Activity,必须通过
WeakReference。 - 拒绝:禁止在 Activity 中随意使用
new ArrayList() {{ ... }}这种双括号写法传递数据给外部。
生命周期对齐:
- 成对出现:
register对应unregister,bind对应unbind。 - 清理战场:
onDestroy中必须移除 Handler 消息、取消 RxJava 订阅、停止动画。
- 成对出现:
工具常驻:
- 开发阶段集成 LeakCanary,将内存泄漏视为 Bug 处理,发现即修。