工控常驻后台核心Service 前台服务、保活方案、守护进程实现工控APP要是能像微信一样躺在后台睡大觉那7x24小时运行的设备早就凉透了——今天咱们就来聊聊怎么让工控APP像打不死的小强一样顽强活着。一、为什么工控APP需要常驻后台工控场景和普通手机APP完全是两个世界。你手机上微信被杀后台最多就是收不到消息下次打开就行了。但工控设备如果后台服务停了生产线可能直接停摆——采集程序不跑了、PLC通信断了、数据库不写了损失是按秒计算的。工控APP的典型后台任务包括持续采集传感器数据温湿度、压力、电流等实时与PLC/单片机通过串口通信定时上传数据到云端服务器监听外设状态变化USB设备插拔、网络状态接收远程控制指令并执行这些任务都要求APP进程不能轻易被系统干掉这就是我们今天要解决的问题。二、普通 Service 与前台 Service 的区别2.1 普通 Service普通Service运行在主线程默认情况下和宿主进程同生共死。虽然它运行在后台但Android系统内存紧张时会毫不犹豫地把它连带进程一起杀掉。对于工控来说这等于判了死刑。// 普通Service系统想杀就杀publicclassMyServiceextendsService{OverridepublicintonStartCommand(Intentintent,intflags,intstartId){// 工作在后台但不保证存活returnSTART_STICKY;// 被杀后会尝试重启但不保证}}START_STICKY能做的就是被杀后系统会尝试重建Service但这个“尝试”很不靠谱——有时候重启失败有时候延迟很久才重启工控场景根本等不起。2.2 前台 Service前台Service是Android官方提供的“合法保活”手段。调用startForeground()后系统会把你的服务提升到前台进程级别优先级仅次于当前用户正在交互的Activity。前台Service必须在通知栏显示一条持久通知告诉用户有个东西在后台跑着呢。publicclassForegroundServiceextendsService{privatestaticfinalintNOTIFICATION_ID1001;privatestaticfinalStringCHANNEL_IDforeground_service_channel;OverridepublicvoidonCreate(){super.onCreate();createNotificationChannel();}OverridepublicintonStartCommand(Intentintent,intflags,intstartId){// 构建通知NotificationnotificationnewNotificationCompat.Builder(this,CHANNEL_ID).setContentTitle(工控采集服务运行中).setContentText(正在采集传感器数据...).setSmallIcon(R.drawable.ic_notification).setOngoing(true)// 不可滑动删除.build();// 提升为前台服务startForeground(NOTIFICATION_ID,notification);returnSTART_STICKY;}privatevoidcreateNotificationChannel(){if(Build.VERSION.SDK_INTBuild.VERSION_CODES.O){NotificationChannelchannelnewNotificationChannel(CHANNEL_ID,前台服务通知,NotificationManager.IMPORTANCE_LOW// 低优先级减少打扰);NotificationManagermanagergetSystemService(NotificationManager.class);manager.createNotificationChannel(channel);}}}前台Service的好处是系统不会轻易杀死它但缺点也很明显——通知栏必须挂一条通知。好在Android 8.0可以通过IMPORTANCE_LOW让通知不发出声音和震动工控设备反正也没人盯着通知栏看这个代价可以接受。三、保活方案大全光靠前台Service还不够某些深度定制的ROM尤其是国内厂商连前台Service都照杀不误。工控设备虽然系统环境相对简单但多几层保护总没坏处。3.1 1像素Activity保活这个方案思路很巧妙当APP退到后台时启动一个只有1像素大小的透明Activity。只要这个Activity在屏幕上系统就认为APP还在前台优先级极高。publicclassKeepAliveActivityextendsActivity{OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);// 设置为1像素大小WindowwindowgetWindow();window.setGravity(Gravity.START|Gravity.TOP);WindowManager.LayoutParamsparamswindow.getAttributes();params.x0;params.y0;params.width1;params.height1;window.setAttributes(params);}}配合Application的生命周期监听在onActivityPaused时判断如果当前没有可见Activity就启动这个1像素的保活Activity。用户完全感觉不到它的存在但系统会把你的进程当大爷供着。不过这个方案在高版本Android上效果减弱推荐作为辅助手段而非主要方案。3.2 双进程守护两个进程互相监控、互相拉起形成经典的守望相助模式。实现方式是在AndroidManifest.xml中给Service设置不同进程serviceandroid:name.LocalServiceandroid:process:local/serviceandroid:name.RemoteServiceandroid:process:remote/两个Service通过AIDL建立跨进程连接使用Binder的linkToDeath机制监听对方进程是否挂掉。一旦发现对方挂了立刻重新启动它。这个方案在早期Android版本上效果拔群但从Android 6.0开始系统对这种互相唤醒的行为做了限制。3.3 JobScheduler / WorkManager 定时唤醒这是Google官方推荐的方案。JobScheduler可以在满足特定条件充电中、网络可用、空闲状态时执行任务。WorkManager是对JobScheduler的高层封装兼容性更好。// 使用WorkManager定时执行保活任务PeriodicWorkRequestkeepAliveWorknewPeriodicWorkRequest.Builder(KeepAliveWorker.class,15,TimeUnit.MINUTES// 每15分钟执行一次).build();WorkManager.getInstance(context).enqueue(keepAliveWork);虽然定时周期不能短于15分钟但对于工控场景——定时检查服务状态如果挂了尝试拉起——这个频率足够了。3.4 AlarmManager 定时唤醒AlarmManager是更底层的定时器可以在指定时间发送广播唤醒APPAlarmManageralarmManager(AlarmManager)getSystemService(ALARM_SERVICE);IntentintentnewIntent(this,KeepAliveReceiver.class);PendingIntentpendingIntentPendingIntent.getBroadcast(this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE);// 每5分钟触发一次alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,SystemClock.elapsedRealtime(),5*60*1000,pendingIntent);配合BroadcastReceiver接收闹钟广播在onReceive中检查服务状态并尝试拉起。setExactAndAllowWhileIdle可以在Doze模式下也能准时触发是工控场景的好帮手。四、守护进程实现思路C层fork子进程当Java层的保活手段都被系统制裁后还可以祭出终极大招——C语言层的守护进程。基本思路是通过JNI调用C代码在C层fork()一个子进程。子进程脱离Java虚拟机独立运行监控父进程Java进程的存活状态。一旦发现父进程挂了子进程通过am startservice命令重新启动Service// C层守护进程核心逻辑pid_tpidfork();if(pid0){// 子进程守护者intparentPidgetppid();while(1){sleep(5);// 检查父进程是否存活if(kill(parentPid,0)!0){// 父进程已死重启Serviceexeclp(am,am,startservice,-n,com.example/.ForegroundService,NULL);exit(0);}}}这个方案的优点是子进程是完全独立的Linux进程Android系统很难区分它和普通后台进程存活率极高。缺点是需要root权限或者系统签名才能用am命令而且在不同Android版本上fork行为可能有差异。五、工控场景下保活方案选择建议工控设备环境相对可控不需要像普通APP那样跟各种ROM斗智斗勇。我的推荐优先级优先级方案适用场景第一优先前台Service所有工控APP的基础配置改动最小第二优先JobScheduler/WorkManager辅助检查与拉活官方推荐第三优先AlarmManager需要较精确的定时唤醒备用方案双进程守护对系统版本有要求的场景如果你的设备是自己刷的固件建议直接在固件层把APP设为白名单进程这才是最彻底的办法。在系统源码中修改ProcessList的mOomAdj参数把你的APP包名加入白名单系统内存再怎么紧张也不会杀你的进程。六、完整代码示例工控前台服务的保活实现下面给出一个可直接用于工控项目的前台Service完整实现publicclassIndustrialForegroundServiceextendsService{privatestaticfinalintNOTIFICATION_ID0x1001;OverridepublicvoidonCreate(){super.onCreate();// 开启双进程守护bindRemoteService();// 注册闹钟保活scheduleKeepAliveAlarm();}OverridepublicintonStartCommand(Intentintent,intflags,intstartId){// 前台通知NotificationnotificationbuildForegroundNotification();startForeground(NOTIFICATION_ID,notification);// 这里执行你的工控核心逻辑startDataCollection();startSerialCommunication();startCloudUpload();returnSTART_STICKY;}privateNotificationbuildForegroundNotification(){// Android 8.0 通知渠道StringchannelIdindustrial_channel;if(Build.VERSION.SDK_INTBuild.VERSION_CODES.O){NotificationChannelchannelnewNotificationChannel(channelId,工控服务,NotificationManager.IMPORTANCE_LOW);channel.setShowBadge(false);((NotificationManager)getSystemService(NOTIFICATION_SERVICE)).createNotificationChannel(channel);}returnnewNotificationCompat.Builder(this,channelId).setContentTitle(工控采集服务运行中).setContentText(正在实时采集数据...).setSmallIcon(R.drawable.ic_industry).setOngoing(true).setPriority(NotificationCompat.PRIORITY_LOW).build();}privatevoidscheduleKeepAliveAlarm(){AlarmManageram(AlarmManager)getSystemService(ALARM_SERVICE);IntentintentnewIntent(this,KeepAliveReceiver.class);PendingIntentpiPendingIntent.getBroadcast(this,0,intent,PendingIntent.FLAG_IMMUTABLE|PendingIntent.FLAG_UPDATE_CURRENT);if(am!null){am.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,SystemClock.elapsedRealtime()5*60*1000,5*60*1000,pi);}}privatevoidstartDataCollection(){// TODO: 传感器数据采集初始化}privatevoidstartSerialCommunication(){// TODO: 串口通信初始化}privatevoidstartCloudUpload(){// TODO: 云端上报初始化}privatevoidbindRemoteService(){// 绑定远程守护进程IntentremoteIntentnewIntent(this,RemoteGuardService.class);bindService(remoteIntent,newServiceConnection(){OverridepublicvoidonServiceConnected(ComponentNamename,IBinderservice){// 注册死亡回调try{service.linkToDeath(()-{// Remote进程死了重启它startService(newIntent(IndustrialForegroundService.this,RemoteGuardService.class));},0);}catch(RemoteExceptione){e.printStackTrace();}}OverridepublicvoidonServiceDisconnected(ComponentNamename){}},Context.BIND_AUTO_CREATE);}}在AndroidManifest.xml中声明serviceandroid:name.IndustrialForegroundServiceandroid:foregroundServiceTypedataSyncandroid:exportedfalse/serviceandroid:name.RemoteGuardServiceandroid:process:remoteandroid:exportedfalse/uses-permissionandroid:nameandroid.permission.FOREGROUND_SERVICE/uses-permissionandroid:nameandroid.permission.FOREGROUND_SERVICE_DATA_SYNC/记住保活是系统工程不是单一技术点。前台上Service是基础WorkManager是补给固件层白名单是终极保险——三层防护下来你的工控APP想死都难。