1. 定位与实时路径跟踪的整体设计思路
1.1 这个项目到底在解决什么问题
先把场景说清楚。假设你手上有一个移动端应用或者一个车载终端,需要在地图上实时显示设备当前的位置,并且随着设备移动,地图上要画出一条轨迹线,让用户能直观看到“从哪来、到哪去、现在在哪”。这个需求在物流配送、外勤打卡、运动轨迹记录、车辆调度等场景里非常常见。
核心要解决三件事:第一,拿到设备的实时经纬度坐标;第二,把这些坐标点在地图上打出来;第三,把连续的点连成线,形成路径轨迹。听起来简单,但实际做起来,坐标漂移、轨迹抖动、定位频率与性能的平衡、地图坐标系转换这些问题一个都跑不掉。
百度地图开放平台提供了一套完整的定位与地图绘制能力,包括定位SDK、地图SDK、覆盖物绘制接口等。这个项目的本质,就是把这些能力串起来,形成一个可用的实时定位与路径跟踪方案。
1.2 为什么选择百度地图的开源能力
市面上做地图的方案不少,有高德、腾讯、百度等。选择百度地图的原因,从我实际使用的体验来看,主要有几点:
- 定位SDK成熟度高:百度定位SDK支持GPS、基站、WiFi混合定位,在城市峡谷和室内环境下表现相对稳定,冷启动速度也还可以。
- 地图覆盖物接口丰富:画线、画点、画多边形都有现成的API,
Polyline和Marker用起来很顺手。 - 坐标系适配完善:百度地图用的是BD09坐标系,SDK内部会自动处理GPS原始坐标到BD09的转换,省去了手动纠偏的麻烦。
- 文档和社区资源多:遇到问题搜索一下,基本都能找到答案,这对快速落地很重要。
当然,它也有坑,比如定位回调频率不稳定、轨迹点过密导致地图卡顿等,这些后面会详细说。
1.3 整体架构拆解
整个方案可以拆成四层:
| 层级 | 职责 | 关键技术点 |
|---|---|---|
| 定位层 | 获取设备实时经纬度 | 百度定位SDK、定位模式选择 |
| 数据处理层 | 坐标过滤、去噪、抽稀 | 卡尔曼滤波、距离阈值过滤 |
| 地图渲染层 | 地图显示、标记点、轨迹线 | 百度地图SDK、Polyline |
| 业务逻辑层 | 轨迹存储、回放、状态管理 | 本地数据库、状态机 |
这个分层的好处是每层职责清晰,定位层只管拿坐标,数据处理层只管清洗,渲染层只管画,业务层只管逻辑。后面要换定位方案或者换地图引擎,只需要改对应层就行,不会牵一发动全身。
提示:很多新手容易把定位和渲染混在一起写,定位回调里直接操作地图UI,结果就是定位频率一高,地图就卡死。分层是必须的。
2. 核心细节解析与实操要点
2.1 定位SDK的初始化与参数配置
百度定位SDK的初始化是整个项目的第一步。在Android端,通常是在Application或者主Activity的onCreate里完成。
// 定位SDK初始化 LocationClient locationClient = new LocationClient(getApplicationContext()); LocationClientOption option = new LocationClientOption(); // 设置定位模式:高精度模式,同时使用GPS和网络定位 option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy); // 设置坐标系:BD09,百度地图专用 option.setCoorType("bd09ll"); // 设置定位间隔:2000ms,即2秒一次 option.setScanSpan(2000); // 设置是否需要地址信息 option.setIsNeedAddress(true); // 设置是否需要方向信息 option.setNeedDeviceDirect(true); locationClient.setLocOption(option); locationClient.registerLocationListener(locationListener); locationClient.start();这里有几个参数需要重点解释:
setScanSpan的设置逻辑:这个值决定了定位回调的频率。设得太小,比如500ms,定位模块频繁唤醒,耗电急剧增加,而且GPS信号本身刷新率也就1Hz左右,设太小没意义。设得太大,比如10秒,轨迹就会变成折线,看起来很不流畅。我的经验是,如果是车载场景,2到3秒比较合适;如果是步行或骑行,3到5秒足够。具体还要看移动速度,速度越快,间隔应该越小。
setLocationMode的选择:高精度模式会同时使用GPS和网络定位,精度最高但耗电也最大。如果对精度要求没那么高,比如只是做城市级别的粗略定位,可以用Battery_Saving模式,只走网络定位,耗电低很多。还有一个Device_Sensors模式,只用GPS,适合户外运动场景。
setCoorType的坑:百度地图SDK默认使用BD09坐标系,但如果你从其他渠道拿到的坐标是WGS84或GCJ02,直接画到百度地图上会有几百米的偏移。所以要么统一用百度定位SDK拿BD09坐标,要么手动做坐标系转换。这个后面会细说。
2.2 坐标系转换的底层逻辑
坐标系这个问题,几乎是每个做地图的人都会踩的坑。简单说一下背景:GPS原始坐标是WGS84,国内地图服务商出于合规要求,会在WGS84基础上做一次非线性偏移,形成GCJ02(俗称火星坐标),百度又在GCJ02基础上再做一次偏移,形成BD09。
所以如果你拿到的是一台普通GPS模块输出的WGS84坐标,直接画在百度地图上,位置会偏出几百米。解决办法有两种:
- 使用百度定位SDK:它内部会自动把WGS84转成BD09,你拿到的就是可以直接用的坐标。
- 手动转换:如果坐标来自其他设备或第三方接口,就需要自己做转换。
手动转换的代码大致如下(WGS84转BD09的简化版):
public class CoordinateConverter { private static final double PI = 3.1415926535897932384626; private static final double A = 6378245.0; private static final double EE = 0.00669342162296594323; // WGS84转GCJ02 public static double[] wgs84ToGcj02(double lat, double lon) { if (outOfChina(lat, lon)) { return new double[]{lat, lon}; } double dLat = transformLat(lon - 105.0, lat - 35.0); double dLon = transformLon(lon - 105.0, lat - 35.0); double radLat = lat / 180.0 * PI; double magic = Math.sin(radLat); magic = 1 - EE * magic * magic; double sqrtMagic = Math.sqrt(magic); dLat = (dLat * 180.0) / ((A * (1 - EE)) / (magic * sqrtMagic) * PI); dLon = (dLon * 180.0) / (A / sqrtMagic * Math.cos(radLat) * PI); return new double[]{lat + dLat, lon + dLon}; } // 判断是否在国内 private static boolean outOfChina(double lat, double lon) { return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271; } private static double transformLat(double x, double y) { double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)); ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0; ret += (160.0 * Math.sin(y / 12.0 * PI) + 320 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0; return ret; } private static double transformLon(double x, double y) { double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)); ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0; ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0; return ret; } }GCJ02转BD09的公式更简单一些:
public static double[] gcj02ToBd09(double lat, double lon) { double x = lon, y = lat; double z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * PI); double theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * PI); double bdLon = z * Math.cos(theta) + 0.0065; double bdLat = z * Math.sin(theta) + 0.006; return new double[]{bdLat, bdLon}; }注意:这些转换公式是近似算法,精度在米级别,对于一般定位场景够用。如果做高精度测绘,需要用官方提供的转换接口。
2.3 轨迹点的去噪与抽稀策略
定位SDK回调出来的原始坐标,直接画到地图上,你会发现轨迹像蚯蚓一样歪歪扭扭,甚至会出现“瞬移”——比如从A点突然跳到几百米外的B点再跳回来。这是因为定位信号受环境影响,存在漂移。
处理这个问题,我一般分三步走:
第一步:距离阈值过滤。如果新点和上一个点的距离小于某个阈值(比如5米),就认为设备没有明显移动,不记录这个点。这样可以过滤掉静止时的抖动。
private static final double MIN_DISTANCE = 5.0; // 米 private boolean shouldRecordPoint(LatLng newPoint, LatLng lastPoint) { if (lastPoint == null) return true; float[] results = new float[1]; Location.distanceBetween(lastPoint.latitude, lastPoint.longitude, newPoint.latitude, newPoint.longitude, results); return results[0] >= MIN_DISTANCE; }第二步:速度阈值过滤。如果两个点之间的计算速度超过了合理范围(比如车载场景超过200km/h),说明这个点大概率是漂移,直接丢弃。
private static final double MAX_SPEED = 55.0; // m/s,约200km/h private boolean isSpeedValid(LatLng p1, LatLng p2, long timeDiffMs) { float[] results = new float[1]; Location.distanceBetween(p1.latitude, p1.longitude, p2.latitude, p2.longitude, results); double speed = results[0] / (timeDiffMs / 1000.0); return speed <= MAX_SPEED; }第三步:抽稀。当轨迹点积累到几千个时,地图渲染压力会很大。这时候需要用Douglas-Peucker算法做抽稀,在保持轨迹形状的前提下减少点数。
public static List<LatLng> douglasPeucker(List<LatLng> points, double tolerance) { if (points.size() <= 2) return points; int maxIndex = 0; double maxDistance = 0; LatLng start = points.get(0); LatLng end = points.get(points.size() - 1); for (int i = 1; i < points.size() - 1; i++) { double distance = perpendicularDistance(points.get(i), start, end); if (distance > maxDistance) { maxDistance = distance; maxIndex = i; } } List<LatLng> result = new ArrayList<>(); if (maxDistance > tolerance) { List<LatLng> left = douglasPeucker(points.subList(0, maxIndex + 1), tolerance); List<LatLng> right = douglasPeucker(points.subList(maxIndex, points.size()), tolerance); result.addAll(left.subList(0, left.size() - 1)); result.addAll(right); } else { result.add(start); result.add(end); } return result; }tolerance的值一般取3到5米,太小了抽稀效果不明显,太大了轨迹会变形。
3. 实操过程与核心环节实现
3.1 地图初始化与定位图层配置
地图初始化的第一步是在布局文件里放一个MapView,然后在Activity里获取地图控制器。
// 布局文件 <com.baidu.mapapi.map.MapView android:id="@+id/bmapView" android:layout_width="match_parent" android:layout_height="match_parent" android:clickable="true" /> // Activity中初始化 SDKInitializer.initialize(getApplicationContext()); mMapView = findViewById(R.id.bmapView); mBaiduMap = mMapView.getMap(); // 开启定位图层 mBaiduMap.setMyLocationEnabled(true); // 配置定位图层显示方式 MyLocationConfiguration configuration = new MyLocationConfiguration( MyLocationConfiguration.LocationMode.FOLLOWING, // 跟随模式 true, // 是否显示方向 null // 自定义定位图标,null表示用默认 ); mBaiduMap.setMyLocationConfiguration(configuration);定位模式有三种:NORMAL(普通模式,地图不跟随)、FOLLOWING(跟随模式,地图中心跟着设备移动)、COMPASS(罗盘模式,地图跟着设备方向旋转)。做实时路径跟踪,一般用FOLLOWING模式,用户体验最好。
3.2 实时轨迹线的绘制
轨迹线的绘制核心是维护一个LatLng列表,每次定位回调时往列表里加一个点,然后更新Polyline。
private List<LatLng> trackPoints = new ArrayList<>(); private Polyline trackPolyline; private void updateTrackLine(LatLng newPoint) { trackPoints.add(newPoint); // 构建Polyline的选项 PolylineOptions options = new PolylineOptions() .points(trackPoints) .width(12) // 线宽,单位像素 .color(0xFF3A8EE6) // 线的颜色,蓝色 .zIndex(10) // 层级 .visible(true); // 如果已经有线,先清除再重绘 if (trackPolyline != null) { trackPolyline.remove(); } trackPolyline = (Polyline) mBaiduMap.addOverlay(options); }这里有个性能问题:每次加点都清除重绘整条线,当点数上千时,地图会明显卡顿。优化方案是分段绘制,比如每100个点作为一段,只更新最后一段。
private static final int SEGMENT_SIZE = 100; private List<Polyline> segments = new ArrayList<>(); private void updateTrackLineOptimized(LatLng newPoint) { trackPoints.add(newPoint); int segmentIndex = (trackPoints.size() - 1) / SEGMENT_SIZE; int startIndex = segmentIndex * SEGMENT_SIZE; List<LatLng> segmentPoints = trackPoints.subList(startIndex, trackPoints.size()); PolylineOptions options = new PolylineOptions() .points(segmentPoints) .width(12) .color(0xFF3A8EE6); if (segments.size() > segmentIndex) { segments.get(segmentIndex).remove(); } Polyline polyline = (Polyline) mBaiduMap.addOverlay(options); if (segments.size() > segmentIndex) { segments.set(segmentIndex, polyline); } else { segments.add(polyline); } }3.3 定位回调的完整处理流程
把前面的内容串起来,一个完整的定位回调处理流程是这样的:
private BDLocationListener locationListener = new BDLocationListener() { @Override public void onReceiveLocation(BDLocation location) { // 1. 检查定位是否成功 if (location == null || location.getLocType() == BDLocation.TypeServerError) { return; } // 2. 获取坐标 LatLng newPoint = new LatLng(location.getLatitude(), location.getLongitude()); // 3. 去噪判断 if (!shouldRecordPoint(newPoint, lastPoint)) { return; } // 4. 速度校验 if (lastPoint != null && !isSpeedValid(lastPoint, newPoint, location.getTime() - lastTime)) { return; } // 5. 更新地图定位图层 MyLocationData locationData = new MyLocationData.Builder() .accuracy(location.getRadius()) .direction(location.getDirection()) .latitude(location.getLatitude()) .longitude(location.getLongitude()) .build(); mBaiduMap.setMyLocationData(locationData); // 6. 更新轨迹线 updateTrackLineOptimized(newPoint); // 7. 更新状态 lastPoint = newPoint; lastTime = location.getTime(); // 8. 存储到本地 savePointToDatabase(newPoint, location.getTime()); } };这个流程里,第3步和第4步是保证轨迹质量的关键。我见过很多项目直接把定位回调的坐标往地图上扔,结果轨迹惨不忍睹。
3.4 轨迹数据的本地存储与回放
实时跟踪只是一部分,很多时候还需要把轨迹存下来,后面做回放或者分析。存储方案我一般用SQLite,轻量且查询方便。
// 建表语句 CREATE TABLE track_points ( id INTEGER PRIMARY KEY AUTOINCREMENT, latitude REAL NOT NULL, longitude REAL NOT NULL, timestamp INTEGER NOT NULL, accuracy REAL, speed REAL ); // 插入点 public void savePoint(double lat, double lon, long time, float accuracy, float speed) { ContentValues values = new ContentValues(); values.put("latitude", lat); values.put("longitude", lon); values.put("timestamp", time); values.put("accuracy", accuracy); values.put("speed", speed); db.insert("track_points", null, values); } // 查询某时间段的轨迹 public List<LatLng> queryTrack(long startTime, long endTime) { List<LatLng> points = new ArrayList<>(); Cursor cursor = db.query("track_points", new String[]{"latitude", "longitude"}, "timestamp BETWEEN ? AND ?", new String[]{String.valueOf(startTime), String.valueOf(endTime)}, null, null, "timestamp ASC"); while (cursor.moveToNext()) { points.add(new LatLng(cursor.getDouble(0), cursor.getDouble(1))); } cursor.close(); return points; }回放的时候,用一个定时器按时间间隔逐个取出点,动态绘制到地图上,就能实现轨迹回放效果。
4. 常见问题与排查技巧实录
4.1 定位漂移与轨迹抖动
这是最高频的问题。表现是设备静止时,地图上的定位点在小范围内乱跳;设备移动时,轨迹出现明显的锯齿或突刺。
排查思路:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 静止时点跳动 | GPS信号反射、多径效应 | 距离阈值过滤,小于5米不记录 |
| 移动时轨迹锯齿 | 定位精度低、网络定位切换 | 提高定位模式精度,开启GPS优先 |
| 偶发瞬移 | 基站定位误差大 | 速度阈值过滤,超速点丢弃 |
| 轨迹整体偏移 | 坐标系不匹配 | 检查CoorType设置,统一坐标系 |
我的经验是,距离阈值和速度阈值这两个过滤组合起来,能解决80%的轨迹质量问题。剩下的20%需要更复杂的滤波算法,比如卡尔曼滤波,但那个实现成本高,一般项目用不上。
4.2 地图卡顿与内存溢出
当轨迹点积累到几千个时,地图渲染压力会非常大。表现是地图拖动卡顿、缩放延迟、甚至OOM崩溃。
解决方案有三个层次:
第一层:抽稀。用Douglas-Peucker算法把轨迹点从几千个压缩到几百个,视觉上几乎看不出差别。
第二层:分段渲染。前面提到的分段Polyline方案,只更新最后一段,避免全量重绘。
第三层:可视区域裁剪。只渲染当前地图视野内的轨迹段,视野外的点不参与渲染。
// 可视区域裁剪 private List<LatLng> filterVisiblePoints(List<LatLng> allPoints) { LatLngBounds bounds = mBaiduMap.getMapStatus().bound; List<LatLng> visible = new ArrayList<>(); for (LatLng point : allPoints) { if (bounds.contains(point)) { visible.add(point); } } return visible; }提示:抽稀和裁剪可以同时用,先抽稀再裁剪,效果最好。
4.3 定位权限与后台限制
Android 6.0以后,定位权限需要动态申请。Android 10以后,后台定位还需要额外申请ACCESS_BACKGROUND_LOCATION权限。很多开发者只申请了前台定位权限,结果应用切到后台后定位就断了。
// 权限申请 String[] permissions = { Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION // Android 10+ }; // 检查并申请 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ActivityCompat.requestPermissions(this, permissions, REQUEST_CODE); }另外,不同厂商的系统对后台定位的限制策略不一样,有的会直接杀掉后台进程。如果项目对后台定位要求高,需要考虑前台服务(Foreground Service)方案,在通知栏常驻一个提示。
4.4 定位回调不触发或延迟大
有时候定位SDK启动了,但回调迟迟不来,或者间隔远大于设定的scanSpan。常见原因:
- 室内环境:GPS信号弱,只能靠网络定位,首次定位可能需要十几秒甚至更久。
- 权限被限制:用户在系统设置里关闭了定位权限,或者选择了“仅使用时允许”。
- 定位服务未开启:设备的定位总开关没打开。
- SDK初始化时机不对:在
attachBaseContext之前初始化会失败。
排查步骤:先检查系统定位开关,再检查应用权限,然后看日志里有没有定位回调。如果都没有,尝试切换定位模式,或者增加setScanSpan的值。
4.5 坐标系转换后的偏移问题
如果你发现轨迹整体偏移了几百米,而且偏移方向固定,那基本就是坐标系问题。检查两个地方:
- 定位SDK的
setCoorType是否设置为bd09ll。 - 如果坐标来自外部接口,确认接口返回的是WGS84还是GCJ02,然后做对应转换。
有个简单的验证方法:把已知坐标点(比如某个地标)画到地图上,看是否吻合。如果偏了,就是坐标系问题。
4.6 常见问题速查表
| 问题 | 排查方向 | 快速解决 |
|---|---|---|
| 定位不回调 | 权限、系统开关、SDK初始化 | 逐项检查,看日志 |
| 轨迹抖动 | 定位精度、过滤策略 | 加距离和速度阈值 |
| 地图卡顿 | 点数过多、渲染方式 | 抽稀+分段渲染 |
| 轨迹偏移 | 坐标系不匹配 | 统一为BD09 |
| 后台断定位 | 后台权限、进程被杀 | 前台服务+后台权限 |
| 首次定位慢 | 环境、定位模式 | 高精度模式+耐心等待 |
| 耗电严重 | 定位频率、模式 | 降低scanSpan,用省电模式 |
5. 性能优化与进阶技巧
5.1 定位频率与耗电的平衡
定位频率是耗电的大头。我做过一个粗略的测试:在同样的设备上,scanSpan设为1秒时,每小时耗电约8%到10%;设为5秒时,耗电降到3%到4%;设为10秒时,耗电约2%。所以如果项目对实时性要求没那么高,适当降低频率能显著延长续航。
一个折中方案是动态调整频率:设备移动快时用高频率,静止时用低频率。
// 根据速度动态调整定位间隔 private void adjustScanSpan(float speed) { int span; if (speed < 1.0) { // 静止 span = 10000; } else if (speed < 5.0) { // 步行 span = 5000; } else { // 车载 span = 2000; } if (span != currentSpan) { option.setScanSpan(span); locationClient.setLocOption(option); currentSpan = span; } }5.2 轨迹平滑处理
即使做了去噪,轨迹线看起来还是会有一些棱角。如果对视觉效果要求高,可以做一次平滑处理,比如贝塞尔曲线插值或者滑动平均。
滑动平均最简单,取当前点前后各N个点的平均值作为新坐标:
private List<LatLng> smoothTrack(List<LatLng> points, int windowSize) { List<LatLng> smoothed = new ArrayList<>(); for (int i = 0; i < points.size(); i++) { double sumLat = 0, sumLon = 0; int count = 0; for (int j = Math.max(0, i - windowSize); j <= Math.min(points.size() - 1, i + windowSize); j++) { sumLat += points.get(j).latitude; sumLon += points.get(j).longitude; count++; } smoothed.add(new LatLng(sumLat / count, sumLon / count)); } return smoothed; }windowSize一般取2到3,太大了轨迹会失真,转弯处会被“切角”。
5.3 多设备轨迹同步
如果是车队管理或者多人协同场景,需要把多个设备的轨迹同时显示在地图上。这时候要注意:
- 每个设备用不同颜色的轨迹线区分。
- 每个设备用一个独立的
Marker表示当前位置。 - 数据同步走长连接或者轮询,长连接实时性好但实现复杂,轮询简单但有延迟。
我一般用WebSocket做长连接,服务端收到设备上报的坐标后,推送给所有订阅的客户端。客户端收到后更新对应设备的轨迹线和标记点。
5.4 轨迹回放的实现细节
轨迹回放的核心是按时间轴逐个绘制点。实现方式有两种:
方式一:定时器逐个加点。用Handler或者Timer每隔一定时间往地图上加一个点,模拟实时跟踪的效果。优点是实现简单,缺点是回放速度不好控制。
方式二:按时间戳插值。根据回放进度计算当前应该显示到哪个时间点,然后从数据库里查出对应的点。优点是回放速度可以任意调节,缺点是实现稍复杂。
// 方式二的核心逻辑 private void playbackAt(long currentTime) { List<LatLng> points = queryTrack(startTime, currentTime); // 重绘轨迹线 redrawTrack(points); // 更新标记点位置 if (!points.isEmpty()) { updateMarker(points.get(points.size() - 1)); } }回放速度通过控制currentTime的推进速度来实现,比如1倍速就是每秒推进1000毫秒,2倍速就是每秒推进2000毫秒。
5.5 离线地图与缓存策略
如果应用场景在网络不稳定的环境下,可以考虑使用百度地图的离线地图功能。提前下载好指定城市的地图数据,即使没有网络也能显示地图底图。轨迹数据本身存在本地数据库,也不依赖网络。
离线地图的下载和更新通过MKOfflineMap类实现,这里不展开,但思路是:应用启动时检查离线地图版本,有更新就后台下载,下载完成后切换为离线模式。
6. 几个我踩过的坑和实操心得
6.1 定位回调里的线程问题
百度定位SDK的回调默认在子线程,如果你直接在回调里操作地图UI,会抛异常。必须切到主线程。
@Override public void onReceiveLocation(BDLocation location) { // 回调在子线程,切主线程 runOnUiThread(() -> { // 这里才能安全操作地图 updateTrackLine(newPoint); }); }这个坑我踩过不止一次,尤其是刚开始用的时候,日志里一堆CalledFromWrongThreadException,排查半天才发现是线程问题。
6.2 地图生命周期管理
MapView的生命周期必须和Activity同步,否则会导致内存泄漏或者地图显示异常。
@Override protected void onResume() { super.onResume(); mMapView.onResume(); } @Override protected void onPause() { super.onPause(); mMapView.onPause(); } @Override protected void onDestroy() { super.onDestroy(); mMapView.onDestroy(); locationClient.stop(); }尤其是onDestroy里一定要停止定位,否则应用退出了定位还在跑,耗电不说,还可能被系统判定为后台异常行为。
6.3 轨迹点的存储频率
不要每个定位回调都往数据库写。定位回调可能一秒好几次,数据库写入频繁会影响性能。我的做法是先在内存里缓存,每积累10个点或者每30秒批量写一次。
private List<TrackPoint> pendingPoints = new ArrayList<>(); private void cachePoint(TrackPoint point) { pendingPoints.add(point); if (pendingPoints.size() >= 10) { flushToDatabase(); } } private void flushToDatabase() { db.beginTransaction(); try { for (TrackPoint p : pendingPoints) { insertPoint(p); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } pendingPoints.clear(); }用事务批量插入,比单条插入快几十倍。
6.4 不同设备的定位精度差异
同样一套代码,在不同设备上的定位表现可能天差地别。有的设备GPS模块好,定位精度能到3米以内;有的设备只能靠网络定位,精度几十米甚至上百米。所以过滤阈值不能写死,要根据设备的定位精度动态调整。
// 根据定位精度动态调整距离阈值 private double getDynamicThreshold(float accuracy) { if (accuracy < 10) return 3.0; // 高精度设备 if (accuracy < 30) return 5.0; // 中等精度 return 10.0; // 低精度设备 }这个细节很多教程不会讲,但实际项目中非常有用。
6.5 测试阶段的模拟定位
开发阶段不可能一直拿着设备跑,所以需要模拟定位。Android的开发者选项里有“模拟位置”功能,可以设置一个模拟位置应用。或者直接在代码里写一个模拟数据源,按固定间隔生成坐标点。
// 模拟数据源 private void startMockLocation() { handler.postDelayed(new Runnable() { @Override public void run() { // 生成一个模拟点,沿着某条路线移动 double lat = baseLat + offset * Math.sin(angle); double lon = baseLon + offset * Math.cos(angle); angle += 0.01; BDLocation mockLocation = new BDLocation(); mockLocation.setLatitude(lat); mockLocation.setLongitude(lon); mockLocation.setLocType(BDLocation.TypeGpsLocation); locationListener.onReceiveLocation(mockLocation); handler.postDelayed(this, 2000); } }, 2000); }模拟定位的好处是可以反复测试轨迹绘制、去噪、存储等逻辑,不用真的跑出去。
6.6 关于轨迹数据的隐私处理
轨迹数据涉及用户位置信息,属于敏感数据。存储和传输时要注意:本地存储可以加密,传输走HTTPS,服务端存储做脱敏处理。如果应用要上架应用商店,隐私政策里必须明确说明位置信息的收集和使用方式。这些合规问题不是技术问题,但如果不注意,应用可能被下架。
这个方案从定位SDK初始化到轨迹绘制、去噪、存储、回放,基本覆盖了实时路径跟踪的完整链路。实际落地时,最花时间的往往不是核心功能,而是各种边界情况的处理和性能调优。我的建议是先把主流程跑通,再逐步加过滤、抽稀、平滑这些优化,不要一上来就追求完美,否则很容易卡在细节里出不来。