后台服务Service销毁逻辑+单例造成的内存泄露

这份总结完全基于你的 GlobalTaskServiceAirIMEngine 的现场问题。我们将这个问题拆解为三个核心维度:保活机制失效单例内存泄露Android 14 合规陷阱

你可以把这份总结作为后续开发后台类应用的技术备忘录。


一、 现场还原:为什么"息屏即死,亮屏诈尸"?

现象描述: 你观察到 GlobalTaskService 在息屏时打印 onDestroy,亮屏时打印 onCreate。特别是当 IM 连接建立(持有 Socket)后,这种现象更频繁。

核心原因:这也是 Android 系统的"杀手"逻辑

  1. 伪后台服务 :你的代码中 startForeground 被注释掉了。对系统而言,这是一个普通的后台 Service。
  2. 省电策略 (Doze Mode):当屏幕关闭,系统进入打盹模式。系统检测到你的 App 在后台跑着,还持有一个高耗电的 Socket 连接(IM),但用户界面上没有任何通知(Notification)显示你在运行。
  3. 系统判定 :"这个 App 在偷偷摸摸耗电,杀掉它。" -> 触发 onDestroy
  4. 诈尸重启 :因为你在 onStartCommand 返回了 START_STICKY。当亮屏系统资源宽裕时,系统会尝试重建服务 -> 触发 onCreate

结论 :不调用 startForeground,Service 在现代 Android 系统中就像"无证驾驶",随时会被交警(系统)扣车。


二、 隐形杀手:Lambda 导致的内存泄露

现象描述: 虽然 Service 被销毁重建了,但旧的 Service 尸体并没有从内存中清除。

代码现场:

kotlin 复制代码
// GlobalTaskService.onCreate
AirIMEngine.addMsgObserver { msg, sender -> 
    // 这个 Lambda 内部隐式持有了 Service 的引用 (this)
    dispatchMsgScene(...) 
}

泄露逻辑闭环:

  1. 谁活着? AirIMEngineobject (单例),它的生命周期 = App 进程的生命周期(它永远活着)。
  2. 谁被抓住了? 当你注册 Lambda 时,AirIMEngine 里的列表(或变量)就引用了这个 Lambda。而 Lambda 为了能调用 dispatchMsgScene,必须持有 GlobalTaskService 的实例。
  3. 悲剧发生:
    • 第一次 Service 启动 -> AirIMEngine 抓住 Service_V1
    • 息屏,系统杀掉 Service -> Service_V1onDestroy但在内存中,因为 AirIMEngine 还抓着它,它无法被回收(GC)。
    • 亮屏,Service 重启 -> 创建 Service_V2
    • Service_V2 再次向 AirIMEngine 注册。
  4. 结果 :内存里同时存在 Service_V1 (僵尸) 和 Service_V2 (活体)。如果不改,在这个 App 的生命周期内,僵尸会越堆越多。

解决方案的核心 : 从 Lambda 改为 接口 (interface) 。因为接口允许我们明确地传入 this,也允许我们在 onDestroy 中明确地调用 removeListener(this),打断单例对 Service 的引用链。


三、 合规陷阱:Android 14 的"类型"强校验

潜在风险: 你的 Manifest 声明了 foregroundServiceType="phoneCall",这在 Android 14+ 是高危操作。

逻辑冲突:

  1. 场景 A(待机) :App 启动,IM 连接中,但没打电话。
    • 系统检查:你启动了前台服务,声明是 phoneCall
    • 系统质问:你现在的 TelecomManager 里有通话会话吗?
    • 回答:没有。
    • 结果 :抛出 ForegroundServiceStartNotAllowedException 或直接 Crash。
  2. 场景 B(通话) :App 真的在自动拨打电话。
    • 此时用 phoneCall 才是合法的。

解决方案 : 采用 "双模切换" 策略。

  • 待机模式 :使用 dataSync 类型(数据同步),告诉系统我在维持 IM 连接,这是合法的后台行为。
  • 通话模式 :检测到拨号指令后,动态更新 Notification,将类型切换为 phoneCall

四、 总结架构图(技术复盘)

这是我们最终修复后的架构逻辑,你可以对照代码理解:

graph TD A[GlobalTaskService 启动] --> B(onCreate); B --> C{修复点1: 立即启动前台服务}; C -->|待机状态| D[startForeground type=DATA_SYNC]; D -- 说明 --> E[系统不再杀后台, 允许持有Socket]; B --> F{修复点2: 注册IM监听}; F -->|AirIMEngine.addListener this | G[AirIMEngine (单例)]; G -- 关键 --> H[使用 WeakReference 或 明确的 Remove]; I[收到拨号指令] --> J{修复点3: 动态切换类型}; J --> K[更新 Notification type=PHONE_CALL]; K -- 说明 --> L[Android 14 允许使用通话API]; M[GlobalTaskService 销毁] --> N(onDestroy); N --> O[AirIMEngine.removeListener this]; O -- 结果 --> P[断开引用链, Service内存成功回收]; N --> Q[stopForeground];

核心心法(记忆口诀)

  1. 要保活,必前台:没有 Notification 的 Service 在息屏时就是系统的"猎物"。
  2. 用单例,必有借有还 :在 onCreateadd 进单例的东西,必须在 onDestroyremove 掉,否则就是内存泄露。
  3. Android 14,类型要对版:干什么事挂什么牌(Type),没打电话别挂"正在通话"的牌子,会被系统封杀。
相关推荐
GoldenPlayer2 小时前
自定义APK&gradle全局配置文件
android
うちは止水2 小时前
Android Hal层开发流程
android·hal
李小轰_Rex2 小时前
把手机变成听诊器!摄像头 30 秒隔空测心率 - 开箱即用
android·音视频开发
为码消得人憔悴4 小时前
Android perfetto - 记录分析memory
android·性能优化
尤老师FPGA4 小时前
使用ZYNQ芯片和LVGL框架实现用户高刷新UI设计系列教程(第四十二讲)
android·java·ui
成都大菠萝4 小时前
2-2-29 快速掌握Kotlin-过滤函数filter
android
成都大菠萝4 小时前
2-2-18 快速掌握Kotlin-扩展属性
android
成都大菠萝4 小时前
2-2-21 快速掌握Kotlin-定义扩展文件
android
成都大菠萝4 小时前
2-2-19 快速掌握Kotlin-可空类型扩展函数
android