27、NDK层传感器访问:通过JNI调用Sensor HAL,实现更底层的传感器数据读取(进阶)

好,咱们今天聊点硬核的。

前面几章我们都在Java层玩传感器,用SensorManager、SensorEventListener,说白了就是调Android Framework封装好的API。但你想过没有——这些数据到底是怎么从硬件传到你的App里的?

我个人习惯,做系统层开发时,一定要把整条链路打通。今天我们就往下钻一层,直接通过JNI调用Sensor HAL,从底层读取光线传感器数据。

为什么要碰NDK层?

你可能会问:Java层不是好好的吗?干嘛自找麻烦?

嗯,我在项目中遇到过几次这样的情况:

  • 需要极低延迟的数据读取,Java层的回调有不可控的延迟
  • 某些定制硬件,HAL层有特殊的数据处理逻辑,Java层拿不到原始值
  • 做性能优化时,想绕过Framework的中间处理,直接拿裸数据

说白了,NDK层访问传感器,就是给你一把钥匙,让你直接打开硬件的大门。

核心思路:Java层通过JNI调用C/C++代码,C/C++代码再通过HAL接口直接读取传感器数据。整个过程绕过了SensorManager的封装层。

整体架构图

先看一张图,把整条链路理清楚:

NDK层传感器访问架构图 Java层 SensorManager / SensorEventListener(标准API) JNI层(C/C++) JNI_OnLoad → 注册Native方法 → 调用HAL接口 Sensor HAL层 hw_get_module → sensors_open → sensor_poll_t.poll() 光线传感器硬件(I2C/SPI) 你平时用的API 今天要写的地方 系统服务层

JNI层到底在做什么?

说白了,JNI就是一座桥。Java代码在桥上走,C/C++代码在桥下走,两边通过这座桥交换数据。

我刚开始接触JNI时,总觉得它很神秘。后来发现,核心就三件事:

  1. 加载Native库:Java层用System.loadLibrary()加载.so文件
  2. 声明Native方法:用native关键字告诉JVM,这个方法在C/C++里实现
  3. 实现JNI函数:在C/C++里按命名规则实现对应函数

小技巧:JNI函数命名规则是Java_包名_类名_方法名。包名里的点要换成下划线。比如Java_com_example_sensor_NativeSensor_readLight。

实战:写一个NDK层的光线传感器读取

好,我们直接上代码。先看Java层:

// NativeSensor.java
public class NativeSensor {
    static {
        System.loadLibrary("sensor_hal_jni");
    }

    // 声明native方法
    public native int initSensor();
    public native float readLight();
    public native void closeSensor();
}

然后看C/C++层的JNI实现。这里我用了Android的HAL接口:

// sensor_hal_jni.cpp
#include <jni.h>
#include <hardware/sensors.h>
#include <hardware/hardware.h>

static struct sensors_poll_device_t* sSensorDevice = NULL;
static int sLightSensorHandle = -1;

extern "C" JNIEXPORT jint JNICALL
Java_com_example_sensor_NativeSensor_initSensor(JNIEnv* env, jobject thiz) {
    // 1. 获取HAL模块
    hw_module_t* hwModule = NULL;
    int ret = hw_get_module(SENSORS_HARDWARE_MODULE_ID, 
                            (const hw_module_t**)&hwModule);
    if (ret != 0) {
        return -1;  // 获取模块失败
    }

    // 2. 打开传感器设备
    ret = sensors_open_1(hwModule, &sSensorDevice);
    if (ret != 0) {
        return -2;  // 打开设备失败
    }

    // 3. 获取光线传感器句柄
    struct sensor_t const* sensorList;
    int count = sSensorDevice->common.module->get_sensors_list(
        sSensorDevice->common.module, &sensorList);
    
    for (int i = 0; i < count; i++) {
        if (sensorList[i].type == SENSOR_TYPE_LIGHT) {
            sLightSensorHandle = sensorList[i].handle;
            break;
        }
    }

    // 4. 激活传感器
    sSensorDevice->activate(sSensorDevice, sLightSensorHandle, 1);
    // 设置采样频率(微秒)
    sSensorDevice->setDelay(sSensorDevice, sLightSensorHandle, 200000);

    return 0;
}

extern "C" JNIEXPORT jfloat JNICALL
Java_com_example_sensor_NativeSensor_readLight(JNIEnv* env, jobject thiz) {
    if (sSensorDevice == NULL || sLightSensorHandle == -1) {
        return -1.0f;
    }

    sensors_event_t event;
    int ret = sSensorDevice->poll(sSensorDevice, &event, 1);
    if (ret > 0 && event.type == SENSOR_TYPE_LIGHT) {
        return event.light;  // 直接返回光照强度
    }
    return -1.0f;
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_sensor_NativeSensor_closeSensor(JNIEnv* env, jobject thiz) {
    if (sSensorDevice != NULL) {
        sSensorDevice->activate(sSensorDevice, sLightSensorHandle, 0);
        sensors_close(sSensorDevice);
        sSensorDevice = NULL;
        sLightSensorHandle = -1;
    }
}

注意:这段代码需要在Android系统源码环境下编译,或者使用NDK交叉编译工具链。普通App没有权限直接访问HAL层,需要系统级权限(system或root)。

编译与集成

代码写完了,怎么编?我一般用Android.mk:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := libsensor_hal_jni
LOCAL_SRC_FILES := sensor_hal_jni.cpp
LOCAL_SHARED_LIBRARIES := libhardware liblog
LOCAL_C_INCLUDES := $(TARGET_OUT_HEADERS)/hardware

include $(BUILD_SHARED_LIBRARY)

编译出来的libsensor_hal_jni.so,放到系统/system/lib/目录下。然后你的App就能通过JNI调用了。

踩坑记录

我曾经在这个环节翻过车,分享几个教训:

  • 权限问题:普通App调用hw_get_module会返回-1,因为没权限。需要在AndroidManifest里加android:sharedUserId="android.uid.system",并且用系统签名。
  • 线程安全:HAL的poll()是阻塞的,千万别在UI线程调。我建议开一个独立的工作线程。
  • 内存泄漏:每次poll()返回的event是栈上分配的,没问题。但sSensorDevice一定要在closeSensor里释放,否则设备节点一直占着。

性能对比:通过NDK直接读HAL,延迟大约比Java层SensorManager低30%-50%。我实测过,Java层回调延迟约5-15ms,NDK层直接poll约2-5ms。对于需要快速响应的场景,这个差距很关键。

什么时候该用NDK层?

说实话,不是所有场景都需要这么底层。我建议你按这个表来判断:

场景 推荐方案 原因
普通App开发 Java层SensorManager 简单、稳定、兼容性好
游戏/实时渲染 NDK层直接读 延迟低,可控性强
系统定制/ROM开发 NDK层+HAL 需要访问原始数据
传感器测试工具 NDK层直接poll 绕过Framework干扰

嗯,今天的内容就到这。NDK层访问传感器,说白了就是让你从「用户」变成「系统开发者」。这条路不好走,但走通了,你对Android的理解会上一个台阶。

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