简介:《进程常驻探索》压缩包内含csdnactivity与servicelive1两个安卓工程模块,面向需要深入理解服务常驻与进程优先级调优的移动开发者和系统开发者,解决后台服务容易被回收、关键进程无法持续运行的难题。项目从实际代码出发,系统演示守护进程、服务管理器、启动脚本以及Supervisor/NSSM等第三方工具的常驻方案,并结合nice值、Windows优先级类等调度策略,帮助读者理解进程创建、执行、等待与结束的生命周期机制。资源共69个文件、16.9MB,以20个Java源码和18个XML配置为主体,配有20张PNG图解(覆盖工程结构、运行流程与优先级说明),另有2个AIDL接口、Gradle构建脚本、ProGuard规则及1个JAR依赖库,目录结构清晰,目前已有319人学习/下载。两份工程分别从界面与服务的场景演示不同常驻实现路径,内含完整源码、配置文件与说明文档,可导入开发环境直接运行分析,并借助跨进程接口理解通信保活方式;配合图片可系统梳理守护进程化、服务注册、优先级调整、内存释放与自动重启等关键知识点,同时关注资源消耗、安全性与系统兼容性,可作为后台保活与服务稳定运行设计的工程参考,便于二次开发时直接复用其中的进程管理思路。1. 进程常驻不是玄学:这份多 module 资源到底在解决什么做 Android 的开发者应该都有过这种经历:App 退到后台不到五分钟,进程就被系统回收,来电悬浮窗、消息推送、计步统计全部断掉。我拆过不少号称“保活”的项目,网上一搜一大把,但真正能跑通、能过厂商测试、能在不同 ROM 上稳定住的不多。这份「进程常驻探索 内含多个 module」是一套面向 Android 的多模块常驻工程,把前台服务、双进程守护、WorkManager 兜底、开机广播四种手段封装成独立模块,按需接入,而不是把所有代码塞进一个 Service 里硬扛。它适合做即时通讯、运动健康、系统工具类 App 的开发者,也适合想彻底搞懂 service 生命周期、进程优先级和厂商后台限制之间关系的人。2. 先看底层逻辑:进程优先级、LMK 与五类常驻方案的选型边界常驻这件事看起来是“写代码”,本质上是“和系统博弈”。Linux 内核里每个进程都有一个 oom_adj 值,Android 在原生 Linux 之上又包了一层 LMK(Low Memory Killer)策略,当系统内存吃紧时,会从 oom_adj 数值高的进程开杀。理解这个优先级队列,才能明白为什么有的 Service 写了几十行就能活一天,有的写了全套保活,一划后台就没了。2.1 进程优先级不是秘密:oom_adj 决定谁先死Android 进程从高到低大致分为五档:前台进程(adj=0)、可见进程(adj=1)、服务进程(adj=2)、后台进程(adj=8)、空进程(adj=9)。前台进程指当前正在和用户交互的 Activity、正在执行 onReceive 的 BroadcastReceiver,或者绑定了用户正在交互组件的前台 Service;服务进程就是正在运行普通 Service 但没有 UI 的进程;后台进程是 Activity 已经不可见的进程;空进程则是没有任何组件存活、只留了一个空壳的进程。实际系统里,oom_adj 值会被各种因素动态调整。举个最常见的例子:一个 App 退到后台,Activity 不可见了,它的 adj 会从 0 慢慢升到 8、9,但如果里面有一个正在运行的前台服务,那么 adj 会保持在 0;如果只有普通 service,那么 adj 会停在 2,比后台 Activity 还靠前。这就是为什么很多人觉得“Service 加个 onStartCommand 就能活”——它确实比什么都强的进程优先级高,但一旦系统内存大幅吃紧,LMK 照样把 adj≥2 的杀干净。提示:不同厂商会砍掉原生 LMK 换成自己的统一内存管理(如各种“内存回收机制”“智能后台清理”),这些策略不看 adj,看的是 App 是否在白名单、电源是否优化、以及用户是否主动锁定。所以原生理论说完了,实战里最常见的翻车点反而在厂商策略上。2.2 五类常驻方案横向对比:强度、副作用与适用场景这份资源里把常驻手段拆成了五个模块,其实对应五种完全不同的实现路径。它们不是互相替代的关系,而是层层兜底的关系。我按实际效果和使用成本整理了一张表:方案实现原理保活强度副作用适用场景前台服务startForeground 提升进程优先级,adj=0较强必须常驻通知,用户可手动移除音乐播放、导航、运动记录双进程守护两个 Service 互相绑定,A 挂掉时 B 拉起 A中(被厂商限制)CPU 耗电、内存占用高内部组件互相健康检查WorkManager系统级任务调度,Doze 模式下也能周期性执行中最小间隔 15 分钟,可能延迟数据同步、上报、清理广播拉活监听开机、网络切换、解锁等系统广播弱(8.0 后限制)用户可见耗电、被标记开机自启、网络变化补偿推送拉起长连接被系统杀掉后,厂商推送通道来唤醒强(有推送通道时)依赖厂商 ROM 支持国内主流厂商设备从这张表能看出,没有哪一种是“银弹”。前台服务强度最高,但用户会看到一条常驻通知,而且 Android 13 以后用户可以一键停止;双进程守护在原生系统上表现不错,到了国产 ROM 上经常被连锅端;WorkManager 看着中规中矩,但它胜在是系统级 API,不会被打上“恶意保活”标签。2.3 选型边界:为什么“永不杀”在 Android 12 后是伪命题很多开发者找这类资源,心里想的其实是“有没有一种办法让我这个 App 永不被杀”。我的结论很直接:在原生 Android 12 及以上,前台服务启动被加了限制,后台进程不能随意调起前台服务;Android 14 对前台服务类型又做了强制校验。也就是说,“永不杀”在系统级已经不存在,任何第三方代码都不可能绕过系统限制。那还做常驻干嘛?答案是争取“最大概率存活”和“最快恢复”。前台服务保证 App 在内存充足时不被挪到后台进程;双进程守护保证进程被异常杀死后,另一个进程在最短时间内把它拉起来;WorkManager 保证即使进程全没了,系统也会在合适时机把任务重新跑一遍。这套组合拳打下来,用户看到的 App 行为是“基本不掉链子,死了也能自己活过来”。选型时先问自己:你需要的是前台持续运行,还是后台保活之后快速恢复?前者选前台服务为主,后者选守护 + WorkManager 组合。3. 拆开多 module 工程:结构与核心 service 代码怎么写“内含多个 module”是这个资源最大的结构亮点。每个常驻方案都封装成了独立的 Gradle module,相互解耦,可以按需依赖。这比把所有代码写成一个大杂烩 Service 强很多,因为你可以只引入你需要的保活能力,不用的模块完全不占代码体积。我按实际依赖顺序从下往上拆开看。3.1 module 划分与依赖关系:core 提供什么,service 模块消费什么整个工程大致分为六个模块:core(公共基础)、service-foreground(前台服务)、service-daemon(守护进程)、service-workmanager(兜底任务)、service-receiver(广播拉活)、app(宿主壳)。依赖方向是从 app 向下依赖各个 service 模块,service 模块之间不互相依赖,都只依赖 core。core 里放的是保活场景的公共配置:通知渠道 ID、ServiceAction 常量、SP 存储的工具类、统一的状态上报接口。// core/src/main/java/.../KeepAliveConfig.kt object KeepAliveConfig { const val CHANNEL_ID = "channel_keep_alive" // 通知渠道,保持一致 const val ACTION_START = "action_start" // 启动前台服务 const val ACTION_STOP = "action_stop" // 停止前台服务 const val DAEMON_ACTION = "daemon_action" // 守护进程互绑标记 enum class ModuleType { FOREGROUND, DAEMON, WORK_MANAGER, RECEIVER } }逻辑说明:core 模块不包含任何 Android 四大组件,只提供常驻相关的常量和工具方法,这样 service 模块之间不会出现循环依赖,谁需要谁直接 import。所有的跨模块通信全部走 Intent action,不直接持有对方类引用,这样能做到单个模块被裁剪时,其他模块的代码不需要改动。参数说明:CHANNEL_ID 是通知渠道的标识,Service 和创建通知的地方必须用同一个字符串,否则 Notification 无法弹出。ModuleType 是为了在统一的上报逻辑里区分当前是哪种保活方式,方便后续日志排查和埋点统计。3.2 前台服务 module:startForeground 正确姿势与通知渠道前台服务是整套方案的地基,也是唯一能让你进程 adj 保持为 0 的代码路径。这段代码的关键点有两个:一是通知渠道必须在 Android 8.0+ 建立,否则通知栏不显示;二是从 Android 10 开始,startForeground 需要传入 foregroundServiceType 参数,到了 Android 14 这个 type 还必须在 Manifest 里声明,否则运行时抛异常。// service-foreground/src/main/java/.../KeepAliveForegroundService.kt class KeepAliveForegroundService : Service() { override fun onCreate() { super.onCreate() createNotificationChannel() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val notification = buildNotification() // 传入 type:媒体播放只是示例,按实际用途改成对应的类型 startForeground( NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK ) return START_STICKY } private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( KeepAliveConfig.CHANNEL_ID, "常驻服务", NotificationManager.IMPORTANCE_LOW // LOW 避免弹出声音 ) channel.description = "用于维持后台运行" getSystemService(NotificationManager::class.java) .createNotificationChannel(channel) } } override fun onBind(intent: Intent?): IBinder? = null }逻辑说明:onStartCommand 返回 START_STICKY,表示 Service 被系统杀死后,系统会尝试重建它,并在 onStartCommand 重新传入 null Intent。这是原生系统里最简单的一层“自动恢复”机制。前台服务一旦 startForeground 成功,进程的 adj 会被拉高到前台级别,LMK 不会优先杀它。参数说明:FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK 只是一个占位类型。如果你的 App 实际用途是导航、通话、远程传输,必须改成对应类型,并且在 Manifest.xml 的 service 节点里加 android:foregroundServiceType 属性。前后不匹配时,部分 ROM 会直接把服务杀掉并报 SecurityException。3.3 双进程守护 module:AIDL 绑定与拉活逻辑的边界双进程守护的思路是:A 进程和 B 进程各自跑一个 Service,互相 bind。A 被系统杀了,A 的 onDestroy 还没来得及执行时,B 通过 binder 连接断开感知到 A 死亡,然后 B 主动调 startService 把 A 拉起;A 反过来监听 B。这个方案在原生系统和部分老设备上表现不错,但在 Android 8.0 之后,后台执行限制让它越来越难做。// service-daemon/src/main/java/.../DaemonService.java public class DaemonService extends Service { private final DaemonBinder mBinder = new DaemonBinder(); private static boolean isRunning = false; public static boolean isRunning() { return isRunning; } @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public void onCreate() { super.onCreate(); isRunning = true; // 绑定对方进程的服务;这里写死另一个守护 service 的 action Intent intent = new Intent("com.xxx.daemon.SECOND"); intent.setPackage(getPackageName()); bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { // 连接成功,说明对方进程还活着 } @Override public void onServiceDisconnected(ComponentName name) { // 对方进程死了,立刻尝试拉起 startService(new Intent(DaemonService.this, SecondDaemonService.class)); } }; @Override public void onDestroy() { super.onDestroy(); isRunning = false; if (mConnection != null) { unbindService(mConnection); } } }逻辑说明:onServiceDisconnected 是双守护的核心回调。两个 Service 绑定的是不同进程,Modify 当对方进程被 LMK 杀掉时,binder 会断开,触发这个回调,当前进程马上尝试把对方拉起来。被拉起的 Service 在 onCreate 里又会重新去绑定对面进程的服务,这样就形成了一个祈使循环。参数说明:BIND_AUTO_CREATE 表示在绑定时如果目标服务不存在就自动创建。这里有个常见误区:两个 Service 必须跑在不同进程中,否则相互 bind 是同一个进程内的绑定,进程一旦被杀死,两个 Service 一起死,守护循环根本建立不起来。3.4 WorkManager 兜底 module:周期任务与约束参数双进程守护再怎么折腾,也挡不住用户手动从任务管理器滑掉,或者厂商把这些进程连根清除。此时系统级的 WorkManager 反而最靠谱——它由系统进程调度,即使你的 App 进程被杀,任务仍有机会在系统决定的时间被执行。它非常适合做兜底。// service-workmanager/src/main/java/.../KeepAliveWorker.kt class KeepAliveWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { // 兜底动作:检查主 Service 是否在运行,不在就拉起 val isServiceRunning = isServiceRunning(KeepAliveConfig.ACTION_START) if (!isServiceRunning) { val intent = Intent(applicationContext, KeepAliveForegroundService::class.java) applicationContext.startForegroundService(intent) } return Result.success() } }// app/src/main/java/.../WorkManagerScheduler.kt fun scheduleKeepAlive(context: Context) { val request = PeriodicWorkRequestBuilder<KeepAliveWorker>(15, TimeUnit.MINUTES) .setConstraints( Constraints.Builder() .setRequiredNetworkType(NetworkType.NOT_REQUIRED) .build() ) .setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES) .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( "keep_alive_work", ExistingPeriodicWorkPolicy.KEEP, request ) }逻辑说明:PeriodicWorkRequestBuilder 的最小周期是 15 分钟,这是系统硬性约束,传小于 15 分钟会被直接忽略。setBackoffCriteria 表示任务失败后按线性退避重试,第一次等 1 分钟,之后递进,避免频繁重试加剧耗电。enqueueUniquePeriodicWork 的 KEEP 策略至关重要——如果已经有一个相同名字的周期任务在排队,就保留旧的,否则每次调用都会新建一个任务,导致任务重复执行。参数说明:NetworkType.NOT_REQUIRED 表示不依赖网络条件,适合保活场景。如果你改成 CONNECTED,在无网络环境下任务会一直等待,兜底效果就打了折扣。work 运行在后台线程,sample 里是协程,不要在里面做耗时超过 10 秒的操作,否则会被系统判定为异常退出。4. 常驻方案避坑:五次翻车现场的起因与解决常驻代码的坑不是“写完不跑”,而是“当时跑得通,换设备、换系统版本就炸”。我把自己在这套多 module 工程里踩过的坑整理成五条,每一条都按现象、原因、解决来写,后面集成别走同样的路。4.1 前台服务明明加了,还是被系统杀了现象:在 A 手机上开着前台服务,通知栏也有常驻通知,但内存一紧张服务还是没了;或者启动时直接闪退,Logcat 里是 ForegroundServiceDidNotStartInTime 的异常。原因:常见的两个坑。一是没有在启动后 5 秒内调用 startForeground,超时后系统直接丢 ANR 异常并杀掉进程;二是启动前台服务的方式用的 startService,而不是开始 Android 8.0 后的 startForegroundService。前者保证了前台服务的类型声明,后者则没有真正把服务提升到前台优先级。解决:代码里统一使用 startForegroundService 启动,并在 onStartCommand 第一行调用 startForeground,入参必须和 Manifest 里声明的 foregroundServiceType 完全一致。如果业务上没法决定类型,可以在 Manifest 里写 android:foregroundServiceType="specialUse",并且让 type 参数传 -1,但这样在 Google Play 上架会比较被动,国内分发没问题。4.2 双进程守护导致 CPU 飙高、手机发热现象:两个守护 Service 跑起来后,用 Profiler 一看,两个进程的 CPU 占用都在 10% 以上,手机明显发热,循环拉起的日志刷个不停。原因:最常见的是误用了“轮询检查”而不是“Binder 死亡监听”。工程里如果写了一个 while(true) 循环每分钟检查一次对方进程在不在,两个进程都在做这种事,CPU 自然拉满。另一种情况是 onServiceDisconnected 被触发后拉起了对方,但对方因为某种原因启动失败,于是这边又拉,那边又断,形成死循环。解决:第一,去掉所有主动轮询,改用 Binder 的死亡回调,逻辑只在状态变化时执行;第二,在拉起动作里加状态保护,比如记录上次拉起时间,5 秒内不重复拉;第三,给守护进程设定退出条件,当 App 主界面不可见且用户主动退出时,允许守护进程一起退出,避免“僵尸互拉”。4.3 WorkManager 周期任务到了 15 分钟不执行现象:在部分品牌手机上,锁屏放置一夜,第二天看日志发现周期任务一次都没跑;偶尔手动打开 App,任务又突然集中执行好几次。原因:WorkManager 虽然由系统调度,但任务的真实触发时间依赖 Idle state、电量状态和厂商后台管理。国产 ROM 的省电策略会把你的 App 加入“待机清理名单”,系统认为应用不可见时就冻结它的所有任务,包括 WorkManager。另一个原因是周期任务的最小间隔虽然设了 15 分钟,但系统可能因为 Doze 模式把它延后到下一次唤醒。解决:把 Doze 模式的白名单和厂商自启动权限引导做进去,这是绕不开的一步。代码里可以检测当前是否在白名单内,不在则引导用户跳转相关设置页添加;判断逻辑封装在 core 模块中,避免业务代码重复写。同时,把 WorkManager 和前台服务切换成“前台服务保当前,WorkManager 兜底”的搭配,而不是单靠 WorkManager 一条路。4.4 通知显示不出来,前台服务直接崩溃现象:在国产 ROM 上运行,通知栏看不到常驻通知,但代码里明明创建了通知渠道;有些设备上一启动就抛 Channel is not registered 的断言异常。原因:Android 8.0 之后通知渠道必须在创建后才能发通知,如果你在不同模块里各创建了一次渠道,用的 channelId 相同但 importance 不同,后创建的会覆盖前创建的缓存配置,导致第一个发通知的地方取不到有效渠道。另一个原因是 App 在通知开关里被用户关闭了通知权限,渠道被系统禁用,此时 startForeground 仍被调用,就会抛异常。解决:把通知渠道的创建统一放在 core 模块里,由 app 启动时先执行一次,不要在每个 service 模块各自创建。service 模块发通知前检查 canUseNotification 权限,拿不到权限时降级处理,比如退化成普通后台任务而不是硬调 startForeground。注意降级后进程优先级会掉,但总比崩溃强。4.5 进程被杀后 START_STICKY 并没有把服务拉回来现象:手动从后台任务管理划掉 App,或者系统内存吃紧处死进程,结果服务一直没重新启动,直到下次用户点开 App 才活过来。原因:START_STICKY 只在系统内存适中、进程被异常杀掉时才会重建。如果进程是被用户手动强制停止,系统直接标记该 App 为“已停止”,之后一段时间内任何后台启动动作都不会生效;后台管理任务里的“滑动清除”和“强制停止”效果相似,但实现路径不同,很多厂商把前者也当成后者处理。解决:这套场景下不能依赖 START_STICKY 当唯一恢复手段,让双进程守护和 WorkManager 在极端情况去兜底。手动停止后,最有效的拉起时机是用户下一次主动触摸 App 图标时,系统解除已停止状态,你再通过前台服务把整个保活链条重新挂上。可以在 Activity 的 onStart 里检测服务是否存活,不存在就重新拉起。5. 进阶验证:把“尽量活着”变成可量化的三层保障常驻方案不应该是写了代码就觉得“应该能活”,而是每改一次都要有明确的验证数据。我实际操作时会把常驻拆成三层,按优先级执行,并建立一套快速验证流程,每次集成新模块都用同一套方式来验收。三层分别是:前台服务兜底当前状态、双进程守护兜底异常被杀的恢复、WorkManager 兜底两个进程全灭后系统级任务重放。每一层都有明确的验收命令。第一层看进程存活,执行 adb shell ps -A | grep 包名,看返回内容;第二层看守护恢复,用 adb shell am kill 包名 杀掉主进程,观察 3 秒内守护进程是否把它拉回来,Logcat 里能看到 binder 断开与重建的日志;第三层看 WorkManager,杀掉 App 所有进程后等待下一个 15 分钟周期,或直接执行 adb shell cmd jobscheduler run -f 包名 强制触发一次作业。验证时不能只看“进程在不在”,还要看耗电曲线和崩溃日志。用 Profiler 观察 CPU 每分钟平均占用,如果常驻比正常业务还多出 3 个百分点的 CPU,说明拉活逻辑有问题,多半是轮询或死循环在作怪。给 main 进程和守护进程分别打不同的统计事件,半小时内如果反复出现 kill 和 restart,就该检查守护循环是不是在空转。从那以后,我每次集成这套多 module 工程,都强制走一遍验证流程:先跑 30 分钟观察存活率,再手动模拟一次强杀、一次后台任务滑动清除、一次系统内存压力测试,最后看耗电曲线和崩溃率。这套流程帮我及时排掉了不少厂商 ROM 上的意外表现,也避免了把“看起来能跑”误当成“真能扛”。希望帮到你。本文还有配套的精品资源,点击获取
简介:《进程常驻探索》压缩包内含csdnactivity与servicelive1两个安卓工程模块,面向需要深入理解服务常驻与进程优先级调优的移动开发者和系统开发者,解决后台服务容易被回收、关键进程无法持续运行的难题。项目从实际代码出发,系统演示守护进程、服务管理器、启动脚本以及Supervisor/NSSM等第三方工具的常驻方案,并结合nice值、Windows优先级类等调度策略,帮助读者理解进程创建、执行、等待与结束的生命周期机制。资源共69个文件、16.9MB,以20个Java源码和18个XML配置为主体,配有20张PNG图解(覆盖工程结构、运行流程与优先级说明),另有2个AIDL接口、Gradle构建脚本、ProGuard规则及1个JAR依赖库,目录结构清晰,目前已有319人学习/下载。两份工程分别从界面与服务的场景演示不同常驻实现路径,内含完整源码、配置文件与说明文档,可导入开发环境直接运行分析,并借助跨进程接口理解通信保活方式;配合图片可系统梳理守护进程化、服务注册、优先级调整、内存释放与自动重启等关键知识点,同时关注资源消耗、安全性与系统兼容性,可作为后台保活与服务稳定运行设计的工程参考,便于二次开发时直接复用其中的进程管理思路。1. 进程常驻不是玄学:这份多 module 资源到底在解决什么做 Android 的开发者应该都有过这种经历:App 退到后台不到五分钟,进程就被系统回收,来电悬浮窗、消息推送、计步统计全部断掉。我拆过不少号称“保活”的项目,网上一搜一大把,但真正能跑通、能过厂商测试、能在不同 ROM 上稳定住的不多。这份「进程常驻探索 内含多个 module」是一套面向 Android 的多模块常驻工程,把前台服务、双进程守护、WorkManager 兜底、开机广播四种手段封装成独立模块,按需接入,而不是把所有代码塞进一个 Service 里硬扛。它适合做即时通讯、运动健康、系统工具类 App 的开发者,也适合想彻底搞懂 service 生命周期、进程优先级和厂商后台限制之间关系的人。2. 先看底层逻辑:进程优先级、LMK 与五类常驻方案的选型边界常驻这件事看起来是“写代码”,本质上是“和系统博弈”。Linux 内核里每个进程都有一个 oom_adj 值,Android 在原生 Linux 之上又包了一层 LMK(Low Memory Killer)策略,当系统内存吃紧时,会从 oom_adj 数值高的进程开杀。理解这个优先级队列,才能明白为什么有的 Service 写了几十行就能活一天,有的写了全套保活,一划后台就没了。2.1 进程优先级不是秘密:oom_adj 决定谁先死Android 进程从高到低大致分为五档:前台进程(adj=0)、可见进程(adj=1)、服务进程(adj=2)、后台进程(adj=8)、空进程(adj=9)。前台进程指当前正在和用户交互的 Activity、正在执行 onReceive 的 BroadcastReceiver,或者绑定了用户正在交互组件的前台 Service;服务进程就是正在运行普通 Service 但没有 UI 的进程;后台进程是 Activity 已经不可见的进程;空进程则是没有任何组件存活、只留了一个空壳的进程。实际系统里,oom_adj 值会被各种因素动态调整。举个最常见的例子:一个 App 退到后台,Activity 不可见了,它的 adj 会从 0 慢慢升到 8、9,但如果里面有一个正在运行的前台服务,那么 adj 会保持在 0;如果只有普通 service,那么 adj 会停在 2,比后台 Activity 还靠前。这就是为什么很多人觉得“Service 加个 onStartCommand 就能活”——它确实比什么都强的进程优先级高,但一旦系统内存大幅吃紧,LMK 照样把 adj≥2 的杀干净。提示:不同厂商会砍掉原生 LMK 换成自己的统一内存管理(如各种“内存回收机制”“智能后台清理”),这些策略不看 adj,看的是 App 是否在白名单、电源是否优化、以及用户是否主动锁定。所以原生理论说完了,实战里最常见的翻车点反而在厂商策略上。2.2 五类常驻方案横向对比:强度、副作用与适用场景这份资源里把常驻手段拆成了五个模块,其实对应五种完全不同的实现路径。它们不是互相替代的关系,而是层层兜底的关系。我按实际效果和使用成本整理了一张表:方案实现原理保活强度副作用适用场景前台服务startForeground 提升进程优先级,adj=0较强必须常驻通知,用户可手动移除音乐播放、导航、运动记录双进程守护两个 Service 互相绑定,A 挂掉时 B 拉起 A中(被厂商限制)CPU 耗电、内存占用高内部组件互相健康检查WorkManager系统级任务调度,Doze 模式下也能周期性执行中最小间隔 15 分钟,可能延迟数据同步、上报、清理广播拉活监听开机、网络切换、解锁等系统广播弱(8.0 后限制)用户可见耗电、被标记开机自启、网络变化补偿推送拉起长连接被系统杀掉后,厂商推送通道来唤醒强(有推送通道时)依赖厂商 ROM 支持国内主流厂商设备从这张表能看出,没有哪一种是“银弹”。前台服务强度最高,但用户会看到一条常驻通知,而且 Android 13 以后用户可以一键停止;双进程守护在原生系统上表现不错,到了国产 ROM 上经常被连锅端;WorkManager 看着中规中矩,但它胜在是系统级 API,不会被打上“恶意保活”标签。2.3 选型边界:为什么“永不杀”在 Android 12 后是伪命题很多开发者找这类资源,心里想的其实是“有没有一种办法让我这个 App 永不被杀”。我的结论很直接:在原生 Android 12 及以上,前台服务启动被加了限制,后台进程不能随意调起前台服务;Android 14 对前台服务类型又做了强制校验。也就是说,“永不杀”在系统级已经不存在,任何第三方代码都不可能绕过系统限制。那还做常驻干嘛?答案是争取“最大概率存活”和“最快恢复”。前台服务保证 App 在内存充足时不被挪到后台进程;双进程守护保证进程被异常杀死后,另一个进程在最短时间内把它拉起来;WorkManager 保证即使进程全没了,系统也会在合适时机把任务重新跑一遍。这套组合拳打下来,用户看到的 App 行为是“基本不掉链子,死了也能自己活过来”。选型时先问自己:你需要的是前台持续运行,还是后台保活之后快速恢复?前者选前台服务为主,后者选守护 + WorkManager 组合。3. 拆开多 module 工程:结构与核心 service 代码怎么写“内含多个 module”是这个资源最大的结构亮点。每个常驻方案都封装成了独立的 Gradle module,相互解耦,可以按需依赖。这比把所有代码写成一个大杂烩 Service 强很多,因为你可以只引入你需要的保活能力,不用的模块完全不占代码体积。我按实际依赖顺序从下往上拆开看。3.1 module 划分与依赖关系:core 提供什么,service 模块消费什么整个工程大致分为六个模块:core(公共基础)、service-foreground(前台服务)、service-daemon(守护进程)、service-workmanager(兜底任务)、service-receiver(广播拉活)、app(宿主壳)。依赖方向是从 app 向下依赖各个 service 模块,service 模块之间不互相依赖,都只依赖 core。core 里放的是保活场景的公共配置:通知渠道 ID、ServiceAction 常量、SP 存储的工具类、统一的状态上报接口。// core/src/main/java/.../KeepAliveConfig.kt object KeepAliveConfig { const val CHANNEL_ID = "channel_keep_alive" // 通知渠道,保持一致 const val ACTION_START = "action_start" // 启动前台服务 const val ACTION_STOP = "action_stop" // 停止前台服务 const val DAEMON_ACTION = "daemon_action" // 守护进程互绑标记 enum class ModuleType { FOREGROUND, DAEMON, WORK_MANAGER, RECEIVER } }逻辑说明:core 模块不包含任何 Android 四大组件,只提供常驻相关的常量和工具方法,这样 service 模块之间不会出现循环依赖,谁需要谁直接 import。所有的跨模块通信全部走 Intent action,不直接持有对方类引用,这样能做到单个模块被裁剪时,其他模块的代码不需要改动。参数说明:CHANNEL_ID 是通知渠道的标识,Service 和创建通知的地方必须用同一个字符串,否则 Notification 无法弹出。ModuleType 是为了在统一的上报逻辑里区分当前是哪种保活方式,方便后续日志排查和埋点统计。3.2 前台服务 module:startForeground 正确姿势与通知渠道前台服务是整套方案的地基,也是唯一能让你进程 adj 保持为 0 的代码路径。这段代码的关键点有两个:一是通知渠道必须在 Android 8.0+ 建立,否则通知栏不显示;二是从 Android 10 开始,startForeground 需要传入 foregroundServiceType 参数,到了 Android 14 这个 type 还必须在 Manifest 里声明,否则运行时抛异常。// service-foreground/src/main/java/.../KeepAliveForegroundService.kt class KeepAliveForegroundService : Service() { override fun onCreate() { super.onCreate() createNotificationChannel() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val notification = buildNotification() // 传入 type:媒体播放只是示例,按实际用途改成对应的类型 startForeground( NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK ) return START_STICKY } private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( KeepAliveConfig.CHANNEL_ID, "常驻服务", NotificationManager.IMPORTANCE_LOW // LOW 避免弹出声音 ) channel.description = "用于维持后台运行" getSystemService(NotificationManager::class.java) .createNotificationChannel(channel) } } override fun onBind(intent: Intent?): IBinder? = null }逻辑说明:onStartCommand 返回 START_STICKY,表示 Service 被系统杀死后,系统会尝试重建它,并在 onStartCommand 重新传入 null Intent。这是原生系统里最简单的一层“自动恢复”机制。前台服务一旦 startForeground 成功,进程的 adj 会被拉高到前台级别,LMK 不会优先杀它。参数说明:FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK 只是一个占位类型。如果你的 App 实际用途是导航、通话、远程传输,必须改成对应类型,并且在 Manifest.xml 的 service 节点里加 android:foregroundServiceType 属性。前后不匹配时,部分 ROM 会直接把服务杀掉并报 SecurityException。3.3 双进程守护 module:AIDL 绑定与拉活逻辑的边界双进程守护的思路是:A 进程和 B 进程各自跑一个 Service,互相 bind。A 被系统杀了,A 的 onDestroy 还没来得及执行时,B 通过 binder 连接断开感知到 A 死亡,然后 B 主动调 startService 把 A 拉起;A 反过来监听 B。这个方案在原生系统和部分老设备上表现不错,但在 Android 8.0 之后,后台执行限制让它越来越难做。// service-daemon/src/main/java/.../DaemonService.java public class DaemonService extends Service { private final DaemonBinder mBinder = new DaemonBinder(); private static boolean isRunning = false; public static boolean isRunning() { return isRunning; } @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public void onCreate() { super.onCreate(); isRunning = true; // 绑定对方进程的服务;这里写死另一个守护 service 的 action Intent intent = new Intent("com.xxx.daemon.SECOND"); intent.setPackage(getPackageName()); bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { // 连接成功,说明对方进程还活着 } @Override public void onServiceDisconnected(ComponentName name) { // 对方进程死了,立刻尝试拉起 startService(new Intent(DaemonService.this, SecondDaemonService.class)); } }; @Override public void onDestroy() { super.onDestroy(); isRunning = false; if (mConnection != null) { unbindService(mConnection); } } }逻辑说明:onServiceDisconnected 是双守护的核心回调。两个 Service 绑定的是不同进程,Modify 当对方进程被 LMK 杀掉时,binder 会断开,触发这个回调,当前进程马上尝试把对方拉起来。被拉起的 Service 在 onCreate 里又会重新去绑定对面进程的服务,这样就形成了一个祈使循环。参数说明:BIND_AUTO_CREATE 表示在绑定时如果目标服务不存在就自动创建。这里有个常见误区:两个 Service 必须跑在不同进程中,否则相互 bind 是同一个进程内的绑定,进程一旦被杀死,两个 Service 一起死,守护循环根本建立不起来。3.4 WorkManager 兜底 module:周期任务与约束参数双进程守护再怎么折腾,也挡不住用户手动从任务管理器滑掉,或者厂商把这些进程连根清除。此时系统级的 WorkManager 反而最靠谱——它由系统进程调度,即使你的 App 进程被杀,任务仍有机会在系统决定的时间被执行。它非常适合做兜底。// service-workmanager/src/main/java/.../KeepAliveWorker.kt class KeepAliveWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { // 兜底动作:检查主 Service 是否在运行,不在就拉起 val isServiceRunning = isServiceRunning(KeepAliveConfig.ACTION_START) if (!isServiceRunning) { val intent = Intent(applicationContext, KeepAliveForegroundService::class.java) applicationContext.startForegroundService(intent) } return Result.success() } }// app/src/main/java/.../WorkManagerScheduler.kt fun scheduleKeepAlive(context: Context) { val request = PeriodicWorkRequestBuilder<KeepAliveWorker>(15, TimeUnit.MINUTES) .setConstraints( Constraints.Builder() .setRequiredNetworkType(NetworkType.NOT_REQUIRED) .build() ) .setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES) .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( "keep_alive_work", ExistingPeriodicWorkPolicy.KEEP, request ) }逻辑说明:PeriodicWorkRequestBuilder 的最小周期是 15 分钟,这是系统硬性约束,传小于 15 分钟会被直接忽略。setBackoffCriteria 表示任务失败后按线性退避重试,第一次等 1 分钟,之后递进,避免频繁重试加剧耗电。enqueueUniquePeriodicWork 的 KEEP 策略至关重要——如果已经有一个相同名字的周期任务在排队,就保留旧的,否则每次调用都会新建一个任务,导致任务重复执行。参数说明:NetworkType.NOT_REQUIRED 表示不依赖网络条件,适合保活场景。如果你改成 CONNECTED,在无网络环境下任务会一直等待,兜底效果就打了折扣。work 运行在后台线程,sample 里是协程,不要在里面做耗时超过 10 秒的操作,否则会被系统判定为异常退出。4. 常驻方案避坑:五次翻车现场的起因与解决常驻代码的坑不是“写完不跑”,而是“当时跑得通,换设备、换系统版本就炸”。我把自己在这套多 module 工程里踩过的坑整理成五条,每一条都按现象、原因、解决来写,后面集成别走同样的路。4.1 前台服务明明加了,还是被系统杀了现象:在 A 手机上开着前台服务,通知栏也有常驻通知,但内存一紧张服务还是没了;或者启动时直接闪退,Logcat 里是 ForegroundServiceDidNotStartInTime 的异常。原因:常见的两个坑。一是没有在启动后 5 秒内调用 startForeground,超时后系统直接丢 ANR 异常并杀掉进程;二是启动前台服务的方式用的 startService,而不是开始 Android 8.0 后的 startForegroundService。前者保证了前台服务的类型声明,后者则没有真正把服务提升到前台优先级。解决:代码里统一使用 startForegroundService 启动,并在 onStartCommand 第一行调用 startForeground,入参必须和 Manifest 里声明的 foregroundServiceType 完全一致。如果业务上没法决定类型,可以在 Manifest 里写 android:foregroundServiceType="specialUse",并且让 type 参数传 -1,但这样在 Google Play 上架会比较被动,国内分发没问题。4.2 双进程守护导致 CPU 飙高、手机发热现象:两个守护 Service 跑起来后,用 Profiler 一看,两个进程的 CPU 占用都在 10% 以上,手机明显发热,循环拉起的日志刷个不停。原因:最常见的是误用了“轮询检查”而不是“Binder 死亡监听”。工程里如果写了一个 while(true) 循环每分钟检查一次对方进程在不在,两个进程都在做这种事,CPU 自然拉满。另一种情况是 onServiceDisconnected 被触发后拉起了对方,但对方因为某种原因启动失败,于是这边又拉,那边又断,形成死循环。解决:第一,去掉所有主动轮询,改用 Binder 的死亡回调,逻辑只在状态变化时执行;第二,在拉起动作里加状态保护,比如记录上次拉起时间,5 秒内不重复拉;第三,给守护进程设定退出条件,当 App 主界面不可见且用户主动退出时,允许守护进程一起退出,避免“僵尸互拉”。4.3 WorkManager 周期任务到了 15 分钟不执行现象:在部分品牌手机上,锁屏放置一夜,第二天看日志发现周期任务一次都没跑;偶尔手动打开 App,任务又突然集中执行好几次。原因:WorkManager 虽然由系统调度,但任务的真实触发时间依赖 Idle state、电量状态和厂商后台管理。国产 ROM 的省电策略会把你的 App 加入“待机清理名单”,系统认为应用不可见时就冻结它的所有任务,包括 WorkManager。另一个原因是周期任务的最小间隔虽然设了 15 分钟,但系统可能因为 Doze 模式把它延后到下一次唤醒。解决:把 Doze 模式的白名单和厂商自启动权限引导做进去,这是绕不开的一步。代码里可以检测当前是否在白名单内,不在则引导用户跳转相关设置页添加;判断逻辑封装在 core 模块中,避免业务代码重复写。同时,把 WorkManager 和前台服务切换成“前台服务保当前,WorkManager 兜底”的搭配,而不是单靠 WorkManager 一条路。4.4 通知显示不出来,前台服务直接崩溃现象:在国产 ROM 上运行,通知栏看不到常驻通知,但代码里明明创建了通知渠道;有些设备上一启动就抛 Channel is not registered 的断言异常。原因:Android 8.0 之后通知渠道必须在创建后才能发通知,如果你在不同模块里各创建了一次渠道,用的 channelId 相同但 importance 不同,后创建的会覆盖前创建的缓存配置,导致第一个发通知的地方取不到有效渠道。另一个原因是 App 在通知开关里被用户关闭了通知权限,渠道被系统禁用,此时 startForeground 仍被调用,就会抛异常。解决:把通知渠道的创建统一放在 core 模块里,由 app 启动时先执行一次,不要在每个 service 模块各自创建。service 模块发通知前检查 canUseNotification 权限,拿不到权限时降级处理,比如退化成普通后台任务而不是硬调 startForeground。注意降级后进程优先级会掉,但总比崩溃强。4.5 进程被杀后 START_STICKY 并没有把服务拉回来现象:手动从后台任务管理划掉 App,或者系统内存吃紧处死进程,结果服务一直没重新启动,直到下次用户点开 App 才活过来。原因:START_STICKY 只在系统内存适中、进程被异常杀掉时才会重建。如果进程是被用户手动强制停止,系统直接标记该 App 为“已停止”,之后一段时间内任何后台启动动作都不会生效;后台管理任务里的“滑动清除”和“强制停止”效果相似,但实现路径不同,很多厂商把前者也当成后者处理。解决:这套场景下不能依赖 START_STICKY 当唯一恢复手段,让双进程守护和 WorkManager 在极端情况去兜底。手动停止后,最有效的拉起时机是用户下一次主动触摸 App 图标时,系统解除已停止状态,你再通过前台服务把整个保活链条重新挂上。可以在 Activity 的 onStart 里检测服务是否存活,不存在就重新拉起。5. 进阶验证:把“尽量活着”变成可量化的三层保障常驻方案不应该是写了代码就觉得“应该能活”,而是每改一次都要有明确的验证数据。我实际操作时会把常驻拆成三层,按优先级执行,并建立一套快速验证流程,每次集成新模块都用同一套方式来验收。三层分别是:前台服务兜底当前状态、双进程守护兜底异常被杀的恢复、WorkManager 兜底两个进程全灭后系统级任务重放。每一层都有明确的验收命令。第一层看进程存活,执行 adb shell ps -A | grep 包名,看返回内容;第二层看守护恢复,用 adb shell am kill 包名 杀掉主进程,观察 3 秒内守护进程是否把它拉回来,Logcat 里能看到 binder 断开与重建的日志;第三层看 WorkManager,杀掉 App 所有进程后等待下一个 15 分钟周期,或直接执行 adb shell cmd jobscheduler run -f 包名 强制触发一次作业。验证时不能只看“进程在不在”,还要看耗电曲线和崩溃日志。用 Profiler 观察 CPU 每分钟平均占用,如果常驻比正常业务还多出 3 个百分点的 CPU,说明拉活逻辑有问题,多半是轮询或死循环在作怪。给 main 进程和守护进程分别打不同的统计事件,半小时内如果反复出现 kill 和 restart,就该检查守护循环是不是在空转。从那以后,我每次集成这套多 module 工程,都强制走一遍验证流程:先跑 30 分钟观察存活率,再手动模拟一次强杀、一次后台任务滑动清除、一次系统内存压力测试,最后看耗电曲线和崩溃率。这套流程帮我及时排掉了不少厂商 ROM 上的意外表现,也避免了把“看起来能跑”误当成“真能扛”。希望帮到你。本文还有配套的精品资源,点击获取