做传感器相关项目时,磁力计(Magnetometer)是手机上最容易被忽视但用处很大的硬件之一。它可以测量设备周围磁场的三个轴向分量,配合加速度计就能算出设备的朝向角度,也可以用来检测周围是否存在异常的磁性物体。不过传感器数据是源源不断产生的,每秒可能触发几十次回调,如果只是打印在日志里,事后想分析就无从下手了。这篇文章就带大家完成一个完整的小项目:用SQLite把磁力计的实时读数持久化存储下来,再做简单的查询和统计分析。

一、项目整体设计与数据库表结构
这个项目的思路很直接:Android端注册磁力计监听器,每当传感器回调触发,就把当前时间戳和三个轴向的磁场强度写入SQLite。为了方便后续分析,建表时要提前想清楚字段。磁力计返回的是x、y、z三个方向的磁场值,单位是微特斯拉(μT),另外强烈建议存一个合成的总磁场强度,也就是三个分量的平方和再开根号,这样查询峰值时不用每次都重新计算。
下面是建表语句,用SQL直接表达:
CREATE TABLE IF NOT EXISTS magnetic_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp INTEGER NOT NULL, -- 毫秒级时间戳
field_x REAL NOT NULL, -- X轴磁场强度,单位μT
field_y REAL NOT NULL, -- Y轴磁场强度
field_z REAL NOT NULL, -- Z轴磁场强度
magnitude REAL NOT NULL, -- 合成总磁场强度
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);有几个设计细节值得说一说。第一,时间戳字段用INTEGER存毫秒值而不是DATETIME字符串,比较和排序效率更高,展示时再格式化即可。第二,magnitude字段看起来是冗余的,因为理论上可以从三个分量推算出来,但把它落库之后,查询最大磁场、绘制趋势图时可以直接对这一列建索引,性能提升非常明显。第三,id用自增主键,SQLite中 INTEGER PRIMARY KEY 本质上就是 rowid,不需要额外占用存储空间。
建索引的语句也别忘了:
CREATE INDEX IF NOT EXISTS idx_mag_time ON magnetic_data(timestamp); CREATE INDEX IF NOT EXISTS idx_mag_value ON magnetic_data(magnitude);
二、采集磁力计数据并写入SQLite
Android中获取磁力计数据需要通过SensorManager注册监听器。这里有一个很多人会踩的坑:传感器回调默认运行在UI线程,如果直接在回调里执行数据库插入操作,磁场传感器采样率一旦设高,UI就会明显卡顿。正确的做法是引入一个单线程的HandlerThread,把监听器注册到这个后台线程的消息队列上,这样采集和写库都在后台完成。
来看核心代码:
private SensorManager sensorManager;
private Sensor magnetometer;
private SQLiteDatabase db;
private HandlerThread sensorThread;
public void startRecording(Context context) {
db = new MagneticDbHelper(context).getWritableDatabase();
sensorThread = new HandlerThread("sensor-thread");
sensorThread.start();
Handler handler = new Handler(sensorThread.getLooper());
sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
sensorManager.registerListener(sensorListener, magnetometer,
SensorManager.SENSOR_DELAY_UI, handler);
}
private final SensorEventListener sensorListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
float x = event.values[0];
float y = event.values[1];
float z = event.values[2];
double magnitude = Math.sqrt(x * x + y * y + z * z);
insertRecord(event.timestamp, x, y, z, magnitude);
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// 精度变化时可记录日志,此处从略
}
};采样率的选择也有讲究。SENSOR_DELAY_FASTEST会让回调频率高达每秒上百次,对数据库压力很大,而且相邻数据差别极小,实际分析价值有限。对于磁场监测这类场景,SENSOR_DELAY_UI(大约每秒60次)或者干脆自己定时抽取,通常已经够用了。如果只想每秒存一条,可以在回调里判断与上次入库时间的间隔,不足一秒就直接返回。
三、批量插入优化:性能差距可能超乎想象
磁力计数据是高频流式数据,如果每来一条就执行一次insert,每条插入都会隐式开启一个事务,磁盘IO开销非常大。我在一台中端手机上做过简单测试:逐条插入一万条记录大约需要十几秒,而用事务批量提交同样的数据,耗时不到一秒,差距在一个数量级以上。
推荐的做法是先把数据攒在内存列表里,达到一定数量或者间隔一定时间后再统一写入。具体实现:
private List<ContentValues> buffer = new ArrayList<>();
private static final int BATCH_SIZE = 200;
private void insertRecord(long timestamp, float x, float y, float z, double mag) {
ContentValues cv = new ContentValues();
cv.put("timestamp", timestamp);
cv.put("field_x", x);
cv.put("field_y", y);
cv.put("field_z", z);
cv.put("magnitude", mag);
buffer.add(cv);
if (buffer.size() >= BATCH_SIZE) {
flushBuffer();
}
}
private void flushBuffer() {
if (buffer.isEmpty()) return;
db.beginTransaction();
try {
for (ContentValues cv : buffer) {
db.insert("magnetic_data", null, cv);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
buffer.clear();
}注意setTransactionSuccessful必须放在endTransaction之前的try块内调用,且只在全部插入成功后调用,否则整个事务会回滚。另外,停止采集时一定要记得调用一次flushBuffer,否则缓冲区里最后不足一批的数据就丢了。除了事务之外,如果数据量确实巨大,还可以考虑定期归档:把超过一定天数的旧数据压缩成每分钟一个均值单独存表,原始明细数据定期清理,这样数据库文件不会无限膨胀。
四、数据查询与磁场异常分析
数据存下来之后,分析就简单了。地球磁场的自然强度大约在25到65微特斯拉之间,如果某个时刻的magnitude明显超出这个范围,通常意味着附近有磁铁、电器或者金属结构干扰。利用SQL的聚合函数可以很快找出异常时段:
-- 查询磁场强度最高的前10条记录
SELECT timestamp, magnitude FROM magnetic_data
ORDER BY magnitude DESC LIMIT 10;
-- 按分钟统计平均磁场强度,用于绘制趋势图
SELECT timestamp / 60000 AS minute, AVG(magnitude) AS avg_mag
FROM magnetic_data
GROUP BY minute ORDER BY minute;
-- 找出磁场超过80μT的异常时间段
SELECT MIN(timestamp) AS start_time, MAX(timestamp) AS end_time,
MAX(magnitude) AS peak_value
FROM (
SELECT *, timestamp / 10000 AS seg
FROM magnetic_data WHERE magnitude > 80
) GROUP BY seg;第二条按分钟聚合的查询在数据量上来之后可能会变慢,因为对timestamp / 60000做运算无法直接利用普通索引。解决办法是加一列冗余的minute_slot字段,写入时就算好分钟序号并对它建索引,查询时按等值分组,速度会快很多。这也是流式数据入库的一个通用技巧:宁可多存一点冗余字段,也要给分析查询让路。
最后再提两点容易忽略的地方。一是数据库文件默认放在应用私有目录,卸载应用数据就没了,如果数据重要,要定期导出到外部存储或上传服务器。二是在onPause或服务销毁时务必调用sensorManager.unregisterListener并关闭数据库,否则传感器持续唤醒会明显耗电。把这个小项目跑通之后,你可以很方便地扩展出环境监测、金属探测、手势识别等多种玩法,SQLite这套存储框架都能直接复用。