Android 14(API34) InputMethodService销毁时WindowOnBackInvokedDispatcher内存泄漏问题
自定义软键盘在Android 14(API34)下的内存泄漏问题
问题现象
基于Compose构建UI、InputMethodService实现IME交互逻辑的自定义软键盘功能正常,但在Android 14(API34)环境下,切换屏幕方向或切换软键盘时,会出现WindowOnBackInvokedDispatcher$OnBackInvokedCallbackWrapper相关的内存泄漏。
复现环境
- 泄漏复现:摩托罗拉Edge 40 Pro、三星Galaxy S24等API34物理设备,Intel x86_64 Atom API34模拟器
- 特殊情况:API35部分模拟器中,LeakCanary会先检测到泄漏,但几秒后提示"All retained objects were garbage collected."
- 无泄漏环境:API27、30物理设备,API33全系列模拟器
LeakCanary堆栈信息
==================================== HEAP ANALYSIS RESULT ==================================== 1 APPLICATION LEAKS References underlined with "~~~" are likely causes. Learn more at https://squ.re/leaks. 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: │ https://issuetracker.google.com/issues/new?component=192705 │ ↓ 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 https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#4-categorizing-leaks ==================================== 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)
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) }
修复思路
该泄漏属于Android框架层问题,已有相似官方Issue记录。可尝试以下方案:
临时修复方案
- 清理ViewTree引用:在
onDestroy中手动解除decorView与生命周期持有者的绑定,避免Service被意外引用:
override fun onDestroy() { window?.window?.decorView?.let { decorView -> decorView.setViewTreeLifecycleOwner(null) decorView.setViewTreeSavedStateRegistryOwner(null) } super.onDestroy() handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) }
- 分离生命周期持有者:创建静态内部类实现
LifecycleOwner和SavedStateRegistryOwner,避免直接将Service本身作为持有者传递,减少强引用风险:
class IMEHexadecimalService : InputMethodService() { private val lifecycleHolder = LifecycleHolder() override fun onCreateInputView(): View { window?.window?.decorView?.let { decorView -> decorView.setViewTreeLifecycleOwner(lifecycleHolder) decorView.setViewTreeSavedStateRegistryOwner(lifecycleHolder) } return ComposeHexadecimalKeyBoardView(this) } override fun onCreate() { super.onCreate() lifecycleHolder.savedStateRegistryController.performRestore(null) lifecycleHolder.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) } override fun onDestroy() { window?.window?.decorView?.let { decorView -> decorView.setViewTreeLifecycleOwner(null) decorView.setViewTreeSavedStateRegistryOwner(null) } lifecycleHolder.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) super.onDestroy() } private class LifecycleHolder : LifecycleOwner, SavedStateRegistryOwner { private val lifecycleRegistry = LifecycleRegistry(this) val savedStateRegistryController = SavedStateRegistryController.create(this) override val lifecycle: Lifecycle get() = lifecycleRegistry override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry fun handleLifecycleEvent(event: Lifecycle.Event) { lifecycleRegistry.handleLifecycleEvent(event) } } }
- 主动触发GC(临时应急):针对API34及以上版本,在Service销毁时主动触发一次GC(不推荐生产环境频繁使用):
override fun onDestroy() { super.onDestroy() handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { System.gc() } }
长期方案
关注官方Issue修复进度,等待Android系统更新解决该框架层泄漏问题。
内容的提问来源于stack exchange,提问作者Pretorian
相关产品推荐
相关产品推荐

