15、后台采集与前台服务:使用前台Service持续采集光线数据,通知栏提示,避免被系统杀死

好,咱们继续往下走。

前面几章,我们都是在 Activity 里直接采集光线数据。这有个问题——App 一退到后台,系统就可能把你的进程给杀了。尤其是国产手机,那后台管理策略,你懂的。我当年做第一个光线采集项目时,就吃过这个亏。用户反馈说「App 挂后台一会儿,数据就不动了」。查了半天,原来是系统把 Service 给回收了。

那怎么解决?用前台 Service。

为什么普通 Service 不行?

Android 系统有个优先级机制。普通 Service 的优先级其实不高,跟 Activity 不在前台时差不多。系统内存吃紧时,第一个被干掉的就是它。

前台 Service 不一样。它会在通知栏显示一条通知,告诉用户「嘿,我还在跑呢」。系统看到这条通知,就知道这个 Service 对用户来说是可见的,优先级会大幅提升。说白了,就是给系统一个「别杀我」的理由。

核心区别:

  • 普通 Service:后台运行,无通知,低优先级,容易被杀
  • 前台 Service:后台运行,有通知,高优先级,系统尽量不杀

实现步骤:三步走

我个人习惯把前台 Service 的实现拆成三步。这样思路清晰,不容易漏东西。

  1. 创建 Service 类——继承 Service,重写生命周期方法
  2. 构建通知——创建 NotificationChannel(Android 8.0+ 必须)和 Notification
  3. 调用 startForeground()——把 Service 提升为前台服务

嗯,这里要注意:startForeground() 必须在 Service 的 onCreate()onStartCommand() 里调用,而且要在 5 秒内完成。否则系统会报错。

代码实战:光线采集前台 Service

来,直接看代码。我写了一个完整的 LightSensorService,你拿去就能用。

public class LightSensorService extends Service {

    private static final String CHANNEL_ID = "light_sensor_channel";
    private static final int NOTIFICATION_ID = 1001;
    private SensorManager sensorManager;
    private Sensor lightSensor;
    private float lastLux = 0f;

    @Override
    public void onCreate() {
        super.onCreate();
        // 初始化传感器
        sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
        lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
        // 创建通知渠道
        createNotificationChannel();
        // 启动前台服务
        startForeground(NOTIFICATION_ID, buildNotification("光线传感器已启动"));
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // 注册传感器监听
        if (lightSensor != null) {
            sensorManager.registerListener(sensorEventListener, lightSensor,
                    SensorManager.SENSOR_DELAY_NORMAL);
        }
        // 如果服务被杀死,自动重启
        return START_STICKY;
    }

    private final SensorEventListener sensorEventListener = new SensorEventListener() {
        @Override
        public void onSensorChanged(SensorEvent event) {
            lastLux = event.values[0];
            // 更新通知栏显示
            Notification notification = buildNotification(
                    "当前光照强度: " + lastLux + " lux");
            NotificationManager manager = (NotificationManager)
                    getSystemService(NOTIFICATION_SERVICE);
            manager.notify(NOTIFICATION_ID, notification);
        }

        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {
            // 精度变化时,可以记录日志
        }
    };

    private Notification buildNotification(String content) {
        Intent notificationIntent = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(
                this, 0, notificationIntent,
                PendingIntent.FLAG_IMMUTABLE);

        return new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle("光线采集服务")
                .setContentText(content)
                .setSmallIcon(R.drawable.ic_light)
                .setContentIntent(pendingIntent)
                .setOngoing(true)  // 不可滑动删除
                .build();
    }

    private void createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(
                    CHANNEL_ID,
                    "光线传感器",
                    NotificationManager.IMPORTANCE_LOW
            );
            channel.setDescription("用于显示光线采集状态");
            NotificationManager manager = getSystemService(NotificationManager.class);
            manager.createNotificationChannel(channel);
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        sensorManager.unregisterListener(sensorEventListener);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

小提示:我在 onStartCommand() 里返回了 START_STICKY。这样即使 Service 被系统意外杀死,它也会自动重启。不过别太依赖这个,国产系统上不一定管用。

别忘了清单文件注册

Service 必须在 AndroidManifest.xml 里注册,否则系统找不到它。还要加上 FOREGROUND_SERVICE 权限。

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<application ...>
    <service
        android:name=".LightSensorService"
        android:enabled="true"
        android:exported="false" />
</application>

注意:Android 9(API 28)开始,前台 Service 必须声明 foregroundServiceType。如果你只做传感器采集,可以声明为 dataSyncspecialUse。不声明的话,高版本系统会直接抛异常。

启动与停止 Service

在 Activity 里启动和停止这个 Service,代码很简单:

// 启动
Intent intent = new Intent(this, LightSensorService.class);
startForegroundService(intent);

// 停止
Intent intent = new Intent(this, LightSensorService.class);
stopService(intent);

注意,这里用的是 startForegroundService(),不是 startService()。这是 Android 8.0 引入的新方法,专门用于启动前台 Service。如果你用 startService(),系统会认为你忘了调用 startForeground(),过一会儿就会报错。

前台 Service 的生命周期

我画了一张图,帮你理清整个流程:

前台 Service 生命周期流程图 Activity 调用 startForegroundService() Service.onCreate() 创建通知 + startForeground() Service.onStartCommand() 注册传感器监听 持续运行 通知栏实时更新光照值 关键点 • startForeground() 必须在 5秒内调用 • 通知必须设置 ongoing=true • 返回 START_STICKY 可自动重启

避坑指南

我曾经在几个项目里踩过前台 Service 的坑,分享给你:

  • 通知渠道必须创建:Android 8.0 以上,不创建 NotificationChannel 直接发通知,通知不会显示。而且这个坑不会崩溃,只是静默失败,特别难排查。
  • 权限问题:Android 13(API 33)开始,POST_NOTIFICATIONS 权限需要动态申请。如果用户拒绝,前台 Service 的通知栏就不显示了。但 Service 本身还能跑。
  • 国产手机适配:小米、华为、OPPO 等厂商有自己的后台管理策略。即使用了前台 Service,也可能被限制。我建议在 onStartCommand() 里加个日志,看看 Service 是否真的在跑。
  • 不要频繁更新通知:每次传感器数据变化都更新通知,会导致通知栏闪烁。我一般设置一个阈值,比如变化超过 10 lux 才更新,或者每 2 秒更新一次。

我的习惯:在开发阶段,我会在通知栏显示详细数据,方便调试。上线前,我会把通知内容改成「光线采集服务运行中」,减少对用户的打扰。毕竟没人喜欢通知栏一直闪。

总结一下

前台 Service 是 Android 后台任务的「免死金牌」。虽然不能保证 100% 不被杀,但至少能让系统三思而后行。配合光线传感器,你就可以实现一个稳定、可靠的后台采集服务。

嗯,这一章的内容就到这儿。代码不多,但每一行都有它的道理。你动手试试,把 Service 跑起来,看看通知栏的变化。实践出真知。


公众号:蓝海资料掘金营,微信deep3321