协程在取消时注销接收者

Coroutine unregister reciever on cancel

当服务停止时,我的协程泄漏了一个广播接收器。这是因为服务在回调完成之前停止。我怎样才能以一种让我注销接收者的方式取消协程?

Service 是这样工作的:

class DataCollectorService : Service(){
    var job : Job? = null

    override fun onStartCommand(...){
        job = GlobalScope.launch {
            val location = async { wifiScanner.getCurrentLocation() }
            //other asynchronous jobs
            location.await() 
            //do something with location
        }
    }

    override fun onDestroy(){
         job?.cancel()
    }
}

这是 class,其中 BroadcastReciever 未在 onDestroy 中正确注销:

class WifiScanner(val context: ContextWrapper) {
    val wifiManager: WifiManager

    init {
        wifiManager = context.baseContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
    }

    suspend fun getCurrentScanResult(): List<ScanResult> =
        suspendCoroutine { cont ->
            val wifiScanReceiver = object : BroadcastReceiver() {
                override fun onReceive(c: Context, intent: Intent) {
                    if (intent.action?.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) == true) {
                        context.unregisterReceiver(this)
                        cont.resume(wifiManager.scanResults)
                    }
                }
            }
            context.registerReceiver(wifiScanReceiver, IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
            wifiManager.startScan()
        }
}

示例堆栈跟踪:

de.leo.smartTrigger E/ActivityThread: Service de.leo.smartTrigger.datacollector.datacollection.DataCollectorService has leaked IntentReceiver de.leo.smartTrigger.datacollector.datacollection.sensors.WifiScanner$getCurrentScanResult$$inlined$suspendCoroutine$lambda@6febc2f that was originally registered here. Are you missing a call to unregisterReceiver()?
        android.app.IntentReceiverLeaked: Service de.leo.smartTrigger.datacollector.datacollection.DataCollectorService has leaked IntentReceiver de.leo.smartTrigger.datacollector.datacollection.sensors.WifiScanner$getCurrentScanResult$$inlined$suspendCoroutine$lambda@6febc2f that was originally registered here. Are you missing a call to unregisterReceiver()?
            at android.app.LoadedApk$ReceiverDispatcher.<init>(LoadedApk.java:1355)
            at android.app.LoadedApk.getReceiverDispatcher(LoadedApk.java:1120)
            at android.app.ContextImpl.registerReceiverInternal(ContextImpl.java:1428)
            at android.app.ContextImpl.registerReceiver(ContextImpl.java:1401)
            at android.app.ContextImpl.registerReceiver(ContextImpl.java:1389)
            at android.content.ContextWrapper.registerReceiver(ContextWrapper.java:622)
            at de.leo.smartTrigger.datacollector.datacollection.sensors.WifiScanner.getCurrentScanResult(WifiScanner.kt:35)
            at de.leo.smartTrigger.datacollector.datacollection.DataCollectorService.getWifi(DataCollectorService.kt:219)
            at de.leo.smartTrigger.datacollector.datacollection.DataCollectorService$uploadDataSet$wifi.invokeSuspend(DataCollectorService.kt:273)
            at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
            at kotlinx.coroutines.DispatchedTask$DefaultImpls.run(Dispatched.kt:221)
            at kotlinx.coroutines.DispatchedContinuation.run(Dispatched.kt:67)
            at ...

答案确实是使用 suspendCancellableCoroutine 并定义 cont.invokeOnCancellation 如下所示:

suspend fun getCurrentScanResult(): List<ScanResult> =
        suspendCancellableCoroutine { cont ->
            //define broadcast reciever
            val wifiScanReceiver = object : BroadcastReceiver() {
                override fun onReceive(c: Context, intent: Intent) {
                    if (intent.action?.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) == true) {
                        context.unregisterReceiver(this)
                        cont.resume(wifiManager.scanResults)
                    }
                }
            }
            //setup cancellation action on the continuation
            cont.invokeOnCancellation {
                context.unregisterReceiver(wifiScanReceiver)
            }
            //register broadcast reciever
            context.registerReceiver(wifiScanReceiver, IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
            //kick off scanning to eventually receive the broadcast
            wifiManager.startScan()
        }