15、传感器测试工具开发:自己写一个SensorTest App,实时显示所有传感器数据。

好,到了这一章,我们终于要动手写点真正能跑的东西了。

前面讲了那么多距离传感器的原理、校准、调试,但说实话,你总不能每次测试都连adb敲命令吧?我个人的习惯是,手边必须有一个趁手的传感器调试工具。今天我们就自己写一个——SensorTest App,实时显示所有传感器数据。

15.1 为什么非要自己写?

你可能会问:Google Play上不是有Sensor Test、Sensor Box之类的现成App吗?

嗯,确实有。但我遇到过好几次坑——有些App只显示加速度计和陀螺仪,距离传感器和光线传感器的数据根本不展示。还有的App刷新率太低,根本抓不到距离传感器的近/远跳变。

说白了,自己写的工具,你最清楚它怎么工作。想加什么字段就加什么字段,想调采样率就调采样率。而且,你还能把它集成到你的自动化测试框架里。

15.2 核心思路:监听所有传感器

Android的传感器框架其实很简单。核心就三个步骤:

  1. 拿到 SensorManager
  2. 遍历 getSensorList(Sensor.TYPE_ALL)
  3. 为每个传感器注册 SensorEventListener

听起来简单?但这里有个坑——不是所有传感器都能同时监听。我曾经在项目里一次性注册了十几个传感器,结果发现某些低端手机直接卡死,甚至崩溃。为什么?因为有些芯片的传感器总线是共享的,同时高频读取会导致冲突。

避坑指南: 我曾经在MTK平台上遇到过,同时监听距离传感器和光线传感器,结果光线数据一直返回-1。后来发现是这两个传感器共用同一个物理通道,需要分时读取。所以,建议每个传感器单独注册,或者至少把冲突的传感器分开处理

15.3 架构设计:一个轻量级的MVP

我们不需要搞太复杂的架构。但也不能把代码全塞进Activity里。我个人习惯用这种分层:

  • Model层:传感器数据实体类,包含传感器名称、类型、数值、精度、时间戳
  • Presenter层:管理传感器注册、注销、数据回调
  • View层:RecyclerView展示所有传感器列表,每个item显示实时数据

下面这张图可以帮你快速理解整体流程:

SensorTest App 架构图 用户界面 (Activity/Fragment) SensorPresenter (注册/注销管理) SensorManager 物理传感器 (距离/光线/加速度/陀螺仪...)

15.4 关键代码实现

我们直接看核心部分。首先是传感器数据实体类:

public class SensorData {
    public String sensorName;      // 传感器名称
    public int sensorType;         // 传感器类型
    public float[] values;         // 数据值(不同传感器长度不同)
    public float accuracy;         // 精度
    public long timestamp;         // 时间戳(纳秒)
    public boolean isRegistered;   // 是否已注册监听
}

然后是Presenter的核心逻辑。这里我特别强调一点:注册传感器时一定要指定采样率。很多人直接用 SENSOR_DELAY_NORMAL,结果距离传感器的响应慢得像蜗牛。

public class SensorPresenter implements SensorEventListener {
    private SensorManager sensorManager;
    private List<Sensor> sensorList;
    private Map<Integer, SensorData> dataMap = new HashMap<>();

    public void init(Context context) {
        sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
        sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL);
        
        // 遍历所有传感器,逐个注册
        for (Sensor sensor : sensorList) {
            // 距离传感器建议用 SENSOR_DELAY_FASTEST 或 UI
            int delay = (sensor.getType() == Sensor.TYPE_PROXIMITY) 
                        ? SensorManager.SENSOR_DELAY_UI 
                        : SensorManager.SENSOR_DELAY_NORMAL;
            sensorManager.registerListener(this, sensor, delay);
            
            SensorData data = new SensorData();
            data.sensorName = sensor.getName();
            data.sensorType = sensor.getType();
            data.isRegistered = true;
            dataMap.put(sensor.getType(), data);
        }
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        SensorData data = dataMap.get(event.sensor.getType());
        if (data != null) {
            data.values = event.values.clone();
            data.accuracy = event.accuracy;
            data.timestamp = event.timestamp;
            // 通知UI更新
            notifyDataChanged(data);
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        SensorData data = dataMap.get(sensor.getType());
        if (data != null) {
            data.accuracy = accuracy;
        }
    }

    public void release() {
        if (sensorManager != null) {
            sensorManager.unregisterListener(this);
        }
    }
}
个人经验: 我在调试一款折叠屏手机时发现,展开状态下距离传感器会自动禁用。后来查了源码才知道,某些厂商会在屏幕状态变化时动态开关传感器。所以,建议在onResume中注册,onPause中注销,避免漏掉状态变化。

15.5 UI展示:实时刷新列表

UI部分我用RecyclerView来实现。每个item显示传感器名称、类型、数值、精度。这里有个小技巧——用DiffUtil来更新列表,避免整个列表闪烁。

public class SensorAdapter extends RecyclerView.Adapter<SensorAdapter.ViewHolder> {
    private List<SensorData> dataList;

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        SensorData data = dataList.get(position);
        holder.nameText.setText(data.sensorName);
        
        // 距离传感器只显示第一个值(厘米)
        if (data.sensorType == Sensor.TYPE_PROXIMITY) {
            holder.valueText.setText(String.format("%.1f cm", data.values[0]));
        } else {
            // 其他传感器显示所有值
            StringBuilder sb = new StringBuilder();
            for (float v : data.values) {
                sb.append(String.format("%.2f, ", v));
            }
            holder.valueText.setText(sb.toString());
        }
        
        // 精度显示:0=不可靠, 1=低, 2=中, 3=高
        String[] accLabels = {"不可靠", "低", "中", "高"};
        int accIndex = (int) data.accuracy;
        if (accIndex >= 0 && accIndex < accLabels.length) {
            holder.accText.setText("精度: " + accLabels[accIndex]);
        }
    }
}

15.6 实测效果与调试技巧

写完之后,我建议你拿真机跑一下。重点关注这几点:

传感器类型 预期行为 常见问题
距离传感器 遮挡时显示0cm或1cm,远离时显示5cm或更大 部分手机只有0和1两个值,属于正常
光线传感器 手电筒照射时数值飙升,遮挡时接近0 数值范围因厂商而异,不必纠结绝对值
加速度计 平放时Z轴约9.8,翻转时变化 静止时数值有微小波动,属于正常噪声
陀螺仪 静止时接近0,旋转时有数值 部分低端手机陀螺仪漂移严重
核心要点: 距离传感器的数据变化是离散的,不是连续的。如果你看到数值在0.5、0.8、1.2之间跳变,那说明你的传感器是模拟输出的。但大多数手机只有0和最大值两个状态。不要惊讶,这是正常的。

15.7 性能优化:别让App卡死

嗯,这里要特别提醒一下。如果你同时注册了十几个传感器,并且都用 SENSOR_DELAY_FASTEST,那你的UI线程可能会被回调淹没。我遇到过最夸张的情况——每秒收到上千次回调,RecyclerView根本来不及刷新,直接ANR。

解决方案有两个:

  • 限流:在Presenter层加一个时间过滤器,比如每100ms只通知UI一次
  • 异步刷新:用 HandlerThread 处理传感器回调,只在主线程做UI更新
// 限流示例:每100ms刷新一次UI
private long lastUpdateTime = 0;
private static final long UPDATE_INTERVAL = 100; // 毫秒

@Override
public void onSensorChanged(SensorEvent event) {
    long now = System.currentTimeMillis();
    if (now - lastUpdateTime < UPDATE_INTERVAL) {
        return; // 跳过这次更新
    }
    lastUpdateTime = now;
    // 继续处理数据...
}

15.8 扩展思路:还能怎么玩?

这个SensorTest App写完之后,你可以继续加功能:

  • 数据记录:把传感器数据保存到CSV文件,方便后续分析
  • 波形图:用Canvas或者MPAndroidChart画实时曲线
  • 阈值报警:比如距离传感器小于某个值时,触发震动或声音
  • 多设备对比:同时连接多台手机,对比同一传感器的数据差异

我个人最喜欢的是波形图功能。有一次在调试一款手机的防误触功能时,我就是靠这个App的波形图,发现距离传感器的响应延迟高达200ms——这显然是不合格的。后来找驱动同事一查,果然是I2C总线时钟配置错了。

你看,自己写工具的好处就在这里——你能看到系统层看不到的细节

最后一个小建议: 把这个App装到你的测试机上,平时没事就打开看看。你会发现很多有意思的现象,比如某些App在后台偷偷注册了传感器,或者某些传感器的精度会随着温度变化。这些经验,都是书本上学不到的。

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