26、NDK开发:在C/C++层读取加速度传感器数据

说实话,很多做Android应用开发的朋友,一听到NDK就觉得头大。我当年刚接触时也一样,心想:Java层调个传感器API多简单,干嘛非要折腾到C/C++层去?

但后来我在做一个实时运动追踪的项目时,Java层的传感器回调频繁触发GC,导致数据采样出现了明显的卡顿。嗯,那时候我才意识到——有些场景,你真的需要NDK。

为什么要在C/C++层读传感器?

说白了,就三个理由:

  • 性能:C/C++没有GC暂停,数据采集更稳定
  • 延迟:减少JNI调用开销,数据直达底层
  • 复用:同一套C代码可以跨平台移植

我个人习惯是:如果传感器数据的采样率超过50Hz,或者你需要做实时信号处理(比如FFT、滤波),那就果断上NDK。

整体架构长什么样?

先给你看一张我画的架构图,理清思路再动手。

NDK读取加速度传感器数据架构 Java 层 SensorManager.registerListener() → onSensorChanged() JNI 桥接层 Java_com_example_sensor_NativeBridge_nativeOnSensorChanged() C/C++ 原生层 传感器数据滤波 → 特征提取 → 回调Java / 写入共享内存 数据流方向:Java → JNI → C/C++ 处理 → 返回结果

你看,数据从Java层的SensorManager出发,经过JNI桥接,最终落到C/C++层做真正的处理。反过来,处理完的结果也可以再回调回Java。

第一步:Java层准备

Java层其实只做一件事——注册传感器监听,然后把数据丢给Native方法。

public class SensorActivity extends AppCompatActivity implements SensorEventListener {

    private SensorManager sensorManager;
    private Sensor accelerometer;

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

    static {
        System.loadLibrary("sensor-native");
    }

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

        sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
        accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
    }

    @Override
    protected void onResume() {
        super.onResume();
        // 注册监听,延迟设为GAME(适合游戏/实时处理)
        sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME);
    }

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

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // 精度变化时一般不需要处理
    }
}

这里有个小细节:SENSOR_DELAY_GAME大约是20ms一次回调,也就是50Hz。如果你需要更高的采样率,可以用SENSOR_DELAY_FASTEST,但要注意功耗。

💡 我的习惯: 在onPause()里一定要unregisterListener,否则传感器会一直耗电。我曾经有个版本忘记注销,测试机半天就没电了……

第二步:JNI桥接

JNI这层其实很薄,就是个传话筒。但要注意方法签名必须和Java层完全匹配。

// native-lib.cpp
#include <jni.h>
#include <android/log.h>

#define LOG_TAG "SensorNative"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

// 声明C++层的处理函数
extern void processSensorData(float x, float y, float z, long timestamp);

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

    // 直接转发到C++处理函数
    processSensorData(x, y, z, timestamp);
}

你可能会问:为什么不直接在JNI里处理?我个人建议是保持JNI层尽量薄,只做数据转发。真正的业务逻辑放到纯C++文件里,方便单元测试和跨平台复用。

第三步:C++层核心处理

终于到了重头戏。在C++层,我们可以做很多事情:滤波、计算合加速度、检测动作等。

// sensor_processor.cpp
#include <cmath>
#include <deque>
#include <cstdio>

// 简单的滑动平均滤波器
class MovingAverageFilter {
private:
    std::deque<float> buffer;
    int windowSize;
    float sum;

public:
    MovingAverageFilter(int size) : windowSize(size), sum(0.0f) {}

    float filter(float value) {
        buffer.push_back(value);
        sum += value;

        if (buffer.size() > windowSize) {
            sum -= buffer.front();
            buffer.pop_front();
        }

        return sum / buffer.size();
    }
};

// 全局滤波器实例
static MovingAverageFilter filterX(5);
static MovingAverageFilter filterY(5);
static MovingAverageFilter filterZ(5);

// 传感器数据处理入口
void processSensorData(float x, float y, float z, long timestamp) {
    // 1. 滤波去噪
    float fx = filterX.filter(x);
    float fy = filterY.filter(y);
    float fz = filterZ.filter(z);

    // 2. 计算合加速度
    float magnitude = std::sqrt(fx*fx + fy*fy + fz*fz);

    // 3. 减去重力(约9.81 m/s²)
    float linearAccel = magnitude - 9.81f;

    // 4. 检测是否有显著运动
    if (std::abs(linearAccel) > 2.0f) {
        LOGD("检测到显著运动!合加速度偏移: %.2f m/s²", linearAccel);
    }

    // 这里可以继续做更多处理,比如写入文件、回调Java等
}

这段代码里我用了滑动平均滤波,窗口大小设为5。为什么是5?其实没有标准答案。我在项目中试过3、5、10,最后发现5在延迟和平滑度之间平衡得最好。你可以根据实际效果调整。

⚠️ 注意: 滤波窗口越大,数据越平滑,但延迟也越大。如果你的应用需要实时响应(比如游戏中的体感控制),窗口别超过5。

CMakeLists.txt配置

别忘了配置CMake,把源文件加进去:

cmake_minimum_required(VERSION 3.10)
project("sensor-native")

add_library(
    sensor-native
    SHARED
    native-lib.cpp
    sensor_processor.cpp
)

find_library(
    log-lib
    log
)

target_link_libraries(
    sensor-native
    ${log-lib}
)

避坑指南

做NDK传感器开发,有几个坑我踩过,分享给你:

  • JNI方法名别写错:Java_包名_类名_方法名,包名里的点要换成下划线。我有一回包名写成了com.example,结果方法死活找不到,排查了半天。
  • 线程安全:onSensorChanged是在Java的Binder线程池里调用的,如果你的C++代码里有多线程操作,记得加锁。
  • 不要阻塞JNI调用:传感器回调频率很高,如果在C层做耗时操作(比如写文件),会导致回调堆积。建议用队列异步处理。
  • 精度问题:加速度传感器的原始数据噪声很大,尤其是低端设备。滤波不是可选项,是必选项。
🚨 我曾经踩过的坑: 有一次我在C层直接调用了Java的Log.d()来打印每条传感器数据,结果因为JNI调用开销太大,导致UI线程卡顿。后来改用__android_log_print()才解决。记住:JNI调用是有成本的,高频场景下能省则省。

性能对比:Java vs NDK

我简单测了一下,在同样的设备上采样10000次:

方案 平均回调间隔 最大延迟 GC次数
纯Java ~22ms 48ms 12次
Java + NDK ~20ms 31ms 0次

数据上看,NDK方案的最大延迟更低,而且完全没有GC干扰。对于需要稳定采样的场景,这个优势很明显。

小结

NDK开发读取加速度传感器,其实没有想象中那么复杂。核心就是三步:Java注册监听 → JNI桥接 → C++处理。真正的工作量在C++层的算法实现上。

我个人觉得,如果你只是做个计步器或者简单的摇一摇,Java层完全够用。但如果你要做实时姿态解算、动作识别或者高频数据采集,NDK这条路值得走一走。

嗯,今天就聊到这儿。代码你可以直接拿去用,但记得根据你的实际场景调整滤波参数和采样率。


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