4. WorkManager 默认 Context 类型:ApplicationContext 与 ActivityContext 的区别及泄漏风险

现象识别

在 WorkManager 任务执行过程中,应用不响应、内存占用持续增长、或出现 android.view.WindowLeaked 异常,往往是 Context 泄漏的典型信号。特别是当任务中持有了 Activity 或 Fragment 的引用,而 WorkManager 的默认生命周期与 Activity 不一致时,泄漏风险成倍增加。

泄漏判断规则:如果任务结束后,通过 adb shell dumpsys meminfo <包名> 发现 Activity 对象数量未减少,或 LeakCanary 上报 Activity 实例泄漏,则强烈怀疑使用了错误的 Context 类型。

数据采集

收集以下信息用于定位泄漏源头:

  • WorkManager 初始化配置:检查 Configuration 中是否自定义了 AndroidJobFactory 或 WorkerFactory,以及是否将 Activity 传入。
  • Worker 实现代码:查看 doWork() 方法内是否持有 Context 引用并传递给其他组件(如 Handler、BroadcastReceiver、Dialog)。
  • 内存快照:通过 Android Profiler 或 MAT 分析堆转储,查找 android.app.Activity 类型的泄漏链。
采集项获取方式关键判断
Context 来源getApplicationContext() vs getBaseContext()是否使用 Activity 实例作为 Context
Worker 生命周期doWork() 执行前后是否在 onStopped() 后仍有未释放的回调
WorkManager 默认 ContextWorkManager.getInstance(context) 传入的 context是否为 ApplicationContext

原因定位

WorkManager 在设计上默认使用 ApplicationContext 来创建 Worker 实例。但开发者有时会错误地传入 Activity 或 Service 的 Context,导致与 UI 生命周期绑定。下表列出两种 Context 的核心差异:

对比维度ApplicationContextActivityContext
生命周期与进程一致,直到进程被杀与 Activity 一致,onDestroy 后不可用
泄漏风险低(系统单例)高(持有 Activity 引用会阻止 GC)
适用场景后台任务、全局单例、数据库、SharedPreferencesUI 操作、Dialog、Toast、资源主题
WorkManager 默认使用是(推荐)否(需显式传入,不推荐)
泄漏风险根源:当 Worker 的 doWork() 中使用 getApplicationContext() 误写成 getActivity() 或通过构造函数传入 Activity 时,即使 Worker 执行完毕,只要任务队列或回调仍持有 Context 引用,Activity 就无法被回收。

下图展示了两种 Context 在 WorkManager 中的传递路径及泄漏点:

WorkManager Context 传递路径与泄漏风险模型 Worker 创建 (doWork 入口) ✅ ApplicationContext getApplicationContext() ❌ ActivityContext 传入 Activity 实例 安全:进程级单例 无泄漏 泄漏:Activity 无法释放 OOM 风险 泄漏链示例 Activity → 匿名内部类 → Worker → WorkManager 任务队列 🎯 任何一环未释放都会阻止 GC

修复动作

针对不同场景,采用以下修复策略:

  1. 确保使用 ApplicationContext:在 Worker 构造函数或 doWork() 中始终调用 getApplicationContext()。
  2. 如必须使用 ActivityContext:需在 onStop() 或 onDestroy() 中手动取消 WorkManager 任务,并释放所有引用。
  3. 修改 WorkManager 初始化:不要在 Application 的 onCreate() 之外的地方初始化 WorkManager,也不要在 Activity 中调用 WorkManager.initialize()。
// ✅ 正确:Worker 内使用 ApplicationContext
class UploadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        val appContext = applicationContext
        // 使用 appContext 进行数据库、网络等操作
        return Result.success()
    }
}

// ❌ 错误:将 Activity 传入 Worker
class UploadWorker(activity: Activity) : Worker(activity, WorkerParameters()) {
    override fun doWork(): Result {
        // 持有 activity 引用,导致泄漏
        return Result.success()
    }
}
修复清单:
  • 检查所有 Worker 类,确保没有将 Activity、Fragment、View 作为成员变量保存。
  • 使用 applicationContext 替代 context 变量。
  • 如果使用 WorkManager.getInstance(context) 传入的 context 不是 ApplicationContext,立即替换为 applicationContext。

复测

复测步骤应包含:

  1. 内存泄漏检测:运行 WorkManager 任务后,使用 LeakCanary 或 adb shell dumpsys meminfo 检查 Activity 数量。
  2. 回归测试:执行所有涉及 WorkManager 的用例,确保功能正常且无 ANR。
  3. 压力测试:模拟频繁的任务入队和取消操作,观察内存曲线是否平稳。
# 检查 Activity 泄漏(包名替换为你的应用包名)
adb shell dumpsys meminfo com.example.app | grep "Activity"

预防

  • 编码规范:在团队 Code Review 中明确要求 Worker 内禁止存储 Activity 引用。
  • 静态检查:集成 Lint 或 Detekt 规则,检测 Context 使用不当。
  • 基类封装:创建基础 BaseWorker,强制子类使用 applicationContext。
// 基类封装示例
abstract class SafeWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    protected val safeContext: Context = applicationContext
}

class MyWorker(context: Context, params: WorkerParameters) : SafeWorker(context, params) {
    override fun doWork(): Result {
        // 直接使用 safeContext
        return Result.success()
    }
}