欢迎光临
我们一直在努力

Android 设备管控开发实战:常见保活与拉活方案怎么组合,实测能到什么程度?

设备管控 App 的持续运行要拆成两个问题:保活是在进程仍存活时降低被回收概率;拉活是在进程已经退出后,重新获得执行入口并恢复业务。死亡检测只是拉活的前置条件,不等于已经把进程拉起来。

本文给出常见的“保活 + 拉活”组合,并结合原项目已有记录说明它们能做到哪里。强制停止后,普通 APK 没有公开接口保证自行恢复。

1. 先分清:哪些方案保活,哪些方案拉活

方案主要能力触发与效果失效边界
前台服务 保活 进程存活时提高重要性,降低后台回收概率 厂商清理、内存压力和强制停止仍可结束进程
START_STICKY 拉活 Service 被系统回收后,系统可择机重新创建 没有重启时限;强制停止后不执行
系统管理的无障碍服务 保活 + 拉活入口 承载真实管控功能;授权有效时系统可能重新绑定 用户关闭授权、应用进入 stopped 状态或厂商限制时失效
Java 双进程 死亡检测 + 拉活 一侧收到 Binder 断开后,由存活进程请求重建对端 两侧同时死亡时无法互拉;不等于整包保活
系统广播 事件型拉活 开机、解锁或 APK 更新时获得一次恢复入口 没有系统事件就不会触发,且后台启动可能受限
JobScheduler 延迟拉活 系统调度到 Job 后检查并请求恢复 执行时间不精确;强制停止后不执行

常见保活组合:进程内互保与系统恢复入口

2. 保活:前台服务降低核心进程被回收概率

核心服务放进 :core 进程,立即进入前台并返回 START_STICKY。Android 14 以后必须声明真实业务类型;下面仅以真实业务确属 specialUse、且目标分发渠道认可的管控会话为例,不能为保活拼接无关类型。

<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 或数据库。

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 互相绑定。

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 可能随即销毁;先启动再绑定,守护进程才有机会发现断开并恢复核心服务。

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 状态时,系统可能重连;成功后再接回核心服务。

class ControlAccessibilityService : AccessibilityService() {
override fun onServiceConnected() {
super.onServiceConnected()
KeepAliveEntry.requestCore(this, "accessibility-connected")
}

override fun onInterrupt() = Unit
}

无障碍必须服务于真实功能;用户关闭授权或应用被停止时,这条入口也会消失。

4. 拉活二:广播与 JobScheduler 等待系统入口

双进程只能处理“死一个、还剩一个”。全部 Java 进程消失后,依靠开机、用户解锁和当前 APK 覆盖更新重新进入。

<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 只请求核心服务并安排延迟检查;依赖用户凭据的业务在解锁后恢复。

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 串行、幂等地恢复业务;它负责避免重复连接,不是新的保活方案。

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 守护实战》

参考资料

  • Foreground services overview
  • Restrictions on starting a foreground service from the background
  • Bound services overview
  • JobScheduler API
赞(0)
未经允许不得转载:171主机测评 » Android 设备管控开发实战:常见保活与拉活方案怎么组合,实测能到什么程度?
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址