4. Android内存泄漏常见模式:四大组件、视图、线程场景解析

内存泄漏这玩意儿,说白了就是对象本该被回收,结果因为有人还拽着它不放,导致GC干瞪眼。我排过不少线上OOM问题,十有八九都能归到几个固定的“坑”里。今天咱们就把四大组件、视图、线程这些重灾区挨个过一遍,看看它们到底是怎么漏的。

核心观点:内存泄漏的本质是长生命周期对象持有了短生命周期对象的强引用。

Android 内存泄漏常见模式全景 四大组件泄漏 Activity 被静态引用持有 Service 内部线程未停止 BroadcastReceiver 未解注册 ContentProvider 游标未关闭 视图与Drawable泄漏 匿名内部类持有Activity View被静态变量引用 Drawable回调未清理 ListView/RecyclerView复用问题 线程与协程泄漏 Handler发送延时消息 AsyncTask持有Activity引用 Thread内部类隐式引用 协程Job未取消 其他高频泄漏模式 单例中Context泄漏 集合类未及时清除 WebView内存管理不当 避坑提醒:以上所有模式最终都指向一个根引用链——GC Roots 无法释放 修复思路:找准长生命周期对象,用弱引用或及时清理

4.1 Activity 泄漏:最常见的“隐形杀手”

Activity 泄漏几乎成了Android内存泄漏的代名词。Activity 本身是个“短命鬼”,onDestroy 之后就应该被回收。但现实生活中,总有人强行留住它。

4.1.1 静态变量持有 Activity

我印象很深,有个同事把 Activity 塞进了一个静态 List 里当缓存用。

// 错误示例:静态List持有Activity
public class ImageCache {
    private static List<Activity> sActivities = new ArrayList<>();
    
    public static void addActivity(Activity activity) {
        sActivities.add(activity);  // Activity被长期持有
    }
}

改法很简单:要么不用静态集合存 Activity,要么改用WeakReference<Activity>。

4.1.2 内部类/匿名内部类隐式持有

非静态内部类会隐式持有外部类的引用,这是Java语法层面的特点。当你的 Activity 里写了个匿名 Runnable,并把这个 Runnable 丢给后台线程执行,Activity 就“被绑票”了。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 长时间操作,Runnable持有Activity引用
                SystemClock.sleep(10000);
            }
        }).start();
    }
}

Activity 都 finish 了,线程还在跑,Runnable 还拽着 Activity 不放——GC 想回收都没法下手。我个人习惯遇到这种情况,要么用静态内部类+弱引用,要么在 onDestroy 里把线程停掉。

小技巧:Android Studio 的 Profiler 里,如果看到 Activity 实例数只增不减,十有八九是用静态变量或者内部类把它“焊死”了。

4.2 Service 泄漏:默默吃内存的“后台服务”

Service 的问题往往不是它自己漏,而是它里面开的线程漏。Service 本身生命周期很长(尤其是前台服务),如果它内部启动了一个 HandlerThread 或者 AsyncTask,又不加管理,Activity 退出后 Service 还在跑,线程里的对象就泄漏了。

我曾经排查过一个音乐播放App:用户切出去半小时后,内存飙到400MB。一查发现 Service 里的 Handler 用了个匿名内部类,持有 Service 引用,而 Service 又被系统绑定着——整个引用链像铁链一样串起来。

解决方案:

  • Service 中尽量使用静态内部类+弱引用
  • onDestroy 时停止所有线程,清除消息队列(Handler.removeCallbacksAndMessages(null))
  • 考虑用 IntentService 处理一次性任务

4.3 BroadcastReceiver 与 ContentProvider 泄漏

这两个组件泄漏一般比较隐蔽。

  • BroadcastReceiver:动态注册后必须调用unregisterReceiver(),否则持有 Activity/Service 引用。我见过很多人在 onResume 注册、onPause 忘了解注册的场景。
  • ContentProvider:如果用了 Cursor,一定要确保在 finally 块里 close。Cursor 内部持有 ContentResolver 的引用,连着连着一大串都没法回收。

注意:onDestroy 里不要依赖系统帮你解注册,有时候 Activity 因为异常销毁可能不会走 onDestroy。我建议在 onPause 或 onStop 里就进行解注册操作,更稳妥。

4.4 视图(View)泄漏:UI 组件的“藏匿点”

View 本身被 Activity 持有,但问题往往出在 View 被“移花接木”到了其他地方。

4.4.1 Drawable 回调泄漏

当 View 设置了 Drawable,而 Drawable 有一个回调(Callback)指向 View,如果这个 Drawable 被某个静态变量持有,View 和它的 Activity 都会被牵连。

// 典型场景:把View的Background Drawable缓存起来
private static Drawable sCachedDrawable;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    View view = new View(this);
    sCachedDrawable = view.getBackground();  // Drawable持有View的Callback
}

解决:要么不缓存 Drawable,要么在 onDestroy 里把 callback 置空。

4.4.2 匿名回调持有 Activity

比如给 View 设置 OnClickListener,如果这个 listener 是匿名内部类,它隐式持有 Activity。Activity 销毁后如果 View 还被其他模块持有(比如被加入一个静态的 list),就泄漏了。

我一般习惯在 onDestroy() 里把 View 的 listener 清空:

@Override
protected void onDestroy() {
    super.onDestroy();
    if (myButton != null) {
        myButton.setOnClickListener(null);
    }
}

4.5 线程与 Handler 泄漏:延时消息的“定时炸弹”

Handler 泄漏是Android经典难题。Handler 内部类持有 Activity,而 Message 又持有 Handler——所以只要 MessageQueue 里还有这条消息,Activity 就永远释放不了。

// 错误写法
Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        // 更新UI
    }
};

正确的做法:

  1. 使用静态内部类 + WeakReference<Activity>
  2. 在 onStop/onDestroy 里移除所有回调:mHandler.removeCallbacksAndMessages(null)

还有一个很多人忽略的点:AsyncTask。这东西虽然Android 11已经废弃,但老项目里随处可见。AsyncTask 内部持有一个 FutureTask,如果它在后台执行,而 Activity 销毁了,AsyncTask 的匿名内部类会泄漏 Activity。

我的建议:新项目一律用 Kotlin 协程 + ViewModel,生命周期感知做得更漂亮;老项目遇到 AsyncTask 能改就改,不能改的话务必在 onCancelled 里取消任务。

4.6 集合类与单例泄漏:长期缓存不清理

这个问题本质上和静态变量一样——把东西放进集合后忘了拿出来。

场景 泄漏原因 修复方案
全局集合缓存Fragment Activity退出后Fragment仍被集合持有 使用WeakHashMap或手动移除
单例中持有多张Bitmap 图片缓存无限增长 设置LRU大小上限,或弱引用
EventListener列表 注册后未取消 使用弱引用观察者模式

还有一个想强调的:单例中传入Context。很多人写工具类时直接传 Activity 的 Context 进去,那整个 Activity 就跟着单例一起活到天荒地老。正确做法是传 ApplicationContext。

4.7 WebView 泄漏:历史遗留的“巨坑”

WebView 的内存泄漏在系统层面就有问题(Android 4.4以下特别严重)。WebView 引擎会持有一个 Activity 的引用,导致 Activity 无法回收。

我曾经在旧项目里遇到一个 WebView 加载了H5页面,用户来回进出8次就OOM了。处理方案:

  • 把 WebView 放在单独的 Activity 或者 Fragment 里,用完即销毁
  • 在 onDestroy 时调用 webView.destroy() 并把引用置空
  • 如果 App 频繁使用 WebView,可以考虑用一个全局的 WebView 实例动态附加到不同 Activity(但要注意 Activity 切换时清除状态)

避坑指南:我曾经犯过一个错误——在 Activity 里new了一个 WebView,但没把它 add 到任何 ViewGroup 中。结果这个 WebView 虽然不显示,但依然持有 Activity 引用。所以记住:不需要显示时也要手动 destroy 掉。

4.8 总结:怎么快速定位泄漏模式

以上几类模式,覆盖了90%以上的Android内存泄漏场景。我个人遇到内存问题时,第一步不是看代码,而是先打开 Memory Profiler,看看 Activity 实例数是不是只增不减。一旦确认有泄漏,第二步就是查 GC Roots。

推荐用 Android Studio 的 Analyze Dump 功能,或者用 LeakCanary 自动检测。熟练以后,看到泄漏路径基本就能猜到是哪个模式。

嗯,咱们这一章先聊到这。记住一句话:见到静态变量、内部类、延时消息、单例、Context——心里就得敲警钟。