这份总结完全基于你的 GlobalTaskService 和 AirIMEngine 的现场问题。我们将这个问题拆解为三个核心维度:保活机制失效 、单例内存泄露 、Android 14 合规陷阱。
你可以把这份总结作为后续开发后台类应用的技术备忘录。
一、 现场还原:为什么"息屏即死,亮屏诈尸"?
现象描述: 你观察到 GlobalTaskService 在息屏时打印 onDestroy,亮屏时打印 onCreate。特别是当 IM 连接建立(持有 Socket)后,这种现象更频繁。
核心原因:这也是 Android 系统的"杀手"逻辑
- 伪后台服务 :你的代码中
startForeground被注释掉了。对系统而言,这是一个普通的后台 Service。 - 省电策略 (Doze Mode):当屏幕关闭,系统进入打盹模式。系统检测到你的 App 在后台跑着,还持有一个高耗电的 Socket 连接(IM),但用户界面上没有任何通知(Notification)显示你在运行。
- 系统判定 :"这个 App 在偷偷摸摸耗电,杀掉它。" -> 触发
onDestroy。 - 诈尸重启 :因为你在
onStartCommand返回了START_STICKY。当亮屏系统资源宽裕时,系统会尝试重建服务 -> 触发onCreate。
结论 :不调用 startForeground,Service 在现代 Android 系统中就像"无证驾驶",随时会被交警(系统)扣车。
二、 隐形杀手:Lambda 导致的内存泄露
现象描述: 虽然 Service 被销毁重建了,但旧的 Service 尸体并没有从内存中清除。
代码现场:
kotlin
// GlobalTaskService.onCreate
AirIMEngine.addMsgObserver { msg, sender ->
// 这个 Lambda 内部隐式持有了 Service 的引用 (this)
dispatchMsgScene(...)
}
泄露逻辑闭环:
- 谁活着?
AirIMEngine是object(单例),它的生命周期 = App 进程的生命周期(它永远活着)。 - 谁被抓住了? 当你注册 Lambda 时,
AirIMEngine里的列表(或变量)就引用了这个 Lambda。而 Lambda 为了能调用dispatchMsgScene,必须持有GlobalTaskService的实例。 - 悲剧发生:
- 第一次 Service 启动 ->
AirIMEngine抓住Service_V1。 - 息屏,系统杀掉 Service ->
Service_V1走onDestroy。但在内存中,因为AirIMEngine还抓着它,它无法被回收(GC)。 - 亮屏,Service 重启 -> 创建
Service_V2。 Service_V2再次向AirIMEngine注册。
- 第一次 Service 启动 ->
- 结果 :内存里同时存在
Service_V1(僵尸) 和Service_V2(活体)。如果不改,在这个 App 的生命周期内,僵尸会越堆越多。
解决方案的核心 : 从 Lambda 改为 接口 (interface) 。因为接口允许我们明确地传入 this,也允许我们在 onDestroy 中明确地调用 removeListener(this),打断单例对 Service 的引用链。
三、 合规陷阱:Android 14 的"类型"强校验
潜在风险: 你的 Manifest 声明了 foregroundServiceType="phoneCall",这在 Android 14+ 是高危操作。
逻辑冲突:
- 场景 A(待机) :App 启动,IM 连接中,但没打电话。
- 系统检查:你启动了前台服务,声明是
phoneCall。 - 系统质问:你现在的
TelecomManager里有通话会话吗? - 回答:没有。
- 结果 :抛出
ForegroundServiceStartNotAllowedException或直接 Crash。
- 系统检查:你启动了前台服务,声明是
- 场景 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];
核心心法(记忆口诀)
- 要保活,必前台:没有 Notification 的 Service 在息屏时就是系统的"猎物"。
- 用单例,必有借有还 :在
onCreate里add进单例的东西,必须在onDestroy里remove掉,否则就是内存泄露。 - Android 14,类型要对版:干什么事挂什么牌(Type),没打电话别挂"正在通话"的牌子,会被系统封杀。