You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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记录。可尝试以下方案:

临时修复方案

  1. 清理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)
}
  1. 分离生命周期持有者:创建静态内部类实现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)
        }
    }
}
  1. 主动触发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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 20:20:53