问题
My coroutine leaks a broadcast receiver, when the service is stopped. This is because the service stops, before the callback is finished. How can I cancel coroutine in a way that lets me unregister the reciever?
The Service
works like this:
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()
}
}
Here is the class where the BroadcastReciever
is not unregistered properly in 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()
}
}
Example stacktrace:
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$1@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$1@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$1$wifi$1.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 ...
回答1:
The answer was indeed to use suspendCancellableCoroutine
and define cont.invokeOnCancellation
as written below:
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()
}
来源:https://stackoverflow.com/questions/53517497/coroutine-unregister-reciever-on-cancel