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

设备管控 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_COMPLETEDUSER_UNLOCKEDMY_PACKAGE_REPLACED 进入统一入口,业务只恢复一次
强制停止或厂商整包清理 强制停止后必须保持停止;厂商未开放后台权限时允许恢复失败

**保活结论:**前台服务和系统管理的服务能降低回收概率,但不是不死进程。Q20 和原生模拟器清理最近任务时进程保持,华为 C3 上相同方案仍会被清理,能力明显依赖系统与后台权限。

**拉活结论:**双进程能在一侧仍存活时恢复另一侧;广播和 Job 能在系统给出入口时恢复全部业务,但都不保证时效。强制停止后必须保持停止,厂商整包清理且没有后续入口时也不能自行恢复。

7. 进阶:增强保活与拉活

上面的组合依赖"至少还有一个 Java 进程存活"或"系统之后再次给入口"。如果厂商清理会同时结束多个进程,或者普通系统恢复入口太慢,下一步需要把保活拉活 分开增强:屏幕联动方案降低被清理概率,Native flock 检测对端死亡,再由仍存活的 Java 进程请求恢复。

继续阅读 VIP 进阶篇: 《Android 设备管控开发实战:第三方设备如何增强管控 App 保活与拉活?Native 守护实战》

参考资料

相关推荐
奔跑的架构师4 小时前
[A-49]ARMv9/v8-PSCI接口规范与工作流程简介
android·linux·arm开发·arm
AFinalStone4 小时前
Android 7系统国际化(七)Resources 与 AssetManager 源码剖析
android·国际化·local
Android-Flutter5 小时前
Android 内存泄漏详解
android·kotlin
catchadmin5 小时前
Fiber 与 PHP 8.6 Polling API 异步初探
android·开发语言·php
Android-Flutter5 小时前
android Handler , Looper , Message , MessageQueue 详解
android·kotlin
雨声不在6 小时前
docker-android 重启后镜像起不来
android·docker·容器
阿pin6 小时前
Android随笔-Framework
android·framework
达达尼昂6 小时前
Flutter AI Harness 如何让 Agent 参与软件开发全流程
android·人工智能·后端
sRect6 小时前
用ESP32-S3开发板开发一个接收消息的BB机
android·serverless·嵌入式
AFinalStone7 小时前
Android 7系统无障碍服务(二)AccessibilityManagerService 启动与初始化
android·无障碍服务