I implemented a custom software keyboard using Compose to build the UI part and InputMethodService() for IME function interaction.
It works as expected. However, memory leak WindowOnBackInvokedDispatcher$OnBackInvokedCallbackWrapper appear when changing screen orientation or switching to a different soft keyboard from the implemented one and vice versa. It only happens on Android API 34 on physical devices like Motorola Edge 40 Pro, Motorola Edge 30 Neo and Samsung Galaxy S24. Also, this leak reproduces on Android Emulators: Intel x86_64 Atom System Image API 34, Google Apis Intel x86_64 Atom System Image API 34. I noticed interesting behavior in Intel x86_64 Atom System Image API 35 and Google Apis Intel x86_64 Atom System Image API 35, where LeakCanary found some/the same leaks. But after a few seconds, the message changed to “All retained objects were garbage collected.”
Leaks absent on physical devices: Moto G5 Plus Android 8.1 (27 Api), Moto Hyper One Android 11 (30 Api). And Android Emulators: Intel x86_64 Atom System Image API 33, Google Apis Intel x86_64 Atom System Image API 33, Google Play Intel x86_64 Atom System Image API 33, Google Play Intel x86_64 Atom System Image API 34, Google Play Intel x86_64 Atom System Image API 35.
Leak stack trace:
====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
References underlined with "~~~" are likely causes.
Learn more at .
299455 bytes retained by leaking objects
Signature: 680efbf9ebebd6f8946131812c75d1798f996c35
┬───
│ GC Root: Global variable in native code
│
├─ android.window.WindowOnBackInvokedDispatcher$OnBackInvokedCallbackWrapper instance
│ Leaking: UNKNOWN
│ Retaining 300.0 kB in 6630 objects
│ WindowOnBackInvokedDispatcher$OnBackInvokedCallbackWrapper is a binder stub. Binder stubs will often be retained
│ long after the associated activity or service is destroyed, as by design stubs are retained until the other side
│ gets GCed. If WindowOnBackInvokedDispatcher$OnBackInvokedCallbackWrapper is not a *static* inner class then that's
│ most likely the root cause of this leak. Make it static. If
│ WindowOnBackInvokedDispatcher$OnBackInvokedCallbackWrapper is an Android Framework class, file a ticket here:
│
│ ↓ WindowOnBackInvokedDispatcher$OnBackInvokedCallbackWrapper.mCallbackRef
│ ~~~~~~~~~~~~
├─ android.window.WindowOnBackInvokedDispatcher$OnBackInvokedCallbackWrapper$CallbackRef instance
│ Leaking: UNKNOWN
│ Retaining 299.5 kB in 6629 objects
│ ↓ WindowOnBackInvokedDispatcher$OnBackInvokedCallbackWrapper$CallbackRef.mStrongRef
│ ~~~~~~~~~~
├─ android.inputmethodservice.InputMethodService$$ExternalSyntheticLambda3 instance
│ Leaking: UNKNOWN
│ Retaining 299.5 kB in 6628 objects
│ f$0 instance of com.example.customsoftkeyboard.service.IMEHexadecimalService
│ ↓ InputMethodService$$ExternalSyntheticLambda3.f$0
│ ~~~
╰→ com.example.customsoftkeyboard.service.IMEHexadecimalService instance
Leaking: YES (ObjectWatcher was watching this because com.example.customsoftkeyboard.service.
IMEHexadecimalService received Service#onDestroy() callback and Service not held by ActivityThread)
Retaining 299.5 kB in 6627 objects
key = 81bcf4b0-11cc-407a-b4e5-66622602a44c
watchDurationMillis = 8244
retainedDurationMillis = 3242
mApplication instance of com.example.customsoftkeyboard.CustomSoftKeyboardApplication
mBase instance of android.app.ContextImpl
====================================
0 LIBRARY LEAKS
A Library Leak is a leak caused by a known bug in 3rd party code that you do not have control over.
See
====================================
0 UNREACHABLE OBJECTS
An unreachable object is still in memory but LeakCanary could not find a strong reference path
from GC roots.
====================================
METADATA
Please include this in bug reports and Stack Overflow questions.
Build.VERSION.SDK_INT: 34
Build.MANUFACTURER: unknown
LeakCanary version: 3.0-alpha-8
App process name: com.example.customsoftkeyboard
Class count: 30515
Instance count: 205029
Primitive array count: 147949
Object array count: 28315
Thread count: 22
Heap total bytes: 29012550
Bitmap count: 6
Bitmap total bytes: 31110
Large bitmap count: 0
Large bitmap total bytes: 0
Stats: LruCache[maxSize=3000,hits=106811,misses=137464,hitRate=43%]
RandomAccess[bytes=7127807,reads=137464,travel=54128216187,range=34936098,size=43515825]
Analysis duration: 16250 ms
Heap dump file path: /storage/emulated/0/Download/leakcanary-com.example.customsoftkeyboard/2025-03-13_16-40-15_225.
hprof
Heap dump timestamp: 1741884036842
Heap dump duration: Unknown
====================================
IMEHexadecimalService code:
import android.inputmethodservice.InputMethodService
import android.view.View
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.example.customsoftkeyboard.view.ComposeHexadecimalKeyBoardView
class IMEHexadecimalService : InputMethodService(), LifecycleOwner,
SavedStateRegistryOwner {
private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override val lifecycle: Lifecycle
get() = lifecycleRegistry
private val savedStateRegistryController = SavedStateRegistryController.create(this)
override val savedStateRegistry: SavedStateRegistry
get() = savedStateRegistryController.savedStateRegistry
override fun onCreateInputView(): View {
window?.window?.decorView?.let { decorView ->
decorView.setViewTreeLifecycleOwner(this)
decorView.setViewTreeSavedStateRegistryOwner(this)
}
return ComposeHexadecimalKeyBoardView(this)
}
override fun onCreate() {
super.onCreate()
savedStateRegistryController.performRestore(null)
handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onDestroy() {
super.onDestroy()
handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
private fun handleLifecycleEvent(event: Lifecycle.Event) =
lifecycleRegistry.handleLifecycleEvent(event)
}
It seems that this interesting behavior is connected to the Android Framework. There is a similar problem with this leak WindowOnBackInvokedDispatcher$OnBackInvokedCallbackWrapper
I would appreciate any information or ideas on how to fix this memory leak. Thanks for your attention!