设备管控 App 的持续运行要拆成两个问题:保活 是在进程仍存活时降低被回收概率;拉活是在进程已经退出后,重新获得执行入口并恢复业务。死亡检测只是拉活的前置条件,不等于已经把进程拉起来。
本文给出常见的"保活 + 拉活"组合,并结合原项目已有记录说明它们能做到哪里。强制停止后,普通 APK 没有公开接口保证自行恢复。
1. 先分清:哪些方案保活,哪些方案拉活
| 方案 | 主要能力 | 触发与效果 | 失效边界 |
|---|---|---|---|
| 前台服务 | 保活 | 进程存活时提高重要性,降低后台回收概率 | 厂商清理、内存压力和强制停止仍可结束进程 |
START_STICKY |
拉活 | Service 被系统回收后,系统可择机重新创建 | 没有重启时限;强制停止后不执行 |
| 系统管理的无障碍服务 | 保活 + 拉活入口 | 承载真实管控功能;授权有效时系统可能重新绑定 | 用户关闭授权、应用进入 stopped 状态或厂商限制时失效 |
| Java 双进程 | 死亡检测 + 拉活 | 一侧收到 Binder 断开后,由存活进程请求重建对端 | 两侧同时死亡时无法互拉;不等于整包保活 |
| 系统广播 | 事件型拉活 | 开机、解锁或 APK 更新时获得一次恢复入口 | 没有系统事件就不会触发,且后台启动可能受限 |
JobScheduler |
延迟拉活 | 系统调度到 Job 后检查并请求恢复 | 执行时间不精确;强制停止后不执行 |

2. 保活:前台服务降低核心进程被回收概率
核心服务放进 :core 进程,立即进入前台并返回 START_STICKY。Android 14 以后必须声明真实业务类型;下面仅以真实业务确属 specialUse、且目标分发渠道认可的管控会话为例,不能为保活拼接无关类型。
xml
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<service
android:name=".keepalive.ControlForegroundService"
android:process=":core"
android:exported="false"
android:stopWithTask="false"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="managed-device-control-session" />
</service>
<service
android:name=".keepalive.GuardService"
android:process=":guard"
android:exported="false" />
服务先展示持续通知,再初始化业务。onStartCommand() 的 intent 可能为空,待恢复状态必须写入 DataStore 或数据库。
kotlin
class ControlForegroundService : Service() {
private val binder = Binder()
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private lateinit var guardLink: PeerLink
override fun onCreate() {
super.onCreate()
createNotificationChannel()
val type = if (Build.VERSION.SDK_INT >= 34) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
} else {
0
}
ServiceCompat.startForeground(this, 1001, buildNotification(), type)
guardLink = PeerLink(this, GuardService::class.java) {
KeepAliveLog.peerLost("guard")
startGuard()
}
startGuard()
guardLink.connect()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
scope.launch {
RestoreCoordinator.restore(intent?.action ?: "sticky-recreate")
}
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder = binder
private fun startGuard() {
runCatching {
startService(Intent(this, GuardService::class.java))
}.onFailure(KeepAliveLog::guardStartFailed)
}
override fun onDestroy() {
guardLink.close()
scope.cancel()
super.onDestroy()
}
}
这里要拆开理解:startForeground() 提供的是保活能力,返回 START_STICKY 提供的是系统择机重建 Service 的拉活机会。后者没有重启时限;Android 12 以后后台启动还可能被拒绝,失败后应安排 Job,而不是循环调用。
3. 拉活一:Java 双进程检测死亡并由存活端恢复
双进程本身不会让整个应用更难被系统清理,它解决的是拉活:Binder 断开负责发现单进程死亡,仍存活的一侧负责请求重建对端。:core 先启动 :guard,两边再以 BIND_AUTO_CREATE 互相绑定。
java
public final class PeerLink {
private final Context app;
private final Class<? extends Service> peerService;
private final Runnable onPeerLost;
private final Handler main = new Handler(Looper.getMainLooper());
private boolean bound;
private boolean closed;
private final Runnable reconnect = this::connect;
public PeerLink(Context context,
Class<? extends Service> peerService,
Runnable onPeerLost) {
this.app = context.getApplicationContext();
this.peerService = peerService;
this.onPeerLost = onPeerLost;
}
private final ServiceConnection connection = new ServiceConnection() {
@Override public void onServiceConnected(ComponentName name, IBinder binder) {
bound = true;
}
@Override public void onServiceDisconnected(ComponentName name) {
main.post(PeerLink.this::retry);
}
@Override public void onBindingDied(ComponentName name) {
main.post(PeerLink.this::retry);
}
};
public void connect() {
if (closed || bound) return;
Intent intent = new Intent(app, peerService);
bound = app.bindService(
intent,
connection,
Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT
);
if (!bound) schedule();
}
private void retry() {
if (closed) return;
if (bound) {
try { app.unbindService(connection); } catch (RuntimeException ignored) {}
}
bound = false;
onPeerLost.run();
schedule();
}
private void schedule() {
main.removeCallbacks(reconnect);
main.postDelayed(reconnect, 1500L);
}
public void close() {
closed = true;
main.removeCallbacksAndMessages(null);
if (bound) {
try { app.unbindService(connection); } catch (RuntimeException ignored) {}
}
bound = false;
}
}
GuardService 必须同时启动并绑定。只互相绑定时,:core 死亡会移除绑定,:guard 可能随即销毁;先启动再绑定,守护进程才有机会发现断开并恢复核心服务。
kotlin
class GuardService : Service() {
private val binder = Binder()
private lateinit var coreLink: PeerLink
override fun onCreate() {
super.onCreate()
coreLink = PeerLink(this, ControlForegroundService::class.java) {
KeepAliveEntry.requestCore(this, "guard-detected-core-death")
}
KeepAliveEntry.requestCore(this, "guard-created")
coreLink.connect()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder = binder
override fun onDestroy() {
coreLink.close()
super.onDestroy()
}
}
业务只在核心服务初始化;守护进程只检测存活、请求重建和重新绑定,避免两边重复创建 Socket 或监听。
若产品本来就依赖无障碍,可将它放在 :access。授权有效且应用未处于 stopped 状态时,系统可能重连;成功后再接回核心服务。
kotlin
class ControlAccessibilityService : AccessibilityService() {
override fun onServiceConnected() {
super.onServiceConnected()
KeepAliveEntry.requestCore(this, "accessibility-connected")
}
override fun onInterrupt() = Unit
}
无障碍必须服务于真实功能;用户关闭授权或应用被停止时,这条入口也会消失。
4. 拉活二:广播与 JobScheduler 等待系统入口
双进程只能处理"死一个、还剩一个"。全部 Java 进程消失后,依靠开机、用户解锁和当前 APK 覆盖更新重新进入。
xml
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<receiver
android:name=".keepalive.RecoveryReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.USER_UNLOCKED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<service
android:name=".keepalive.RecoveryJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true" />
Receiver 只请求核心服务并安排延迟检查;依赖用户凭据的业务在解锁后恢复。
kotlin
object KeepAliveEntry {
fun requestCore(context: Context, origin: String) {
val intent = Intent(context, ControlForegroundService::class.java)
.setAction(origin)
try {
ContextCompat.startForegroundService(context, intent)
} catch (blocked: IllegalStateException) {
RecoveryJob.schedule(context)
} catch (denied: SecurityException) {
RecoveryJob.schedule(context)
}
}
}
class RecoveryReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
KeepAliveEntry.requestCore(context, intent.action ?: "broadcast")
RecoveryJob.schedule(context, delayMs = 60_000L)
}
}
object RecoveryJob {
private const val JOB_ID = 21001
fun schedule(context: Context, delayMs: Long = 15 * 60_000L) {
val service = ComponentName(context, RecoveryJobService::class.java)
val job = JobInfo.Builder(JOB_ID, service)
.setMinimumLatency(delayMs)
.setOverrideDeadline(delayMs + 15 * 60_000L)
.setPersisted(true)
.build()
context.getSystemService(JobScheduler::class.java).schedule(job)
}
}
class RecoveryJobService : JobService() {
override fun onStartJob(params: JobParameters): Boolean {
KeepAliveEntry.requestCore(this, "job")
return false
}
override fun onStopJob(params: JobParameters): Boolean = true
}
JobScheduler 只是延迟机会,执行时间由系统决定。强制停止后,Job 和普通广播都不能自恢复,必须等待用户或管理员重新进入应用。
5. 所有入口最终只恢复一次业务状态
多个入口只请求核心服务,由 RestoreCoordinator 串行、幂等地恢复业务;它负责避免重复连接,不是新的保活方案。
kotlin
object RestoreCoordinator {
private val mutex = Mutex()
suspend fun restore(origin: String) = mutex.withLock {
val desired = RuntimeStore.read()
if (!desired.controlEnabled) return
AccessibilityRules.ensureRegistered()
CommandChannel.ensureConnected(
endpoint = desired.endpoint,
revision = desired.configRevision
)
KeepAliveLog.ready(origin, desired.configRevision)
}
}
两个 ensure 对相同版本直接复用,版本变化时先关闭旧连接再替换。
6. 实测:分别看保活与拉活能力
以下只列原项目已有记录。"普通方案"指前台服务、系统管理的无障碍服务、广播和 JobScheduler;Java 双进程只覆盖单进程死亡,不改变整包停止结论。
| 设备与系统 | 操作 | 已记录结果 |
|---|---|---|
| Q20 中性设备,Android 11 | 最近任务清除全部、上滑关闭应用 | 前台服务和系统绑定服务测试中,各进程均未被杀;JobScheduler 测试可再次唤醒应用 |
| Q20 中性设备,Android 11 | 设置页强制停止或 adb shell am force-stop |
所有普通方案均被终止,无法自行唤醒 |
| Q20 中性设备,Android 11 | 设备重启 | 收到开机广播后可以进入恢复流程 |
| 华为 C3,Android 10 | 最近任务清除全部、上滑关闭应用 | 仅靠前台服务或系统绑定服务仍会被杀 |
| 华为 C3,Android 10 | JobScheduler 延迟恢复 | 默认后台限制下无效;开启"允许后台活动"后才具备执行机会 |
| 华为 C3,Android 10 | 设置页强制停止或 adb shell am force-stop |
被终止,普通方案不能恢复 |
| 原生模拟器 Android 10、11、12、12L、13、14、15 | 清理最近任务 / 强制停止 | 清理最近任务时普通方案进程保持;强制停止后普通方案失效 |
测试时先记录 :core、:guard、:access 的 PID,再验收以下五条链路。
| 必测场景 | 正确判定 |
|---|---|
只杀 :core |
:guard 收到 Binder death,核心服务重新创建,业务通道只恢复一次 |
只杀 :guard |
:core 继续运行并重新绑定守护服务 |
| 杀掉全部应用进程 | 双进程失效;只能等待无障碍系统回连、Job 或后续系统广播,不承诺立即恢复 |
| 设备重启或 APK 覆盖更新 | BOOT_COMPLETED、USER_UNLOCKED 或 MY_PACKAGE_REPLACED 进入统一入口,业务只恢复一次 |
| 强制停止或厂商整包清理 | 强制停止后必须保持停止;厂商未开放后台权限时允许恢复失败 |
**保活结论:**前台服务和系统管理的服务能降低回收概率,但不是不死进程。Q20 和原生模拟器清理最近任务时进程保持,华为 C3 上相同方案仍会被清理,能力明显依赖系统与后台权限。
**拉活结论:**双进程能在一侧仍存活时恢复另一侧;广播和 Job 能在系统给出入口时恢复全部业务,但都不保证时效。强制停止后必须保持停止,厂商整包清理且没有后续入口时也不能自行恢复。
7. 进阶:增强保活与拉活
上面的组合依赖"至少还有一个 Java 进程存活"或"系统之后再次给入口"。如果厂商清理会同时结束多个进程,或者普通系统恢复入口太慢,下一步需要把保活 和拉活 分开增强:屏幕联动方案降低被清理概率,Native flock 检测对端死亡,再由仍存活的 Java 进程请求恢复。
继续阅读 VIP 进阶篇: 《Android 设备管控开发实战:第三方设备如何增强管控 App 保活与拉活?Native 守护实战》