18、传感器事件与线程管理:传感器事件队列、Handler与Looper使用、异步处理模式、避免ANR

传感器开发里有个很常见的坑——数据来得太快,UI 来不及反应。我早期做计步器项目时就吃过这个亏:传感器回调里直接更新 TextView,结果界面卡成 PPT,用户走了一百步,屏幕上数字才跳到三十。嗯,今天我们就来聊聊怎么优雅地处理传感器事件,以及如何避免 ANR 这个“应用杀手”。

传感器事件的“脾气”

传感器事件是通过 onSensorChanged() 回调传递给我们的。这个回调跑在哪个线程?答案是:取决于你注册传感器时用的 Looper

如果你用 SensorManager.registerListener(listener, sensor, rate, handler) 传了一个 Handler,那么回调就发生在该 Handler 绑定的线程上。如果你没传 Handler,默认就在主线程。你想想看,主线程上每秒几十上百次的回调,UI 还能流畅吗?

⚠️ 我曾经见过一个项目:直接在 onSensorChanged() 里做文件写入操作,结果 ANR 频发。传感器回调里千万别做耗时操作,这是铁律。

传感器事件队列:数据洪流的“缓冲池”

传感器硬件产生数据的频率远高于我们能处理的频率。系统内部维护了一个事件队列,把传感器数据先缓存起来。我们通过 onSensorChanged() 一个个取出来处理。

但这里有个关键点:事件队列是共享的。如果你处理得太慢,队列会积压,新事件会覆盖旧事件吗?不会。系统会一直往队列里塞,直到队列满。满了之后怎么办?丢掉最旧的事件。这就是为什么有时候你会感觉传感器数据“跳变”——因为中间的一些事件被丢弃了。

我个人习惯的做法是:在 onSensorChanged() 里只做最轻量的操作——把数据塞进一个自定义的线程安全队列,然后立刻返回。真正的处理逻辑交给工作线程。

// 线程安全的传感器事件队列
private final ConcurrentLinkedQueue<SensorEvent> eventQueue = new ConcurrentLinkedQueue<>();

@Override
public void onSensorChanged(SensorEvent event) {
    // 只做入队操作,绝不在这里处理数据
    eventQueue.offer(event);
}

Handler 与 Looper:线程间通信的“信使”

Handler 和 Looper 是 Android 线程通信的核心。说白了,Looper 就是一个消息循环,不断从消息队列里取消息,交给 Handler 处理。

主线程默认就有 Looper,所以我们可以直接在主线程创建 Handler。但工作线程需要手动调用 Looper.prepare()Looper.loop()

在传感器场景下,我建议你创建一个专用的工作线程,让它持有自己的 Looper 和 Handler。这样传感器事件就在工作线程处理,不阻塞主线程。

// 创建一个带 Looper 的工作线程
HandlerThread sensorThread = new HandlerThread("sensor-thread");
sensorThread.start();

// 获取该线程的 Looper,创建 Handler
Handler sensorHandler = new Handler(sensorThread.getLooper()) {
    @Override
    public void handleMessage(Message msg) {
        // 在这里处理传感器数据
        // 可以做滤波、计算、存储等操作
    }
};

// 注册传感器时传入这个 Handler
sensorManager.registerListener(listener, accelerometer, 
    SensorManager.SENSOR_DELAY_NORMAL, sensorHandler);
💡 小技巧:HandlerThread 用完后记得调用 quitSafely() 释放资源。我见过不少开发者忘记这步,导致线程泄漏。

异步处理模式:让传感器数据“流水线”起来

传感器数据处理的理想模式是生产者-消费者模型。传感器是生产者,不断产生事件;工作线程是消费者,不断消费事件。中间用队列解耦。

我常用的异步处理模式是这样的:

  1. 采集层:onSensorChanged() 里只做入队操作
  2. 缓冲层:线程安全的队列,暂存事件
  3. 处理层:工作线程从队列取数据,做滤波、特征提取等
  4. 输出层:处理结果通过 Handler 发回主线程更新 UI

这样做的好处很明显:每一层各司其职,互不阻塞。即使传感器数据爆发式增长,也只是队列变长,不会卡死 UI。

核心原则:主线程只做 UI 更新,不做数据处理。传感器回调里只做数据传递,不做业务逻辑。

避免 ANR:传感器开发的“生死线”

ANR(Application Not Responding)是 Android 系统对主线程阻塞的警告。主线程超过 5 秒没处理完输入事件,或者 BroadcastReceiver 超过 10 秒没返回,系统就会弹出 ANR 对话框。

传感器场景下,最容易触发 ANR 的操作有:

  • 在 onSensorChanged() 里做网络请求
  • 在 onSensorChanged() 里写数据库
  • 在 onSensorChanged() 里做复杂计算(如 FFT)
  • 在 onSensorChanged() 里更新大量 UI 元素

我曾经接手过一个项目,传感器回调里直接调用了 Retrofit 的网络接口。结果用户一走路,应用就 ANR。排查了半天才发现问题。

避免 ANR 的几条铁律:

操作类型 允许在传感器回调中执行? 建议做法
变量赋值 ✅ 可以 直接赋值,但要考虑线程安全
入队操作 ✅ 可以 使用 ConcurrentLinkedQueue 等线程安全集合
UI 更新 ❌ 不可以 通过 Handler 发到主线程
文件 I/O ❌ 不可以 在工作线程中异步执行
网络请求 ❌ 绝对不可以 使用协程或 RxJava 异步处理
数据库操作 ❌ 不可以 使用 Room 的异步查询

实战:一个完整的传感器异步处理框架

下面是我在项目中常用的传感器处理框架,你可以直接拿来用:

public class SensorManagerWrapper {
    private HandlerThread sensorThread;
    private Handler sensorHandler;
    private Handler mainHandler;
    private ConcurrentLinkedQueue<float[]> dataQueue;
    
    public SensorManagerWrapper() {
        sensorThread = new HandlerThread("sensor-worker");
        sensorThread.start();
        sensorHandler = new Handler(sensorThread.getLooper());
        mainHandler = new Handler(Looper.getMainLooper());
        dataQueue = new ConcurrentLinkedQueue<>();
    }
    
    // 传感器回调——只做入队
    private SensorEventListener listener = new SensorEventListener() {
        @Override
        public void onSensorChanged(SensorEvent event) {
            float[] values = event.values.clone();
            dataQueue.offer(values);
            // 触发工作线程处理
            sensorHandler.post(processingRunnable);
        }
        
        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {
            // 精度变化一般不需要频繁处理
        }
    };
    
    // 工作线程的处理逻辑
    private Runnable processingRunnable = new Runnable() {
        @Override
        public void run() {
            float[] data = dataQueue.poll();
            while (data != null) {
                // 在这里做滤波、计算等耗时操作
                float result = processData(data);
                // 把结果发回主线程
                mainHandler.post(() -> updateUI(result));
                data = dataQueue.poll();
            }
        }
    };
    
    private float processData(float[] values) {
        // 示例:简单的低通滤波
        // 实际项目中可能是步数检测、姿态解算等
        return values[0] * 0.8f + values[1] * 0.2f;
    }
    
    private void updateUI(float value) {
        // 更新 UI 控件
        // textView.setText(String.valueOf(value));
    }
    
    public void release() {
        sensorThread.quitSafely();
    }
}

SVG:传感器事件处理流程

传感器事件异步处理流程 传感器硬件 系统 Sensor 事件队列 onSensorChanged() 只做入队操作,不处理业务 ConcurrentLinkedQueue<SensorEvent> 线程安全,解耦生产者与消费者 工作线程 滤波/计算/存储 主线程 Handler 更新 UI 结果回调

这张图展示了传感器事件从硬件到 UI 的完整流转路径。核心思想就是:每一层只做一件事,层与层之间用队列或 Handler 解耦

💡 我的经验:如果你使用 Kotlin,可以考虑用协程的 Channel 替代 ConcurrentLinkedQueue,代码会更简洁。但 Java 项目里 ConcurrentLinkedQueue 已经足够好用了。

总结

传感器事件管理说白了就三件事:别在主线程处理、用队列缓冲、异步回传结果。做到这三点,你的传感器应用基本不会出现 ANR 问题。

我记得刚入行时,带我的前辈说过一句话:“传感器数据就像流水,你堵不住它,只能引导它。” 嗯,现在想想,确实如此。


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