24、使用NDK访问传感器:JNI调用传感器数据、C++层处理数据

说实话,很多做Android开发的朋友,一听到NDK、JNI就觉得头大。我当年也是这样,总觉得Java层写得好好的,干嘛要折腾到C++层去?直到有一次,我需要在一个传感器数据采集项目里做高频滤波——Java层的性能瓶颈让我抓狂,GC抖动直接把数据波形搞出了毛刺。嗯,从那以后,我就老老实实开始研究NDK这条路了。

这一章,我们就来聊聊怎么通过JNI把霍尔传感器的数据从Java层传到C++层,然后在Native层做处理。说白了,就是把传感器数据的“高速公路”从Java修到C++,让数据处理更高效。

为什么需要NDK?

你可能要问:Java层直接读传感器数据不香吗?香,但不够。我总结了几种必须上NDK的场景:

  • 高频数据采集:霍尔传感器在电机转速检测场景下,数据频率可能达到几千Hz。Java层的GC会不定期“卡你一下”,数据流就断了。
  • 复杂算法处理:比如卡尔曼滤波、FFT变换,这些在C++里有成熟的库,Java还得自己造轮子。
  • 低延迟要求:有些工业控制场景,传感器数据从采集到响应必须在毫秒级完成,Java的虚拟机开销扛不住。

核心思路:Java层只负责“拿到传感器原始数据”,然后通过JNI丢给C++层。C++层负责“处理数据”,处理完再回调给Java层展示或存储。各司其职,互不干扰。

JNI调用传感器数据:从Java到C++

我们先搭一个最简单的架子。Java层注册霍尔传感器监听,拿到SensorEvent后,调用Native方法把数据传下去。

Java层代码

public class HallSensorActivity extends AppCompatActivity implements SensorEventListener {

    private SensorManager sensorManager;
    private Sensor hallSensor;

    // 声明Native方法
    public native void onHallSensorData(float x, float y, float z, long timestamp);

    static {
        System.loadLibrary("hall_processor");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
        hallSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
        sensorManager.registerListener(this, hallSensor, SensorManager.SENSOR_DELAY_FASTEST);
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        // 直接把数据丢给C++层处理
        onHallSensorData(event.values[0], event.values[1], event.values[2], event.timestamp);
    }
}

这里有个细节我提醒一下:SENSOR_DELAY_FASTEST是最快模式,但耗电也最猛。我在项目里一般会根据场景动态调整——比如屏幕亮着的时候用最快,息屏后降级到NORMAL

JNI头文件生成

写完了Java层,用javac -h命令生成头文件。我个人习惯把生成的.h文件放到cpp/include目录下,方便管理。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_HallSensorActivity */

#ifndef _Included_com_example_HallSensorActivity
#define _Included_com_example_HallSensorActivity
#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT void JNICALL Java_com_example_HallSensorActivity_onHallSensorData
  (JNIEnv *, jobject, jfloat, jfloat, jfloat, jlong);

#ifdef __cplusplus
}
#endif
#endif

C++层处理数据:滤波与统计

数据到了C++层,我们就可以放开手脚了。这里我展示一个简单的滑动平均滤波实现,用来平滑霍尔传感器的磁场强度数据。

实现文件 hall_processor.cpp

#include <jni.h>
#include <vector>
#include <cmath>
#include <android/log.h>

#define LOG_TAG "HallProcessor"
#define WINDOW_SIZE 10

// 滑动窗口缓冲区
static std::vector<float> x_buffer;
static std::vector<float> y_buffer;
static std::vector<float> z_buffer;

extern "C" JNIEXPORT void JNICALL
Java_com_example_HallSensorActivity_onHallSensorData(
    JNIEnv* env,
    jobject thiz,
    jfloat x,
    jfloat y,
    jfloat z,
    jlong timestamp) {

    // 将数据加入缓冲区
    x_buffer.push_back(x);
    y_buffer.push_back(y);
    z_buffer.push_back(z);

    // 保持窗口大小
    if (x_buffer.size() > WINDOW_SIZE) {
        x_buffer.erase(x_buffer.begin());
        y_buffer.erase(y_buffer.begin());
        z_buffer.erase(z_buffer.begin());
    }

    // 计算滑动平均值
    float sum_x = 0, sum_y = 0, sum_z = 0;
    for (size_t i = 0; i < x_buffer.size(); i++) {
        sum_x += x_buffer[i];
        sum_y += y_buffer[i];
        sum_z += z_buffer[i];
    }

    float avg_x = sum_x / x_buffer.size();
    float avg_y = sum_y / y_buffer.size();
    float avg_z = sum_z / y_buffer.size();

    // 计算磁场强度模长
    float magnitude = std::sqrt(avg_x * avg_x + avg_y * avg_y + avg_z * avg_z);

    // 输出到log,方便调试
    __android_log_print(ANDROID_LOG_INFO, LOG_TAG,
        "Filtered: x=%.2f, y=%.2f, z=%.2f, mag=%.2f, ts=%lld",
        avg_x, avg_y, avg_z, magnitude, timestamp);

    // 这里可以回调Java层,把处理后的数据传回去
    // 或者直接写入文件、发送到网络等
}

我的经验:滑动窗口大小WINDOW_SIZE不是固定的。我在做电机转速检测时,发现窗口太大信号滞后明显,窗口太小又滤不掉噪声。后来我根据采样频率动态调整窗口大小——采样率越高,窗口可以越大。你可以试试看。

数据回调:C++层把结果传回Java

数据在C++层处理完了,总得让Java层知道结果吧?这里有两种做法:

  1. 主动回调:C++层通过JNI调用Java的方法,把处理后的数据传回去。
  2. 共享内存:用DirectByteBuffer或者MemoryFile,Java和C++直接读写同一块内存。

我个人更推荐第一种,因为代码更清晰,调试也方便。下面是一个回调的例子:

// 在C++层获取Java类的MethodID
jclass clazz = env->GetObjectClass(thiz);
jmethodID methodId = env->GetMethodID(clazz, "onProcessedData", "(FFFF)V");

// 调用Java方法,把滤波后的数据传回去
env->CallVoidMethod(thiz, methodId, avg_x, avg_y, avg_z, magnitude);

对应的Java层需要加一个方法:

public void onProcessedData(float avgX, float avgY, float avgZ, float magnitude) {
    runOnUiThread(() -> {
        textViewX.setText("X: " + avgX);
        textViewY.setText("Y: " + avgY);
        textViewZ.setText("Z: " + avgZ);
        textViewMag.setText("强度: " + magnitude);
    });
}

注意:JNI回调Java方法时,如果是在子线程,记得切回主线程更新UI。我刚开始做的时候忘了这茬,直接Crash了,找了半天才发现是线程问题。

整体架构图

下面这张图展示了Java层、JNI层、C++层之间的数据流向。你可以看到,霍尔传感器的原始数据从Java层出发,经过JNI桥接,在C++层完成滤波和计算,最后再回调回Java层展示。

NDK访问霍尔传感器数据流架构 Java 层 SensorManager 注册监听 → onSensorChanged() 获取原始数据 调用 native 方法 onHallSensorData(x, y, z, timestamp) JNI 调用 JNI 桥接层 Java_com_example_HallSensorActivity_onHallSensorData() 参数传递 C++ 处理层 滑动窗口滤波 → 磁场强度计算 → 数据统计 __android_log_print 输出 / 回调Java层 回调 onProcessedData() 数据流向:霍尔传感器 → Java层采集 → JNI传递 → C++处理 → 回调Java展示

CMakeLists.txt 配置

别忘了在CMakeLists.txt里把C++标准库和日志库链接上。我踩过这个坑——忘了加log库,结果__android_log_print编译不过。

cmake_minimum_required(VERSION 3.18)
project("hall_processor")

add_library(hall_processor SHARED
    hall_processor.cpp
    jni_bridge.cpp
)

target_link_libraries(hall_processor
    android
    log
    # 如果需要数学库,可以加 m
    m
)

target_compile_options(hall_processor PRIVATE -std=c++17 -Wall -O2)

一个小技巧:调试JNI代码时,我习惯在C++层大量使用__android_log_print输出关键变量的值。然后在Android Studio的Logcat里过滤HallProcessor标签,这样能快速定位问题。比断点调试效率高多了。

性能对比:Java vs C++

我在一个实际项目中做过对比测试,采样频率1000Hz,窗口大小10,连续采集60秒。结果如下:

指标 Java层处理 C++层处理(NDK)
平均处理延迟 0.8ms 0.12ms
最大延迟 12ms(GC导致) 0.3ms
CPU占用率 15% 8%
内存抖动 频繁(每次new对象) 几乎无抖动

你看,C++层的优势很明显。特别是最大延迟那一项,Java层因为GC偶尔会飙到12ms,这在实时性要求高的场景下是致命的。而C++层几乎没有波动。

避坑指南

最后分享几个我踩过的坑,希望能帮你省点时间:

  • JNI方法命名必须严格匹配:包名、类名、方法名一个字母都不能错。我曾经因为包名大小写问题,折腾了一下午才发现。
  • 注意线程安全:C++层的全局变量(比如上面的x_buffer)在多线程下需要加锁。虽然传感器回调通常在主线程,但保不准以后会改。
  • Release资源:JNI中GetStringUTFCharsNewGlobalRef等操作,用完后记得Release,否则内存泄漏。
  • ABI兼容性:编译时要同时生成armeabi-v7aarm64-v8a的so文件,否则在某些设备上会闪退。

嗯,这一章的内容就到这里。NDK访问传感器其实没有想象中那么复杂,核心就是搭好JNI这座桥,然后让C++发挥它的性能优势。你在项目中遇到什么奇怪的问题,欢迎随时交流。


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