Android NotificationService协程内存泄漏与任务执行保障方案咨询
问题背景
在代码#1中,onListenerDisconnected内的代码无法执行完成,因为Service销毁时协程会被取消。
代码#1
@AndroidEntryPoint class NotificationService : NotificationListenerService() { @Inject lateinit var updateIsNotificationServiceActiveUseCase: UpdateIsNotificationServiceActiveUseCase private val job = SupervisorJob() private val scope = CoroutineScope(Dispatchers.IO + job) override fun onListenerConnected() { scope.launch { updateIsNotificationServiceActiveUseCase.execute( UpdateIsNotificationServiceActiveUseCase.Companion.Param( isActive = true ) ) } } override fun onListenerDisconnected() { scope.launch { updateIsNotificationServiceActiveUseCase.execute( UpdateIsNotificationServiceActiveUseCase.Companion.Param( isActive = false ) ) } } override fun onDestroy() { super.onDestroy() if (job.isActive) { job.cancel() } } }
尝试的有缺陷解决方案
为了让代码能执行完成,我尝试使用绑定到应用生命周期的CoroutineScope(而非Service自己的Scope),但LeakCanary检测到NotificationService存在内存泄漏。
对应代码
@AndroidEntryPoint class NotificationService : NotificationListenerService() { @Inject lateinit var updateIsNotificationServiceActiveUseCase: UpdateIsNotificationServiceActiveUseCase @Inject lateinit var lifeCycleScope: CoroutineScope override fun onListenerConnected() { lifeCycleScope.launch { updateIsNotificationServiceActiveUseCase.execute( UpdateIsNotificationServiceActiveUseCase.Companion.Param( isActive = true ) ) } } override fun onListenerDisconnected() { lifeCycleScope.launch { updateIsNotificationServiceActiveUseCase.execute( UpdateIsNotificationServiceActiveUseCase.Companion.Param( isActive = false ) ) } } }
核心问题
- 我是否需要关注LeakCanary检测到的内存泄漏?还是可以为了实现需求而忽略该问题?
LeakCanary检测报告:
┬─── │ GC Root: Global variable in native code │ ├─ android.service.notification. │ NotificationListenerService$NotificationListenerWrapper instance │ Leaking: UNKNOWN │ Retaining 2.5 kB in 35 objects │ this$0 instance of link.limecode.histotify.services.NotificationService │ ↓ NotificationListenerService$NotificationListenerWrapper.this$0 │ ~~~~~~ ╰→ link.limecode.histotify.services.NotificationService instance Leaking: YES (ObjectWatcher was watching this because link.limecode. histotify.services.NotificationService received Service#onDestroy() callback and Service not held by ActivityThread) Retaining 1.9 kB in 34 objects key = 1532cc3d-e8d6-40dc-81d4-914260acb10d watchDurationMillis = 18592 retainedDurationMillis = 13584 mApplication instance of link.limecode.histotify.HistotifyApp mBase instance of android.app.ContextImpl METADATA Build.VERSION.SDK_INT: 29 Build.MANUFACTURER: HUAWEI LeakCanary version: 2.10 App process name: link.limecode.histotify Class count: 16160 Instance count: 130077 Primitive array count: 93652 Object array count: 18860 Thread count: 26 Heap total bytes: 17295269 Bitmap count: 11 Bitmap total bytes: 8450411 Large bitmap count: 0 Large bitmap total bytes: 0 Db 1: open /data/user/0/link.limecode.histotify/databases/leaks.db Stats: LruCache[maxSize=3000,hits=34712,misses=89445,hitRate=27%] RandomAccess[bytes=4417411,reads=89445,travel=29347908102,range=21153586,size=26 687475] Analysis duration: 7242 ms
- 如果该内存泄漏不可忽略,还有什么方法可以在无内存泄漏的前提下实现需求?
回答
问题1:是否需要关注该内存泄漏?
必须关注,不能忽略。从LeakCanary报告来看,NotificationService实例在onDestroy后仍被NotificationListenerWrapper(Native层全局引用)持有,会导致该实例无法被GC回收。虽然单次泄漏内存不大,但如果Service频繁创建销毁,累计泄漏会逐步占用更多内存,最终可能引发OOM,同时也违背Android内存管理的最佳实践。
另外需要注意:使用应用级Scope本身不会直接导致泄漏,但这里的泄漏根源是NotificationListenerService内部的NotificationListenerWrapper持有Service实例引用,应用Scope的协程可能间接延长了引用生命周期,或者这是特定厂商(华为Android 29)的系统层面问题,但无论哪种情况,都应该处理以避免潜在风险。
问题2:无内存泄漏的实现方案
要在Service销毁后仍能完成updateIsNotificationServiceActiveUseCase.execute的执行,同时避免内存泄漏,推荐以下几种方案:
方案1:线程池执行同步任务
如果execute方法可以改为同步执行(或内部封装为同步),直接用线程池执行,避免持有Service引用:
@AndroidEntryPoint class NotificationService : NotificationListenerService() { @Inject lateinit var updateIsNotificationServiceActiveUseCase: UpdateIsNotificationServiceActiveUseCase // 应用级线程池,避免重复创建线程 private val executor = Executors.newSingleThreadExecutor() override fun onListenerConnected() { executor.execute { updateIsNotificationServiceActiveUseCase.execute( UpdateIsNotificationServiceActiveUseCase.Companion.Param(isActive = true) ) } } override fun onListenerDisconnected() { executor.execute { updateIsNotificationServiceActiveUseCase.execute( UpdateIsNotificationServiceActiveUseCase.Companion.Param(isActive = false) ) } } override fun onDestroy() { super.onDestroy() // 优雅关闭线程池,等待已提交任务完成 executor.shutdown() } }
注:executor.shutdown()不会中断已执行任务,只会拒绝新任务,确保onListenerDisconnected中的任务能完成。
方案2:独立CoroutineScope + 避免持有Service引用
如果必须用协程,创建不依赖Service生命周期的Scope,同时确保UseCase不持有Service引用(所有Context依赖使用Application Context):
@AndroidEntryPoint class NotificationService : NotificationListenerService() { @Inject lateinit var updateIsNotificationServiceActiveUseCase: UpdateIsNotificationServiceActiveUseCase // 创建独立Scope,不绑定Service生命周期 private val globalJob = SupervisorJob() private val globalScope = CoroutineScope(Dispatchers.IO + globalJob) override fun onListenerConnected() { globalScope.launch { updateIsNotificationServiceActiveUseCase.execute( UpdateIsNotificationServiceActiveUseCase.Companion.Param(isActive = true) ) } } override fun onListenerDisconnected() { globalScope.launch { updateIsNotificationServiceActiveUseCase.execute( UpdateIsNotificationServiceActiveUseCase.Companion.Param(isActive = false) ) } } override fun onDestroy() { super.onDestroy() // 不取消Job,让已启动的协程完成执行 } }
关键:确保updateIsNotificationServiceActiveUseCase内部无NotificationService引用,所有Context依赖用Application Context,避免协程执行时持有Service实例。
方案3:WorkManager(推荐用于需保证执行的任务)
如果需要保证更新操作即使应用进程重启也能完成,使用WorkManager提交一次性任务:
@AndroidEntryPoint class NotificationService : NotificationListenerService() { @Inject lateinit var workManager: WorkManager override fun onListenerConnected() { val request = OneTimeWorkRequestBuilder<UpdateNotificationStatusWorker>() .setInputData(workDataOf("is_active" to true)) .build() workManager.enqueue(request) } override fun onListenerDisconnected() { val request = OneTimeWorkRequestBuilder<UpdateNotificationStatusWorker>() .setInputData(workDataOf("is_active" to false)) .build() workManager.enqueue(request) } } // Worker实现类 class UpdateNotificationStatusWorker( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { @Inject lateinit var updateIsNotificationServiceActiveUseCase: UpdateIsNotificationServiceActiveUseCase init { // 初始化依赖注入 DaggerAppComponent.factory().create(applicationContext).inject(this) } override suspend fun doWork(): Result { val isActive = inputData.getBoolean("is_active", false) return try { updateIsNotificationServiceActiveUseCase.execute( UpdateIsNotificationServiceActiveUseCase.Companion.Param(isActive = isActive) ) Result.success() } catch (e: Exception) { Result.retry() } } }
WorkManager完全不依赖Service生命周期,也不会持有Service引用,是最安全的方案之一,适合需要保证执行的任务。
内容的提问来源于stack exchange,提问作者Net Nani

