skip to Main Content

Hello hope someone can help me.

Problem: In code #1. The code inside "onListenerDisconnected" will not finish executing because the coroutine will be cancelled via onDestroy.

code #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()
        }
    }
}

My defective Solution: Use a coroutine that outlive the service lifecycle which in this case I used a coroutine that is attached to the application lifecycle. but when I do this leak canary detects memory leak says that NotificationService is leaking

@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
                )
            )
        }
    }
}

Question:

  1. Should I be concerned with the memory leak that leak canary detects or is this just a small price and can be ignored to achieve what i want to do?
┬───
│ 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. If this memory leak cant be ignored what other way can I do this without memory leak?

2

Answers


  1. Chosen as BEST ANSWER

    Thanks to @Tenfour04 that gave me an idea on how to solve this. Instead of injecting the lifecyclescope inside the service I injected it inside the usecase. I have verified it with leak canary and it says all retained objects are garbage collected

    class UpdateIsNotificationServiceActiveUseCase @Inject constructor(
        private val appPreferenceRepository: AppPreferenceRepository,
        private val lifeCycleScope: CoroutineScope
    ) : NonSuspendingBaseUseCase<UpdateIsNotificationServiceActiveUseCase.Companion.Param, Unit> {
    
        companion object {
            data class Param(
                val isActive: Boolean
            )
        }
    
        override fun execute(param: Param): Result<Unit> {
            lifeCycleScope.launch {
                appPreferenceRepository.setIsNotificationServiceActive(value = param.isActive)
            }
            return Result.Success(Unit)
        }
    }
    

  2. I think your problem in the second solution with the injected CoroutineScope is that the coroutine is capturing a reference to your whole class because it uses the updateIsNotificationServiceActiveUseCase property of that class. So, copy the value of that property to a local variable before you launch the coroutine and use only the local variable in the coroutine:

    override fun onListenerConnected() {
        val useCase = updateIsNotificationServiceActiveUseCase
        lifeCycleScope.launch {
            useCase.execute(
                UpdateIsNotificationServiceActiveUseCase.Companion.Param(
                    isActive = true
                )
            )
        }
    }
    

    "lifeCycleScope" seems to be a misleading name for this scope since you explicitly designed it to outlive this class’s lifecycle.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search