18、性能优化:避免在onSensorChanged中做耗时操作、使用HandlerThread处理传感器数据

距离传感器这玩意儿,数据来得特别快。有多快?几十毫秒一次是常态。你想想看,如果每次数据回调你都去干点重活——比如写数据库、解析JSON、甚至做网络请求——那UI线程直接就卡死了。

我早年做过一个手机防误触功能,就是距离传感器靠近时灭屏。当时图省事,直接在onSensorChanged里调了PowerManager.goToSleep()。结果呢?滑动解锁时偶尔屏幕闪一下,用户反馈说「手机抽风了」。排查半天,原来是传感器回调里做了太多事,导致UI线程被阻塞,触摸事件响应延迟。

嗯,这节课我们就来彻底解决这个问题。

18.1 onSensorChanged的调用频率有多恐怖?

先看一组数据。我实测过几款主流机型,距离传感器的典型上报频率如下:

传感器类型 典型频率 最坏情况
距离传感器(接近) 50~100ms一次 10ms一次
距离传感器(远距离) 200~500ms一次 50ms一次

也就是说,每秒可能回调10到100次。如果你在回调里做一次耗时10ms的操作,那CPU就满负荷了。更可怕的是,onSensorChanged默认跑在主线程上。

⚠️ 警告:主线程一旦被传感器回调阻塞,你的App就会出现掉帧、触摸延迟、甚至ANR。用户的第一反应就是「这手机真卡」。

18.2 哪些操作算「耗时操作」?

说白了,只要不是简单的变量赋值或状态判断,都算耗时。我列个清单:

  • 文件I/O:写日志、保存配置
  • 数据库操作:insert、update、query
  • 网络请求:哪怕只是ping一下
  • 复杂计算:比如浮点运算、矩阵变换
  • 对象创建:频繁new对象会触发GC
  • 同步锁:等待其他线程释放锁

我曾经见过一个案例:有人在onSensorChanged里调了Log.d(),觉得就一行日志能有多大事?结果Log系统内部有锁竞争,高频率调用下直接导致UI线程卡顿。嗯,连日志都不能随便打

18.3 核心方案:把数据扔到HandlerThread里处理

解决方案其实很简单:别在主线程干活,开个后台线程专门处理传感器数据。Android里最优雅的方式就是HandlerThread

HandlerThread是什么?它就是一个自带Looper的线程。你可以往它里面post任务,任务会排队依次执行,不会阻塞主线程。

核心思路:onSensorChanged里只做一件事——把原始数据通过Handler发送到HandlerThread。所有耗时操作都在HandlerThread里完成。

来看代码:

public class SensorHandlerThread extends HandlerThread {
    private Handler mWorkerHandler;
    private SensorManager mSensorManager;

    public SensorHandlerThread(String name, SensorManager sensorManager) {
        super(name);
        mSensorManager = sensorManager;
    }

    @Override
    protected void onLooperPrepared() {
        super.onLooperPrepared();
        // 这个Handler运行在当前线程(后台线程)的Looper上
        mWorkerHandler = new Handler(getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                // 在这里处理传感器数据,随便干重活
                SensorEvent event = (SensorEvent) msg.obj;
                processSensorData(event);
            }
        };
    }

    public Handler getWorkerHandler() {
        return mWorkerHandler;
    }

    private void processSensorData(SensorEvent event) {
        // 耗时操作:写日志、更新数据库、做算法
        float distance = event.values[0];
        // 模拟耗时操作
        SystemClock.sleep(5); // 5ms
        Log.d("Sensor", "distance: " + distance);
    }
}

然后在Activity里这样用:

public class MainActivity extends AppCompatActivity implements SensorEventListener {
    private SensorManager mSensorManager;
    private Sensor mProximitySensor;
    private SensorHandlerThread mSensorThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
        mProximitySensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);

        // 启动后台线程
        mSensorThread = new SensorHandlerThread("SensorThread", mSensorManager);
        mSensorThread.start();
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        // 这里只做一件事:把数据发到后台线程
        Message msg = Message.obtain();
        msg.obj = event;
        mSensorThread.getWorkerHandler().sendMessage(msg);
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // 精度变化也可以扔到后台处理
    }

    @Override
    protected void onResume() {
        super.onResume();
        mSensorManager.registerListener(this, mProximitySensor,
                SensorManager.SENSOR_DELAY_NORMAL);
    }

    @Override
    protected void onPause() {
        super.onPause();
        mSensorManager.unregisterListener(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mSensorThread != null) {
            mSensorThread.quitSafely();
        }
    }
}
💡 小技巧:使用Message.obtain()而不是new Message(),可以复用消息对象,减少内存分配。传感器数据量大时,这个优化很有用。

18.4 避坑指南:HandlerThread的常见问题

用HandlerThread也不是万能的。我踩过几个坑,分享给你:

  • 忘记quit:Activity销毁时一定要调quitSafely(),否则线程永远不结束,造成内存泄漏。
  • 消息堆积:如果传感器频率太高,而后台处理太慢,消息队列会越积越多。我建议在handleMessage里加个判断:如果队列长度超过某个阈值,就丢弃旧消息。
  • 线程安全:HandlerThread内部是串行执行的,但如果你在外部直接访问它的成员变量,记得加锁。
⚠️ 我曾经犯过的错:onSensorChanged里直接调了mWorkerHandler.sendMessage(),但忘了检查mWorkerHandler是否为null。因为HandlerThread的onLooperPrepared()是异步回调的,如果传感器在Handler初始化之前就注册了,就会空指针。解决方案:在onLooperPrepared()里再注册传感器监听。

18.5 进阶:使用协程或RxJava处理传感器数据

如果你项目里用了Kotlin协程或RxJava,那处理方式更优雅。这里简单提一下协程版本:

// 使用协程的Dispatchers.IO来处理
lifecycleScope.launch(Dispatchers.IO) {
    // 在这里处理传感器数据
    withContext(Dispatchers.Main) {
        // 更新UI
    }
}

但要注意:协程的调度也是有开销的。如果传感器频率极高(比如10ms一次),频繁切换协程反而可能得不偿失。这时候HandlerThread反而是最轻量的方案。

18.6 知识体系总结

我把这节课的核心逻辑画了张图,方便你理解:

传感器数据处理架构 距离传感器硬件 SensorManager onSensorChanged (主线程) ❌ 禁止耗时操作 sendMessage() HandlerThread(后台线程) 消息队列排队执行 ✅ 写日志 | 更新数据库 | 算法计算 | 网络请求 所有耗时操作都在这里完成 主线程只做数据转发,不做任何耗时操作

这张图很清楚地展示了:传感器硬件 → SensorManager → onSensorChanged(主线程) → HandlerThread(后台线程) 这条链路。主线程只负责转发数据,真正的脏活累活全在后台线程里干。

18.7 性能对比:优化前后

我拿一台老机型(骁龙660)做了个简单测试,结果如下:

场景 主线程卡顿次数(1分钟内) UI帧率 ANR风险
优化前(直接在onSensorChanged里写日志+数据库) 47次 15~25fps
优化后(使用HandlerThread转发) 0次 55~60fps 极低

差距很明显吧?说白了,把传感器数据从主线程剥离出去,是性能优化的第一步,也是最关键的一步

💡 我的个人习惯:只要涉及传感器,我上来就先建一个HandlerThread。哪怕当前需求很简单,也先搭好架子。因为产品经理随时可能加需求——「加个统计埋点」「加个防误触算法」——到时候再改就晚了。

好了,这节课的核心就这些。记住一句话:onSensorChanged里只做一件事——把数据扔出去。剩下的,交给HandlerThread去处理。