代码背景:
- GlobalTaskService集成自原始的Android Service,并且动态声明为一个前台服务
- GlobalTaskService在Manifest 声明了
foregroundServiceType="phoneCall"(后续需要追加datasyns ) GlobalTaskService从onCreate就监听AirIMEngine(object单例)来自IM Server端发送来的数据(这是一个很常见的移动端和Server端的交互模型)
问题描述: 当app退到后台,GlobalTaskService按照产品设计预期,可以持续保活接收来自Server端的数据及进行后续处理,但实际现象是app在后台运行一段时间后,GlobalTaskService就会被自动杀掉
基于 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 尸体并没有从内存中清除。
代码现场:
// 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),没打电话别挂“正在通话”的牌子,会被系统封杀。