Android 工业终端保活实战:前台服务 + 开机自启 + 更新自启的三重保障

Android 工业终端保活实战:前台服务 + 开机自启 + 更新自启的三重保障

适合场景:定制 Android 设备(工业平板、记录仪、自助终端等)需要 7×24 小时运行后台任务,定时上报设备状态。

一、背景

我们的应用运行在海康定制的 Android 工业终端上,核心职责是每 10 秒向服务器上报一次设备状态(电量、IP、序列号、定位等)。这类设备没有用户交互,App 必须"永远活着"。

消费级 App 谈"保活"是对抗系统,工业终端谈"保活"是合规地利用系统机制。本文介绍我们在生产环境中验证有效的三重保障方案。

二、整体方案

scss 复制代码
开机 ──→ BootCompletedReceiver ──→ startForegroundService()
                                      │
应用更新 ──→ AppUpdateReceiver ──→ startForegroundService()
                                      │
                                      ▼
                          MyBackgroundService(前台服务)
                          START_STICKY + Handler 定时循环

三、第一重:前台服务 + START_STICKY

Android 8.0(API 26)之后,后台启动 Service 会被系统直接抛 IllegalStateException,必须走前台服务(Foreground Service),并在 5 秒内调用 startForeground() 挂出通知:

kotlin 复制代码
class MyBackgroundService : Service() {

    private val mHandler = Handler(Looper.getMainLooper())
    private val mRunnable = Runnable {
        try {
            // 核心业务:上报设备状态 + 检查版本更新
            viewModel?.sendMsg(this, longitude, latitude)
            viewModel?.checkVersion(this)
        } catch (e: Exception) {
            // 注意:定时任务内必须 catch 所有异常,
            // 否则一次崩溃会导致整个循环停止
        }
        restartCommand() // 无论成功失败,都调度下一次
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                "service_channel", "后台服务", NotificationManager.IMPORTANCE_HIGH
            )
            getSystemService(NotificationManager::class.java)
                .createNotificationChannel(channel)
            val notification = Notification.Builder(this, "service_channel")
                .setContentTitle("服务运行中")
                .setSmallIcon(R.mipmap.app_icon)
                .build()
            startForeground(1, notification)
        }
        restartCommand()
        return START_STICKY // 关键:服务被系统杀掉后,系统会尝试重启
    }

    private fun restartCommand() {
        mHandler.removeCallbacks(mRunnable)
        // 间隔存数据库,支持远程/界面动态调整
        mHandler.postDelayed(mRunnable, getDuration() * 1000L)
    }
}

几个关键细节:

  • START_STICKY:系统内存不足杀掉服务后,会在资源允许时重建服务(intent 为 null,注意判空)。
  • 定时用 Handler.postDelayed 循环而非 TimerTimer 在异常后会终止整个调度,而 Runnable 末尾重新 postDelayed,配合全局 try-catch,保证单次任务异常不会中断后续轮询。
  • 轮询间隔存数据库(我们用 LitePal),可以随时改,不用发版。

四、第二重:开机自启

监听 BOOT_COMPLETED 广播。注意两点:需要声明 RECEIVE_BOOT_COMPLETED 权限;Android 8.0+ 必须用 startForegroundService()

kotlin 复制代码
class BootCompletedReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (context == null || intent == null) return
        if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
            val serviceIntent = Intent(context, MyBackgroundService::class.java)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                context.startForegroundService(serviceIntent)
            } else {
                context.startService(serviceIntent)
            }
        }
    }
}
xml 复制代码
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<receiver
    android:name=".BootCompletedReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

注意:Android 12+ 对"后台启动前台服务"有限制,但开机广播属于豁免场景之一,仍然可以启动。前提是应用至少被用户打开过一次(处于非 force-stopped 状态)。

五、第三重:应用更新后自启(最容易被忽略的一环)

设备上的 App 会远程推送 APK 自动更新。覆盖安装后,进程必然被杀,前台服务也没了------如果只依赖开机广播,设备不重启服务就一直挂着,状态上报中断。

解决方案是监听 ACTION_MY_PACKAGE_REPLACED(这个广播只发给"被更新的应用自己",不需要额外权限):

kotlin 复制代码
class AppUpdateReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        when (intent.action) {
            Intent.ACTION_MY_PACKAGE_REPLACED -> {
                // 应用覆盖安装完成,重新拉起后台服务
                val serviceIntent = Intent(context, MyBackgroundService::class.java)
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    context.startForegroundService(serviceIntent)
                } else {
                    context.startService(serviceIntent)
                }
            }
        }
    }
}
xml 复制代码
<receiver
    android:name=".AppUpdateReceiver"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
    </intent-filter>
</receiver>

这个广播同样是后台启动前台服务的豁免场景,实测 Android 13 的定制设备上稳定可用。

六、资源释放与防抖

onDestroy() 里要做完整的清理,否则服务重启后会累积多个定时器:

kotlin 复制代码
override fun onDestroy() {
    mHandler.removeCallbacks(mRunnable)  // 移除定时任务
    HikLocationManager.stop()            // 解绑外部 SDK 服务
    KafkaLocationManager.stop()          // 停掉 Kafka 消费线程
    super.onDestroy()
}

另外 restartCommand() 里先 removeCallbackspostDelayed,保证任何时刻最多只有一个待执行任务,避免 onStartCommand 被多次调用导致任务叠加。

七、总结

保障层 机制 覆盖场景
前台服务 startForeground + 常驻通知 防 LMK 低优先级查杀
START_STICKY 系统重建服务 被系统杀掉后恢复
开机广播 BOOT_COMPLETED 设备重启
更新广播 MY_PACKAGE_REPLACED APK 覆盖安装

这套方案面向的是自有硬件、自有签名的工业场景。如果你的 App 要上应用市场、跑在杂牌手机上,厂商的省电策略(自启动白名单、后台限制)才是最大的敌人,那就是另一个话题了。

相关推荐
字节暗面2 小时前
SO加固强度怎么量化?腾讯ACE与FairGuard静态分析实测
android·逆向
终端安全笔记4 小时前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
字节暗面4 小时前
一次游戏安全SO静态分析记录:腾讯ACE与FairGuard加固强度对比
android·逆向工程·游戏加固
传奇开心果编程4 小时前
【Jetpack Compose基础语法学与练】第9课 SideEffect副作用 + LaunchedEffect,处理异步任务和生命周期
android·学习·ui·kotlin·android jetpack
2401_833269306 小时前
RecyclerView多布局详解
android
v_34836087627 小时前
Android_8 zygote启动分析
android
消失的旧时光-19437 小时前
Android 系统层扫盲 11:JNI 到底是什么?Java / Kotlin 是怎么调用 C/C++ 的?
android·binder·zygote·system_server·aops
mmsx7 小时前
Android 地图几万要素卡成幻灯片?顶点缓冲批上传与显存复用
android·opengl·地图·栅格
北京自在科技18 小时前
谷歌 Find Hub 迎来新版本更新
android·安卓