RxAndroidBle: scanBleDevices causes memory leak

Summary

Doing a basic scan of ble devices causes a memory leak despite ending the scan subscription at onStop().

Library version

1.10.1

Preconditions

Have sample app cloned & loaded: https://github.com/seljabali/rxandroidble2-leak

Steps to reproduce actual result


1. Enable Bluetooth on device
2. Enable GPS
3. Open app
4. Hit back button
5. Pull system drop down
6. Wait for Canary to analyze heap
7. Canary momentarily shows leak.

Actual result

  • Canary showing a memory leak

Expected result

  • Canary not showing a memory leak

Minimum code snippet reproducing the issue

scanDisposable = RxBle.get()
    .scanBleDevices(
        ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
            .build()
    )
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .doOnError {
        onScanError() // commenting this out stops leak
    }
    .doOnNext {
    }
    .subscribe()

Logs from the application running with settings:

https://github.com/seljabali/rxandroidble2-leak/blob/master/leakLog.txt

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 2
  • Comments: 18 (11 by maintainers)

Most upvoted comments

This looks exactly as before, starting the leak at Android’s BleScanCallbackWrapper. I will try to run it once I will get my hands on an API 29 device.

Hello, I’ve created a PR with a solution to the issue

https://github.com/Polidea/RxAndroidBle/pull/708

Hope it can be merged soon

Great investigative efforts @dariuszseweryn! Thank you. I’ll create a Google Issue accordingly, and post it back here as a comment – closing ticket.

I have modified the example so it does not use RxAndroidBle at all and run the vanilla API. Changes looks like this:

    var scanCallback: ScanCallback? = null

    private fun startScan() {
        scanCallback = object : ScanCallback() {
            override fun onScanFailed(errorCode: Int) {
                super.onScanFailed(errorCode)
                Log.e("onScanFailed", errorCode.toString())
                onScanError()
            }

            override fun onScanResult(callbackType: Int, result: ScanResult?) {
                super.onScanResult(callbackType, result)
                Log.e("onScanResult", callbackType.toString())
            }

            override fun onBatchScanResults(results: MutableList<ScanResult>?) {
                super.onBatchScanResults(results)
                Log.e("onBatchScanResults", results.toString())
            }
        }
        BluetoothAdapter.getDefaultAdapter().bluetoothLeScanner.startScan(scanCallback!!)
    }

    private fun stopScan() {
        BluetoothAdapter.getDefaultAdapter().bluetoothLeScanner.stopScan(scanCallback!!)
        scanCallback = null
    }

LeakCanary result:

2019-09-12 17:58:11.660 14017-14262/com.seljabali.rxandroidble2leak D/LeakCanary: HeapAnalysisSuccess(heapDumpFile=/data/user/0/com.seljabali.rxandroidble2leak/files/leakcanary/2019-09-12_17-57-50_005.hprof, createdAtTimeMillis=1568303891655, analysisDurationMillis=19414, applicationLeaks=[ApplicationLeak(className=com.seljabali.rxandroidble2leak.ScanBleFragment, leakTrace=
    ┬
    ├─ android.bluetooth.le.BluetoothLeScanner$BleScanCallbackWrapper
    │    Leaking: UNKNOWN
    │    GC Root: Global variable in native code
    │    ↓ BluetoothLeScanner$BleScanCallbackWrapper.mScanCallback
    │                                                ~~~~~~~~~~~~~
    ├─ com.seljabali.rxandroidble2leak.ScanBleFragment$startScan$1
    │    Leaking: UNKNOWN
    │    Anonymous subclass of android.bluetooth.le.ScanCallback
    │    ↓ ScanBleFragment$startScan$1.this$0
    │                                  ~~~~~~
    ╰→ com.seljabali.rxandroidble2leak.ScanBleFragment
    ​     Leaking: YES (Fragment#mFragmentManager is null and ObjectWatcher was watching this)
    ​     key = dc9bf023-d4b3-41a6-b2ec-1a8bf3566e01
    ​     watchDurationMillis = 5152
    ​     retainedDurationMillis = 149
    , retainedHeapByteSize=1933)], libraryLeaks=[])

I think that this issue should be checked against the newest Android OS version and reported on Google Issue Tracker

I have looked through the classes that are referenced by the trace but all of those should be garbage collected normally with their Observable chains. I do not see anything leaking in either ScanOperationApi21 (which is held by the OS) nor in FIFORunnableEntry which is held because the operation is not collected.

Correct me if I am wrong but it seems that the root-cause of the leak is

2019-09-04 18:25:36.760 20056-20556/com.seljabali.rxandroidble2leak D/LeakCanary: ┬
2019-09-04 18:25:36.760 20056-20556/com.seljabali.rxandroidble2leak D/LeakCanary: ├─ android.bluetooth.le.BluetoothLeScanner$BleScanCallbackWrapper
2019-09-04 18:25:36.760 20056-20556/com.seljabali.rxandroidble2leak D/LeakCanary: │    Leaking: UNKNOWN
2019-09-04 18:25:36.760 20056-20556/com.seljabali.rxandroidble2leak D/LeakCanary: │    GC Root: Global variable in native code
2019-09-04 18:25:36.760 20056-20556/com.seljabali.rxandroidble2leak D/LeakCanary: │    ↓ BluetoothLeScanner$BleScanCallbackWrapper.mScanCallback

Which is a part of Android OS — not library’s?