20、位置数据存储:SQLite存储位置记录、Room数据库集成、位置历史查询
位置数据拿到手了,总得存下来吧?
说实话,早期我做定位开发时,最头疼的就是数据存储。那时候没有Room,大家清一色用原生SQLite。写SQL语句写到吐,还得自己处理线程安全问题。嗯,今天我们就来聊聊位置数据怎么存、怎么查。
20.1 为什么需要本地存储位置数据
你想想看,用户的位置信息是实时变化的。如果每次都要从网络获取历史轨迹,那体验肯定很差。本地存储有几个好处:
- 离线可用:没网也能查看历史位置
- 节省流量:不用反复请求服务器
- 响应快:本地查询毫秒级返回
- 隐私可控:敏感数据留在本地
我个人习惯把位置数据分为两类:实时轨迹点和签到记录。前者是连续的坐标流,后者是用户主动标记的位置。存储策略上略有不同。
20.2 原生SQLite存储位置记录
先说说最基础的方式。Android自带的SQLite,轻量级,不需要额外依赖。我在早期项目里就用它存过骑行轨迹数据。
20.2.1 创建位置表
public class LocationDbHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "location_history.db";
private static final int DB_VERSION = 1;
public static final String TABLE_LOCATIONS = "locations";
public static final String COL_ID = "_id";
public static final String COL_LATITUDE = "latitude";
public static final String COL_LONGITUDE = "longitude";
public static final String COL_ALTITUDE = "altitude";
public static final String COL_ACCURACY = "accuracy";
public static final String COL_SPEED = "speed";
public static final String COL_BEARING = "bearing";
public static final String COL_TIMESTAMP = "timestamp";
public static final String COL_PROVIDER = "provider";
private static final String CREATE_TABLE =
"CREATE TABLE " + TABLE_LOCATIONS + " (" +
COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
COL_LATITUDE + " REAL NOT NULL, " +
COL_LONGITUDE + " REAL NOT NULL, " +
COL_ALTITUDE + " REAL, " +
COL_ACCURACY + " REAL, " +
COL_SPEED + " REAL, " +
COL_BEARING + " REAL, " +
COL_TIMESTAMP + " INTEGER NOT NULL, " +
COL_PROVIDER + " TEXT" +
");";
public LocationDbHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE);
// 给时间戳加索引,查询会快很多
db.execSQL("CREATE INDEX idx_timestamp ON " + TABLE_LOCATIONS + "(" + COL_TIMESTAMP + ")");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 生产环境要做迁移,不能直接drop
db.execSQL("DROP TABLE IF EXISTS " + TABLE_LOCATIONS);
onCreate(db);
}
}
20.2.2 插入位置数据
public long insertLocation(Location location) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COL_LATITUDE, location.getLatitude());
values.put(COL_LONGITUDE, location.getLongitude());
values.put(COL_ALTITUDE, location.hasAltitude() ? location.getAltitude() : null);
values.put(COL_ACCURACY, location.hasAccuracy() ? location.getAccuracy() : null);
values.put(COL_SPEED, location.hasSpeed() ? location.getSpeed() : null);
values.put(COL_BEARING, location.hasBearing() ? location.getBearing() : null);
values.put(COL_TIMESTAMP, location.getTime());
values.put(COL_PROVIDER, location.getProvider());
return db.insert(TABLE_LOCATIONS, null, values);
}
这里有个坑:hasAltitude()、hasAccuracy()这些方法一定要判断。我曾经遇到过GPS信号弱时,海拔返回的是0,但实际是无效数据。存进去后查询出来的海拔全是0,用户还以为自己在海平面跑步呢。
20.2.3 查询位置历史
public List<Location> getLocationsBetween(long startTime, long endTime) {
List<Location> locations = new ArrayList<>();
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query(
TABLE_LOCATIONS,
null,
COL_TIMESTAMP + " BETWEEN ? AND ?",
new String[]{String.valueOf(startTime), String.valueOf(endTime)},
null, null,
COL_TIMESTAMP + " ASC"
);
while (cursor.moveToNext()) {
Location location = new Location(cursor.getString(
cursor.getColumnIndexOrThrow(COL_PROVIDER)));
location.setLatitude(cursor.getDouble(
cursor.getColumnIndexOrThrow(COL_LATITUDE)));
location.setLongitude(cursor.getDouble(
cursor.getColumnIndexOrThrow(COL_LONGITUDE)));
// ... 其他字段赋值
locations.add(location);
}
cursor.close();
return locations;
}
20.3 Room数据库集成
原生SQLite写起来太啰嗦了。Google后来推出了Room,说白了就是SQLite的ORM框架。我个人非常推荐,尤其是新项目,直接用Room。
20.3.1 添加依赖
// build.gradle (app)
dependencies {
def room_version = "2.6.1"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// 如果是Kotlin项目,用kapt
// kapt "androidx.room:room-compiler:$room_version"
}
20.3.2 定义实体类
@Entity(tableName = "locations")
public class LocationRecord {
@PrimaryKey(autoGenerate = true)
private long id;
@ColumnInfo(name = "latitude")
private double latitude;
@ColumnInfo(name = "longitude")
private double longitude;
@ColumnInfo(name = "altitude")
private Double altitude;
@ColumnInfo(name = "accuracy")
private Float accuracy;
@ColumnInfo(name = "speed")
private Float speed;
@ColumnInfo(name = "bearing")
private Float bearing;
@ColumnInfo(name = "timestamp")
private long timestamp;
@ColumnInfo(name = "provider")
private String provider;
// 构造函数、getter、setter省略
// 注意:Room要求字段要么public,要么有getter/setter
}
20.3.3 定义DAO接口
@Dao
public interface LocationDao {
@Insert
long insert(LocationRecord record);
@Insert
void insertAll(List<LocationRecord> records);
@Query("SELECT * FROM locations WHERE timestamp BETWEEN :startTime AND :endTime ORDER BY timestamp ASC")
List<LocationRecord> getLocationsBetween(long startTime, long endTime);
@Query("SELECT * FROM locations ORDER BY timestamp DESC LIMIT 1")
LiveData<LocationRecord> getLatestLocation();
@Query("DELETE FROM locations WHERE timestamp < :beforeTime")
int deleteOldLocations(long beforeTime);
@Query("SELECT COUNT(*) FROM locations")
int getLocationCount();
}
为什么用LiveData?
Room天然支持LiveData,数据变化时自动通知UI更新。比如地图上实时显示当前位置,用LiveData<LocationRecord>就非常方便。不用手动刷新,数据一变,UI跟着变。
20.3.4 创建数据库
@Database(entities = {LocationRecord.class}, version = 1, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
public abstract LocationDao locationDao();
private static volatile AppDatabase INSTANCE;
public static AppDatabase getInstance(Context context) {
if (INSTANCE == null) {
synchronized (AppDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.getApplicationContext(),
AppDatabase.class,
"location_history.db"
)
.fallbackToDestructiveMigration()
.build();
}
}
}
return INSTANCE;
}
}
这里用了单例模式,避免多次创建数据库实例。我见过有人每次调用都new一个数据库,结果内存里好几个实例,数据还不同步。
20.4 位置历史查询实战
光会存不行,还得会查。实际项目中,查询场景五花八门。我总结了几种常见需求:
20.4.1 按时间段查询轨迹
// 查询今天的位置记录
long todayStart = getStartOfDay(System.currentTimeMillis());
long todayEnd = getEndOfDay(System.currentTimeMillis());
List<LocationRecord> todayRecords = locationDao.getLocationsBetween(todayStart, todayEnd);
20.4.2 分页查询
@Query("SELECT * FROM locations ORDER BY timestamp DESC LIMIT :limit OFFSET :offset")
List<LocationRecord> getLocationsPaged(int limit, int offset);
// 使用示例:每页20条,加载第3页
List<LocationRecord> page3 = locationDao.getLocationsPaged(20, 40);
20.4.3 聚合查询
@Query("SELECT COUNT(*) as count, " +
"AVG(speed) as avg_speed, " +
"MAX(speed) as max_speed, " +
"MIN(timestamp) as start_time, " +
"MAX(timestamp) as end_time " +
"FROM locations WHERE timestamp BETWEEN :startTime AND :endTime")
LocationStats getLocationStats(long startTime, long endTime);
这个聚合查询特别有用。比如用户想看看今天跑了多远、最高速度多少,一条SQL就搞定了。
20.5 数据清理策略
位置数据越积越多,不清理的话,数据库会越来越大。我建议定期清理:
- 按时间清理:保留最近30天的数据,删除更早的
- 按数量清理:最多保留10万条记录,超出则删除最旧的
- 按精度清理:精度差的数据(比如accuracy > 100米)可以删除
// 每天凌晨清理一次
public void cleanOldData() {
long thirtyDaysAgo = System.currentTimeMillis() - 30L * 24 * 60 * 60 * 1000;
int deleted = locationDao.deleteOldLocations(thirtyDaysAgo);
Log.d("LocationClean", "清理了 " + deleted + " 条旧数据");
}
20.6 知识体系总览
下面这张图,把位置数据存储的核心脉络梳理清楚了:
从这张图可以看出来,位置数据存储其实就两条路:要么用原生SQLite自己造轮子,要么用Room省心省力。我个人强烈推荐Room,它帮你处理了大部分繁琐的事情,比如线程安全、类型转换、编译时SQL校验。
好了,位置数据存储这块就聊到这儿。记住一点:存数据不是目的,查得快、用得稳才是关键。下次你写位置相关的功能时,不妨试试Room,相信你会爱上它的。
公众号:蓝海资料掘金营,微信deep3321