Android 后台存活率提升实战:从"被厂商杀"到推送稳定送达(Android 7–16)
T e l e g r a m \textcolor{red}{Telegram}Telegram:@lily059
开篇:先别急着写代码
“我的应用在小米上被杀得妈都不认识,在华为上活不过两小时。”
这句抱怨我在评论区看过太多次。但大多数人在动手之前,其实连自己的进程是被谁杀的都不知道——于是开始堆各种"保活奇技淫巧",最后既没解决问题,还把自己送进了应用市场的审核黑名单。
这篇文章不讲玄学,只讲三件事:
- 用系统 API定位真实死因,而不是猜;
- 系统允许你做的合规且有效的手段,以及各自的适用边界;
- 哪些做法是红线——技术上"能用",但会带来审核、合规和卸载率的代价。
一、先分清 5 种"死法"
进程被杀不是一件事,是五件事。搞错病因,药就不可能对。
| 类型 | 触发者 | 典型表现 | 能否用合规手段缓解 |
|---|---|---|---|
| LMK(低内存杀手) | 内核 | 内存吃紧时按 oom_adj 依次回收 | 可以:降低自身占用、提升 oom_adj 优先级 |
| 后台执行限制 | Android 8+ 框架 | 后台startService()抛IllegalStateException;隐式广播收不到 | 可以:前台服务、显式广播、WorkManager |
| Doze / App Standby | 系统省电 | 息屏静置后网络、闹钟、任务被延迟 | 可以:setExactAndAllowWhileIdle、电池优化白名单 |
| 厂商省电策略 | MIUI / EMUI / ColorOS / Funtouch… | 锁屏后清后台、自启动被拦、"关联启动"被禁 | 部分可以:引导用户开白名单,无法代码绕过 |
| 用户强停 / 划掉卡片 | 用户 | 最近任务划掉、设置里点"强制停止" | 不应该缓解——这是用户的明确意图 |
最后一行请记住:用户强停是用户的权利,不是 bug。后面会专门讲为什么去对抗它是笔亏本买卖。
二、用ApplicationExitInfo定位死因(这是本文最有价值的一段)
Android 11(API 30)之后,系统提供了官方接口查询进程历史退出原因,别再靠"我猜是 MIUI 杀的"了。
@RequiresApi(Build.VERSION_CODES.R)fundumpExitReasons(context:Context){valam=context.getSystemService(Context.ACTIVITY_SERVICE)asActivityManager// 参数:packageName(传 null 表示自己)、maxNum、maxNum 上限am.getHistoricalProcessExitReasons(null,0,20).forEach{info->Log.i("ExitInfo",""" time=${info.timestamp}reason=${info.reason.toReadable()}importance=${info.importance}// 被杀时的前台/后台状态desc=${info.description?:"-"}rss=${info.rss}// 峰值内存,排查 LMK 很有用pss=${info.pss}""".trimIndent())// 如果是崩溃/ANR,还能拿到 traceinfo.traceInputStream?.use{/* 交给崩溃平台解析 */}}}@RequiresApi(Build.VERSION_CODES.R)funInt.toReadable():String=when(this){ApplicationExitInfo.REASON_LOW_MEMORY->"LMK 内存不足被杀"ApplicationExitInfo.REASON_USER_REQUESTED->"用户主动结束/强停"ApplicationExitInfo.REASON_OTHER->"系统回收(厂商策略常见)"ApplicationExitInfo.REASON_FREEZER->"被系统冻结后回收(Android 12+)"ApplicationExitInfo.REASON_ANR->"ANR"ApplicationExitInfo.REASON_CRASH->"Java 崩溃"ApplicationExitInfo.REASON_CRASH_NATIVE->"Native 崩溃"ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE->"资源占用超限被回收"ApplicationExitInfo.REASON_DEPENDENCY_DIED->"依赖进程退出连带"ApplicationExitInfo.REASON_INITIALIZATION_FAILURE->"启动初始化失败"else->"其他($this)"}怎么读这些数据:
- 大量
REASON_LOW_MEMORY→ 你的常驻内存太大,先做内存优化,别急着加服务; - 大量
REASON_OTHER/REASON_FREEZER→ 大概率是厂商策略,属于"引导用户开白名单"的场景; REASON_USER_REQUESTED占比高 →停下来想想产品体验,用户在用脚投票;REASON_CRASH*/REASON_ANR→ 先修 bug,崩溃的进程谈何存活。
小提示:这套 API 在部分 OEM ROM 上粒度有限,
REASON_OTHER占比会偏高。配合"进程启动时间 + 前后台切换时间点"一起埋点,才能还原完整链路。
三、合规且有效的手段(按场景选型)
3.1 前台服务:首选,但类型必须选对
Android 14(API 34)起foregroundServiceType是强制项,且必须声明对应的FOREGROUND_SERVICE_*权限,否则直接崩溃。
<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE"/><uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/><serviceandroid:name=".SyncService"android:exported="false"android:foregroundServiceType="dataSync"/>overridefunonStartCommand(intent:Intent?,flags:Int,startId:Int):Int{startForeground(NOTIFY_ID,buildNotification(),// 必须用户可见,别做成透明通知ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC// Android 10+ 可显式指定)returnSTART_STICKY}几个必须知道的边界:
- 类型不是随便选的:
dataSync用于数据同步,mediaPlayback用于播放,location用于导航——选错类型在市场审核时会被打回; - Android 15 起,
dataSync/mediaProcessing类型的前台服务有"24 小时内累计 6 小时"的时长上限,超时会收到onTimeout()回调,必须在那里收尾,别装作没看见; - 从后台启动前台服务有限制(Android 12+ 收紧了 exception 列表),Android 15 起
SYSTEM_ALERT_WINDOW也不再是豁免理由。想靠"悬浮窗权限换后台起服务"的老套路已经不通了。
3.2 推送通道:真正决定"消息能不能到"的,是它
如果你的诉求是"消息别丢",那么长连接和推送通道的权重,远高于进程存活。进程被杀后能被推送拉起,比死撑进程省电得多。
- 海外:老老实实用FCM;
- 国内:接入厂商推送(小米、华为 HMS、OPPO、vivo、荣耀、魅族)是硬需求——这些通道在应用被杀后仍可送达,而且不需要你保活;
- 需要自建长连接时,做好断线重连退避、心跳与厂商通道的互补(在线走长连接,离线走推送)。
一句话:能用推送解决的,不要用保活解决。
3.3 WorkManager:可延迟任务的正确姿势
valrequest=PeriodicWorkRequestBuilder<SyncWorker>(15,TimeUnit.MINUTES).setBackoffCriteria(BackoffPolicy.EXPONENTIAL,10,TimeUnit.MINUTES).build()WorkManager.getInstance(context).enqueueUniquePeriodicWork("sync",ExistingPeriodicWorkPolicy.KEEP,request)- 周期任务最小间隔 15 分钟,别指望它做秒级轮询;
ExistingPeriodicWorkPolicy.KEEP避免每次启动都重建任务;- WorkManager保证会执行,但不保证准时(受 Doze / 电池策略影响),对时效敏感就别用它。
3.4 精确闹钟:注意 Android 12/13 的权限变化
valam=context.getSystemService(AlarmManager::class.java)valtriggerAt=System.currentTimeMillis()+5*60_000valcanExact=if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){am.canScheduleExactAlarms()// Android 12+ 必须运行时判断}elsetrueif(canExact){am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,triggerAt,pendingIntent)}else{// 降级为不精确闹钟,同样能穿透 Doze,只是时间有偏差am.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,triggerAt,pendingIntent)}权限上有两个坑:
SCHEDULE_EXACT_ALARM(Android 12+):用户可撤销,必须运行时判断canScheduleExactAlarms();USE_EXACT_ALARM(Android 13+):仅限闹钟、日历等以精确时间为核心功能的应用,Google Play 有明确的用途审核,别拿来当"免申请精确闹钟"的后门。
3.5 电池优化白名单:能要,但要说清为什么
valpm=context.getSystemService(PowerManager::class.java)if(!pm.isIgnoringBatteryOptimizations(packageName)){// 先给用户一个"为什么"的说明页,再跳转,转化率会高很多startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).setData(Uri.parse("package:$packageName")))}注意:Google Play 对REQUEST_IGNORE_BATTERY_OPTIMIZATIONS的使用场景有白名单限制(报警、VoIP 等),滥用会被下架。要在隐私政策与商店说明里写清楚用途。
3.6 厂商自启动 / 后台白名单:没有统一 API,只能好好引导
这是国内场景绕不开的一环,但要接受一个现实:没有稳定、官方支持的跳转接口。
- 各家设置路径都不同(MIUI 自启动、华为"应用启动管理"、OPPO/vivo"后台高耗电"、三星"未监控的应用"…),写死厂商 Activity 的做法会随 ROM 版本失效;
- 稳妥做法:先
try/catch尝试跳转,失败就兜底到本应用详情页,让用户自己找到开关;
// 兜底方案:永远不会失效startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(Uri.parse("package:$packageName")))- 更好的做法是在 App 内做图文引导(机型识别 + 截图指引),比直接甩一个系统设置页有效得多。
3.7 如果是真有硬件,用CompanionDeviceManager
应用确实要长期和 BLE 设备(手环、车机、健康设备)保持连接时,Android 12+ 的CompanionDeviceService是官方为你准备的正当方案,系统会给予后台运行豁免。
前提是:你真的有配对设备。为了保活伪造一个 BLE 关联,既过不了审核,也拿不到系统信任。
3.8 系统绑定服务:功能必须匹配
NotificationListenerService、AccessibilityService、DeviceAdminService、VpnService都能带来极高的进程优先级,但它们都有强烈的"用途契约":
- 通知监听 → 你要真的做通知聚合/过滤;
- 无障碍 → 你要真的有辅助功能(否则属高危,Play 会重点审查);
- VPN → 你要真的做流量处理,而不是建一个 127.0.0.1 的回环隧道;
- DeviceAdmin → 你要真的是企业设备管理场景。
"为了保活而申请"是这个领域最常见的自杀行为:要么审核不过,要么被用户投诉后下架。
四、明确劝退的反模式(红线)
下面这些在技术上都"做得到",但我不建议你在任何要上架的产品里使用,理由写在后面。
| 反模式 | 为什么不要做 |
|---|---|
| 1 像素 / 透明 Activity 拉活 | 违反 Android 10+ 后台启动 Activity 限制;"莫名弹出"是用户投诉与"欺骗行为"判定的高发点 |
| 播放无声音频 | 抢占音频焦点、显著增加耗电;部分 ROM 直接判定异常进程 |
| 空 SyncAdapter / 空账户 只为触发同步 | 账户与同步必须有真实用途,属审核重点 |
| 双进程 / 空进程互拉 | Android 8+ 后台服务限制下已基本失效,且明确被视为滥用 |
滥用 MediaStyle / CallStyle 绕过POST_NOTIFICATIONS | 这是绕过权限管控,与 Android 13+ 的通知权限设计初衷冲突,风险极高 |
| 反射隐藏 API、直调 AMS、“防强停” | 用户在设置里点"强制停止"是明确意图;对抗它=更高的卸载率 + 市场审核风险 |
am instrument拉活 | 滥用测试框架,属明确异常行为 |
需要特别说明"防强停":这类方案在某些灰色场景(如靠后台复活刷广告曝光)里被当作卖点,但对正经产品它是负资产——你对抗的是掏钱买你服务的用户,赢了一次强停,输掉的是留存和口碑。
合规上还有一条通用红线:Google Play 的Device and Network Abuse、Foreground Service 政策,以及国内各应用市场的后台行为规范,都会对上述行为做拦截或下架处理。
五、落地:指标 + 选型表
5.1 建议埋点的指标
| 指标 | 采集方式 | 用来判断 |
|---|---|---|
| 进程退出原因分布 | ApplicationExitInfo | 死因构成,方向对不对 |
| 前后台切换时间点 | onTrimMemory(TRIM_MEMORY_UI_HIDDEN)/ProcessLifecycleOwner | 被杀时是否在后台 |
| 冷启动次数 / 日 | 启动埋点去重 | 存活率最直观的代理指标 |
| 推送到达率 | 推送平台侧数据 | 最终业务指标,比存活率更重要 |
| 前台服务时长与超时次数 | onTimeout()回调 | Android 15 时长限制是否踩线 |
5.2 场景 → 方案
| 场景 | 推荐组合 |
|---|---|
| IM / 社交消息 | 厂商推送 + 前台服务(dataSync)+ WorkManager 兜底 |
| 音乐 / 播客播放 | 前台服务(mediaPlayback)+ MediaSession(真播放时) |
| 导航 / 运动轨迹 | 前台服务(location)+ 精准闹钟 |
| IoT / BLE 外设 | CompanionDeviceService+ 前台服务 |
| 企业设备管理 | Device Owner +DeviceAdminService |
| 天气 / 资讯定时刷新 | WorkManager(15 分钟 +) |
| 闹钟 / 提醒类 | 精确闹钟 +USE_EXACT_ALARM(符合政策前提) |
六、上线前检查清单
- 已用
ApplicationExitInfo统计过真实死因分布,且方向与数据匹配 - 前台服务:类型选对、
FOREGROUND_SERVICE_*权限齐全、通知用户可见 - Android 15 时长限制场景已实现
onTimeout()收尾 - 已完成 FCM / 厂商推送接入,消息链路不依赖"进程活着"
- 精确闹钟做了
canScheduleExactAlarms()判断与降级 - 电池优化白名单、自启动引导均有用途说明,且跳转有兜底
- 未使用第四节表格里的任何反模式
- 隐私政策、商店说明与申请的权限一一对应
- 真机矩阵覆盖:小米 / 华为 / OPPO / vivo / 三星 / 原生
结语
后台存活率这件事,核心不是"堆多少种保活手段",而是"用对系统给你的那把钥匙":把消息交给推送通道,把可延迟任务交给 WorkManager,把持续工作交给带正确类型的前台服务,把系统限制的边界用引导和授权去沟通——而不是去绕过它。
你的机型上有遇到过
REASON_OTHER特别高的死亡姿势吗?欢迎在评论区贴出你的exitReason分布,一起看看是谁在动手。