常见泄漏场景:静态变量持有Activity、内部类隐式持有外部类、Handler导致泄漏

好,咱们直接进入正题。内存泄漏这东西,说白了就是本该被回收的对象,因为被某个“钉子户”强引用着,GC拿它没办法。我排查过上百个泄漏案例,发现80%以上都逃不出今天讲的这三个坑。

场景一:静态变量持有Activity

先看一个我印象特别深的例子。有一次线上反馈,用户反复进入某个详情页后,App直接OOM崩溃。我抓了heap dump一看,Activity实例竟然有几十个,全被一个静态的ArrayList拽着。

为什么会这样?

public class DataManager {
    private static List<Listener> sListeners = new ArrayList<>();

    public static void registerListener(Listener listener) {
        sListeners.add(listener);
    }
}

// 在Activity中注册
public class MainActivity extends AppCompatActivity implements DataManager.Listener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        DataManager.registerListener(this);  // 问题就在这里
    }
}

你看,sListeners是静态的,生命周期和App一样长。Activity把自己注册进去后,即使退出了,静态列表还持有它的引用。GC一看,嗯,还有人用着,不能回收。

核心要点:静态变量的生命周期 = 进程生命周期。任何传入静态集合的Activity引用,都必须显式移除。

我个人习惯的做法是:

  • onDestroy()里调用DataManager.unregisterListener(this)
  • 或者改用弱引用(WeakReference)包装Activity
  • 再或者,干脆用EventBus这类解耦框架

避坑指南:我曾经接手过一个项目,里面有个全局的静态HashMap缓存了所有Activity的引用,美其名曰“方便管理”。结果每个Activity泄漏几十KB,用户切几个页面就炸了。记住:静态变量只适合存Application Context或者纯数据,千万别存Activity。

场景二:内部类隐式持有外部类

这个坑更隐蔽。Java内部类会隐式持有外部类的引用,你写代码时根本看不见那个this$0字段。

public class MainActivity extends AppCompatActivity {
    private TextView mTextView;

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

        // 匿名内部类,隐式持有MainActivity.this
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 模拟耗时操作
                try { Thread.sleep(10000); } catch (InterruptedException e) { }
                // 这里访问了外部类的成员变量
                mTextView.setText("完成");
            }
        }).start();
    }
}

你想想看,这个Runnable是个匿名内部类,它偷偷持有了MainActivity的引用。如果Activity在10秒内退出了,这个线程还在跑,那Activity就泄漏了。

内部类类型 是否隐式持有外部类 泄漏风险
非静态内部类
匿名内部类
静态内部类
局部内部类 是(如果访问外部成员)

怎么解决?我建议你养成一个习惯:

  • 能用静态内部类就别用非静态的
  • 如果必须用内部类,把外部引用用WeakReference包一层
  • 异步任务结束后,记得清理回调

注意:即使内部类没有显式访问外部类的成员变量,它仍然持有外部类的引用。这是Java语言规范决定的,你躲不掉。唯一的办法就是改成静态内部类。

场景三:Handler导致泄漏

这个可以说是Android开发者的“老朋友”了。每个写过Handler的人,大概率都踩过这个坑。

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // 处理消息,这里隐式持有MainActivity
            updateUI();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 发送一个延迟消息
        mHandler.sendEmptyMessageDelayed(0, 60000); // 1分钟后执行
    }
}

问题出在哪?

  1. Handler是匿名内部类,持有外部Activity的引用
  2. 发送的消息Message又持有Handler的引用(通过target字段)
  3. Message被放在MessageQueue里,MessageQueue被Looper持有
  4. Looper是线程局部变量,生命周期和线程一样长

这一串引用链下来,Activity就被牢牢拴住了。哪怕用户已经退出了,只要消息还没处理,Activity就泄漏着。

引用链:Looper → MessageQueue → Message → Handler → Activity

我在项目中遇到过类似情况,当时是一个聊天页面,用Handler做心跳包。用户切到后台后,Activity一直泄漏,直到OOM。后来我用了两种方案:

方案一:静态内部类 + 弱引用

public class MainActivity extends AppCompatActivity {
    private MyHandler mHandler = new MyHandler(this);

    private static class MyHandler extends Handler {
        private WeakReference<MainActivity> mActivityRef;

        MyHandler(MainActivity activity) {
            mActivityRef = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = mActivityRef.get();
            if (activity != null) {
                activity.updateUI();
            }
        }
    }

    @Override
    protected void onDestroy() {
        // 关键:移除所有未处理的消息和回调
        mHandler.removeCallbacksAndMessages(null);
        super.onDestroy();
    }
}

方案二:使用Lifecycle感知组件

现在有了AndroidX,我推荐直接用LiveData或者协程。它们能自动感知生命周期,省心很多。

我的经验:onDestroy()里调用removeCallbacksAndMessages(null)是个好习惯。但要注意,如果Handler是静态的,记得在不需要时把Handler置空,否则弱引用也救不了你。

总结一下这三个场景

泄漏场景 根本原因 推荐解法
静态变量持有Activity 静态变量生命周期 > Activity 及时移除引用 / 用WeakReference
内部类隐式持有外部类 非静态内部类持有外部类引用 改成静态内部类 / 弱引用包装
Handler导致泄漏 消息队列持有Handler链 静态Handler + 弱引用 + 清理消息

嗯,这三个场景你排查时优先看。我一般拿到heap dump后,先搜Activity的实例数,如果发现本该销毁的Activity还活着,就顺着GC Root链往上查。八成都能落到这三个坑里。

下一章咱们聊聊更隐蔽的泄漏场景——比如资源未关闭、监听器未解注册这些。到时候我会分享一个我花了三天才定位到的奇葩泄漏案例。