Android 14 车机 GNSS 定位数据接收问题排查全过程:从 onLocationChanged 到 Framework 节流阀的深度拆解
一、问题背景与现象
在一个基于 Android 14 的车机(Automotive)项目升级过程中,导航应用中,出现"某一秒收不到 GPS 数据"的情况。
典型表现:
LocationListener.onLocationChanged(List<Location> locations)在大部分秒(1Hz)能稳定回调,但某一秒会毫无回调。- 应用层日志缺失,框架层日志却仍可见
onReportLocation上报。 - 通过 AOSP 自定义日志前缀
wukexiang-、xiangke-关键字,能在logcat里精确定位到这一秒发生了"拦截/dropped"。
从应用侧角度看,现象就是:
明明请求了
requestLocationUpdates(GPS_PROVIDER, 0, 0, listener),时间、距离门槛都已放到最低(0, 0),为什么还会有某一秒拿不到数据?
要回答这个问题,必须顺着 GNSS 数据流向,把整条分发链路梳理清楚。
二、GNSS 定位调用流程总览(4 层心智模型)
把这次问题涉及的链路从下到上串起来看,可以浓缩成 4 层心智模型。每一层做出 return false/null 的决策,应用层就会看到那一秒没有回调。
scss
┌──────────────────────────────────────────────────────────────────┐
│ GNSS 卫星(至少 4 颗做三角测距,输出经纬度/速度/时间戳) │
└─────────────┬────────────────────────────────────────────────────┘
↓ 通过串口/UART/USB 上报 NMEA/RAW
┌──────────────────────────────────────────────────────────────────┐
│ 车机 SoC HAL 层:高通/芯驰/MTK 等 vendor 的 GNSS HAL │
│ - 合并 NMEA 帧、做 PVT 解算 │
│ - 通过 JNI 把结果交给 Framework │
└─────────────┬────────────────────────────────────────────────────┘
↓ reportLocation(...)
┌──────────────────────────────────────────────────────────────────┐
│ frameworks/base/.../location/gnss/GnssLocationProvider.java │
│ - GNSS HAL → Framework 的第一站 │
│ - 可在这里 "篡改" 卫星数量等 extras(方向二造假方案) │
└─────────────┬────────────────────────────────────────────────────┘
↓
┌─────── Provider 层(一帧)─────────┐
│ LocationProviderManager │
│ onReportLocation (Line 2518) │
│ - 过滤 0,0 / incomplete │
│ - 更新 last location │
│ - 遍历所有 Registration │
└──────────────┬─────────────────────┘
↓ 每个 Registration 一次
┌─── Registration 层(一客户端)─────┐
│ LocationRegistration │
│ acceptLocationChange (Line 867) │
│ - 过期 / 权限检查 │
│ - Predicate 节流阀 ★重灾区 │
│ · too fast │
│ · too close │
│ - 返回 ListenerOperation │
└──────────────┬─────────────────────┘
↓ ListenerMultiplexer 在 executor 上跑
┌─── Transport 层(一回调)──────────┐
│ LocationListenerTransport │
│ deliverOnLocationChanged (207) │
│ - mListener.onLocationChanged │
│ (Binder oneway 跨进程) │
└──────────────┬─────────────────────┘
↓ Binder
┌──── App 层(一回调)──────────────┐
│ ILocationListener │
│ onLocationChanged(List<Location>) │
│ - 应用进程业务回调 │
└────────────────────────────────────┘
关键提示:因为本项目在做 Android 11 → 14 的升级,而 Location 架构在 Android 12 之后经历了较大重构。过滤逻辑位于:
frameworks/base/services/core/java/com/android/server/location/provider/LocationProviderManager.java(核心分发类,本文重点)frameworks/base/services/core/java/com/android/server/location/LocationManagerService.java(入口与调度)
定位代码最直接的方式是在源码根目录全文搜索自定义日志字符串:
bash
grep -rn "xiangke-deliverOnLocationChanged" frameworks/base/services/core/java/com/android/server/location/
三、LocationProviderManager 上报链路深度拆解
LocationProviderManager 是 Android 14 重构后的单 Provider 生命周期管理器 。一个 GPS provider 实例对应一个 LocationProviderManager,它负责:
- 管理 provider 的启动/停止/状态
- 维护所有客户端 Registration(监听器注册)
- 把底层上报的 Location 过滤后分发给每个客户端
继承关系:
typescript
LocationProviderManager
extends ListenerMultiplexer<Object, LocationTransport, Registration, ProviderRequest>
implements AbstractLocationProvider.Listener
3.1 入口:onReportLocation(LocationResult) --- Line 2518
kotlin
@GuardedBy("mMultiplexerLock")
@Override
public void onReportLocation(LocationResult locationResult) {
LocationResult filtered;
if (mPassiveManager != null) {
// 非 passive manager 走完整过滤
filtered = locationResult.filter(location -> {
// 1.1 拦截 (0, 0) 假点
if (!location.isMock()
&& location.getLatitude() == 0
&& location.getLongitude() == 0) {
Log.e(TAG, "blocking 0,0 location from " + mName + " provider");
return false;
}
// 1.2 拦截不完整字段
if (!location.isComplete()) {
Log.e(TAG, "blocking incomplete location from " + mName + " provider");
return false;
}
return true;
});
if (filtered == null) return; // 整批全被过滤则中止
EVENT_LOG.logProviderReceivedLocations(mName, filtered.size());
} else {
// passive provider 拿到的已经是上面过滤过的结果
filtered = locationResult;
}
// 2. 非单调时间戳纠错(只对非 passive)
if (mPassiveManager != null) {
Location last = getLastLocationUnsafe(USER_CURRENT, PERMISSION_FINE, true, Long.MAX_VALUE);
if (last != null && locationResult.get(0).getElapsedRealtimeNanos()
< last.getElapsedRealtimeNanos()) {
Log.e(TAG, "non-monotonic location received from " + mName + " provider");
}
}
// 3. 更新 last location(每个 user 维度的缓存)
setLastLocation(filtered.getLastLocation(), UserHandle.USER_ALL);
// 4. 给所有活跃的 Registration 派发
deliverToListeners(registration -> {
return registration.acceptLocationChange(filtered);
});
// 5. 通知 passive provider(让其它应用通过 passive 拿到这次定位)
if (mPassiveManager != null) {
mPassiveManager.updateLocation(filtered);
}
}
关键点提炼
| 步骤 | 作用 | 行号 |
|---|---|---|
| 过滤 (0,0) 和 incomplete | 防止 HAL 上报脏数据 | 2521--2535 |
| 非单调时间戳 warning | 监控 HAL 时钟回退 | 2554--2557 |
setLastLocation |
把这帧写入"本 provider 最近一次定位"缓存(按 user 分桶) | 2561 |
deliverToListeners(...) |
遍历所有 Registration,每个调用 acceptLocationChange 并执行返回的 ListenerOperation |
2564 |
mPassiveManager.updateLocation |
让 PassiveProvider 也把这帧发给那些只监听 PASSIVE 的客户端 | 2570 |
注意:这里
mPassiveManager != null判断的语义是"自己不是 passive manager"。passive manager 自己不会做这套 0,0/incomplete 过滤,因为它拿到的输入是其它 provider 已经过滤过的。
厂商日志wukexiang-onReportLocation通常被加在这一方法的入口处,所以日志里看到这行就证明 HAL 已经把帧报给了 Framework。
3.2 acceptLocationChange(LocationResult) --- Line 867
这是 LocationRegistration(每个客户端一份的注册)上的过滤+构造分发操作方法。文件里有两个重写:
LocationRegistration.acceptLocationChange(Line 867):持续监听场景 ★ 我们的重点GetCurrentLocationListenerRegistration.acceptLocationChange(Line 1271):单次getCurrentLocation场景
LocationRegistration.acceptLocationChange 详解
scss
@GuardedBy("mMultiplexerLock")
@Override
@Nullable ListenerOperation<LocationTransport> acceptLocationChange(
LocationResult fineLocationResult) {
// ① 过期检查
if (SystemClock.elapsedRealtime() >= mExpirationRealtimeMs) {
if (D) Log.d(TAG, mName + " provider registration " + getIdentity()
+ " expired at " + TimeUtils.formatRealtime(mExpirationRealtimeMs));
remove();
return null;
}
// ② 权限定级,决定 FINE / COARSE
LocationResult permittedLocationResult = Objects.requireNonNull(
getPermittedLocationResult(fineLocationResult, getPermissionLevel()));
// ③ 关键的节流/位移过滤(Predicate)
LocationResult locationResult = permittedLocationResult.filter(
new Predicate<Location>() {
private Location mPreviousLocation = getLastDeliveredLocation();
@Override
public boolean test(Location location) {
// ★ 厂商魔改:min interval == 0 时直接放行并更新状态机
if (getRequest().getMinUpdateIntervalMillis() == 0) {
mPreviousLocation = location;
return true;
}
if (mPreviousLocation != null) {
// ③a too fast 判定
long deltaMs = location.getElapsedRealtimeMillis()
- mPreviousLocation.getElapsedRealtimeMillis();
long maxJitterMs = min(
(long)(FASTEST_INTERVAL_JITTER_PERCENTAGE
* getRequest().getIntervalMillis()),
MAX_FASTEST_INTERVAL_JITTER_MS); // 0.10 * interval,封顶 30s
if (deltaMs < getRequest().getMinUpdateIntervalMillis() - maxJitterMs) {
if (D) Log.v(TAG, "... dropped delivery - too fast");
return false; // ← 太快,丢弃
}
// ③b too close 判定
double smallestDisplacementM = getRequest().getMinUpdateDistanceMeters();
if (smallestDisplacementM > 0.0
&& location.distanceTo(mPreviousLocation) <= smallestDisplacementM) {
if (D) Log.v(TAG, "... dropped delivery - too close");
return false; // ← 太近,丢弃
}
}
mPreviousLocation = location; // ★ 必须在 return true 前更新状态机
return true;
}
});
if (locationResult == null) { // Predicate 把所有 Location 都过滤掉
return null;
}
// ④ AppOps 记账(noteOp)
if (!mAppOpsHelper.noteOpNoThrow(
LocationPermissions.asAppOp(getPermissionLevel()), getIdentity())) {
if (D) Log.w(TAG, mName + " provider registration " + getIdentity() + " noteOp denied");
return null;
}
// ⑤ 决定是否使用 wake lock(passive 请求不使用)
boolean useWakeLock = getRequest().getIntervalMillis() != LocationRequest.PASSIVE_INTERVAL;
// ⑥ 构造 ListenerOperation:真正执行跨进程回调
return new ListenerOperation<LocationTransport>() {
@Override public void onPreExecute() {
setLastDeliveredLocation(locationResult.getLastLocation()); // ★ 状态机更新
if (useWakeLock) mWakeLock.acquire(WAKELOCK_TIMEOUT_MS); // 30s wake lock
}
@Override public void operate(LocationTransport listener) throws Exception {
// 同进程要 deepCopy,因为 Location 是 mutable 的
LocationResult deliverLocationResult;
if (getIdentity().getPid() == Process.myPid()) {
deliverLocationResult = locationResult.deepCopy();
} else {
deliverLocationResult = locationResult;
}
listener.deliverOnLocationChanged(deliverLocationResult,
useWakeLock ? mWakeLockReleaser : null);
EVENT_LOG.logProviderDeliveredLocations(mName, locationResult.size(), getIdentity());
}
@Override public void onPostExecute(boolean success) {
if (!success && useWakeLock) mWakeLock.release();
if (success) {
boolean remove = ++mNumLocationsDelivered >= getRequest().getMaxUpdates();
if (remove) {
if (D) Log.d(TAG, "... finished after " + mNumLocationsDelivered + " updates");
remove();
}
}
}
};
}
GetCurrentLocationListenerRegistration.acceptLocationChange --- Line 1271
单次定位(getCurrentLocation())的版本不做 too fast / too close 节流 ------单次定位永远放行,永远不持 wake lock,无论成功失败只要 operate 跑过一次就 remove() 自注销。本文讨论的"丢一秒"问题不涉及这条路径。
两个易混点
- 状态机更新的位置 :
Predicate.test()内部mPreviousLocation = location(Line 923)只在return true前执行;外层setLastDeliveredLocation(Line 952) 在onPreExecute阶段执行。两者必须保持一致,否则会出现"Predicate 通过,但 mPreviousLocation 状态机被锁死"的 Bug(详见第 9 章)。 - 重注册时的 mPreviousLocation 来源 :
onRegistrationReplaced(Line 2085) 会通过newRegistration.setLastDeliveredLocation(oldRegistration.getLastDeliveredLocation())把旧 Registration 的"上次成功分发"灌给新 Registration,从而保留状态机连续性。这是为了避免重注册瞬间立刻拦截下一帧。
3.3 Transport 层:deliverOnLocationChanged --- 三种实现
LocationTransport 抽象传输层接口告诉 ListenerMultiplexer "拿到 LocationResult 该怎么发给客户端"。本类有 3 个具体实现。
(1) LocationListenerTransport --- Line 207(最常见,对应 requestLocationUpdates(listener))
less
@Override
public void deliverOnLocationChanged(LocationResult locationResult,
@Nullable IRemoteCallback onCompleteCallback) throws RemoteException {
try {
mListener.onLocationChanged(locationResult.asList(), onCompleteCallback);
} catch (RuntimeException e) {
// 系统进程内直接调用时才能冒泡 RuntimeEx
RuntimeException wrapper = new RuntimeException(e);
FgThread.getExecutor().execute(() -> { throw wrapper; });
}
}
mListener.onLocationChanged(...)就是应用层看到的LocationListener.onLocationChanged(List<Location>)回调locationResult.asList()把LocationResult拆成List<Location>onCompleteCallback=mWakeLockReleaser,应用处理完后回调释放 wake lock;passive 请求时为 null- 异常处理:把运行时异常转到 FgThread 抛出,避免污染当前 binder 线程
(2) LocationPendingIntentTransport --- Line 272(对应 PendingIntent 方式)
less
@Override
public void deliverOnLocationChanged(LocationResult locationResult,
@Nullable IRemoteCallback onCompleteCallback)
throws PendingIntent.CanceledException {
BroadcastOptions options = BroadcastOptions.makeBasic();
options.setDontSendToRestrictedApps(true);
// 允许接收方启动前台服务(PI 定位方式的特殊待遇)
options.setTemporaryAppAllowlist(TEMPORARY_APP_ALLOWLIST_DURATION_MS,
TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
REASON_LOCATION_PROVIDER, "");
Intent intent = new Intent().putExtra(KEY_LOCATION_CHANGED,
locationResult.getLastLocation());
if (locationResult.size() > 1) {
intent.putExtra(KEY_LOCATIONS, locationResult.asList().toArray(new Location[0]));
}
PendingIntentSender.send(mPendingIntent, mContext, intent, callback, options.toBundle());
}
- 通过广播 Intent 把 Location 塞进 extra
KEY_LOCATION_CHANGED(location)和KEY_LOCATIONS(数组,>1 个时) setTemporaryAppAllowlist让接收方可临时启动 fg service(10 秒)
(3) GetCurrentLocationTransport --- Line 334(单次定位)
less
@Override
public void deliverOnLocationChanged(@Nullable LocationResult locationResult,
@Nullable IRemoteCallback onCompleteCallback) throws RemoteException {
Preconditions.checkState(onCompleteCallback == null); // 不支持完成回调
try {
if (locationResult != null) {
mCallback.onLocation(locationResult.getLastLocation());
} else {
mCallback.onLocation(null); // 过期或不允许时返回 null
}
} catch (RuntimeException e) { ... }
}
- 调用
ILocationCallback.onLocation(Location),单次语义 - 允许返回
null(超时/权限被拒)
3.4 Executor 选择与 wake lock 配套
deliverToListeners(fn) 的语义是:遍历所有活跃的 Registration,对每个调用 fn.apply(registration) 拿回一个 ListenerOperation<LocationTransport>,然后在该 Registration 的 Executor 上依次执行:
onPreExecute()--- 在锁内同步执行:setLastDeliveredLocation+acquire wake lockoperate(listener)--- 在 executor 上跑:触发deliverOnLocationChangedonPostExecute(success)--- 在 executor 上跑:释放 wake lock、累计mNumLocationsDelivered、触发remove
| Registration 类型 | Executor | Line |
|---|---|---|
LocationListenerRegistration |
identity.isMyProcess() ? FgThread.getExecutor() : DIRECT_EXECUTOR |
1023 |
LocationPendingIntentRegistration |
DIRECT_EXECUTOR |
1102 |
GetCurrentLocationListenerRegistration |
同 Listener | 1166 |
- 跨进程应用:
DIRECT_EXECUTOR即阻塞执行,因为后面mListener.onLocationChanged本身是 oneway Binder,非阻塞 - 系统进程内(
FgThread.getExecutor()):避免阻塞当前线程
3.5 完整链路心法对照表
| 链路阶段 | 函数 | 行号 | 职责 |
|---|---|---|---|
| Provider 层 | onReportLocation |
2518 | HAL→Framework 入口,过滤 0,0 / incomplete,分发到所有 Registration |
| Registration 层 | acceptLocationChange |
867 | 每客户端的过期/权限/节流/位移过滤 |
| Predicate | test(Location) |
888 | too fast + too close 节流阀 |
| ListenerOperation | onPreExecute/operate/onPostExecute |
947-996 | wake lock + 跨进程投递 + 计数 |
| Transport 层 | deliverOnLocationChanged |
207/272/334 | ILocationListener / PendingIntent / ILocationCallback |
| App 层 | ILocationListener.onLocationChanged |
AIDL | 应用进程业务回调 |
四、问题的两类典型成因
排查早期发现,"丢一秒"在同一日志里出现了两种完全不同的原因,必须分开看:
| 场景 | 触发原因 | 日志特征 | 卫星情况 |
|---|---|---|---|
| A | 最小位移 smallestDisplacementMeters / 重注册巧合 |
经纬度完全不变、速度≈0 | 卫星数好(10 颗) |
| B | 硬件时间抖动命中节流阀 too fast |
两次间隔 < 900ms | 卫星数骤降到 3 颗 |
另外还有一类上游成因:低卫星数过滤(3 颗触发高精度 HIGH_ACCURACY 哨兵拦截),会影响场景 A 的现场一(17:12:14)。
五、场景 A:位移过滤 + 重注册巧合导致丢一秒
5.1 现场一:分发次数从 5 次骤降为 1 次
对三个连续秒的 xiangke-deliverOnLocationChanged 打印次数:
| 时间戳 (UTC) | 分发次数 | 卫星数 | 说明 |
|---|---|---|---|
| 17:12:12 (132000) | 5 次 | 7 颗 | 正常全员分发 |
| 17:12:13 (133000) | 5 次 | 6 颗 | 正常全员分发 |
| 17:12:14 (134000) | 1 次 | 3 颗 | 4 个客户端被拦截 |
这一秒只有 1 个客户端拿到了数据(大概率是注册了 PASSIVE_PROVIDER 或无距离限制的后台 Service),其他 4 个客户端(包括导航)全被过滤掉。
5.2 为什么卫星数掉到 3 颗会被拦截
- GNSS 定位原理:标准三维空间定位(经度、纬度、高度、钟标定)需要至少 4 颗卫星。少于 4 颗时解算置信度极低,只能退化成 2D 漂移。
- 车机定制的"精度/卫星数哨兵过滤":导航应用以
HIGH_ACCURACY申请定位。当 Framework 判断satellites < 4或hAcc不满足要求时,会直接continue跳过高精度 Listener,防止低质量数据污染导航图层。
伪代码结构:
ini
Bundle extras = location.getExtras();
int satellites = extras != null ? extras.getInt("satellites", 0) : 0;
// !!! 就是这行类似代码拦下了导航 !!!
if (satellites < 4 && request.isHighAccuracy()) {
continue; // 跳过分发
}
5.3 现场二:坐标一模一样导致 too close 拦截
更隐蔽的一个案例发生在卫星数非常好(10 颗)的秒:
| 时间戳 (UTC) | 分发次数 | 经纬度 | 速度 (vel) | 卫星数 |
|---|---|---|---|---|
| 1780303766000 | 4 | 30.557408, 104.212616 | 0.00154 | 10 |
| 1780303767000 | 1 | 30.557408, 104.212616 | 0.00102 | 10 |
| 1780303768000 | 4 | 30.557408, 104.212616 | 0.00257 | 10 |
1780303767000 这秒数据特征:
- 卫星 10 颗、信号好(maxCn0=48),可排除"卫星不够导致丢数";
- 坐标与上一秒完全一致;
- 速度 = 0.00102(近乎零漂移)。
结合源码(Line 910-919),这一秒拦截由两个强相关行为决定:
嫌疑犯 1:smallestDisplacementMeters 最小位移过滤
导航应用为了防止车辆静止时图标"原地乱跳",请求定位时通常设置了一个最小距离阈值(例如 minUpdateDistanceMeters = 1.0,移动超过 1 米才回调)。
源码 Line 910-919:
scss
// ③b too close 判定
double smallestDisplacementM = getRequest().getMinUpdateDistanceMeters();
if (smallestDisplacementM > 0.0
&& location.distanceTo(mPreviousLocation) <= smallestDisplacementM) {
if (D) Log.v(TAG, "... dropped delivery - too close");
return false;
}
在 1780303767000,系统计算:
kotlin
location.distanceTo(mPreviousLocation) == 0 米
0 米 < 1 米,触发 return false
对导航应用返回 null,acceptLocationChange 拿不到 ListenerOperation。
而那个唯一打印出来的 operate-1 属于系统内部的 PASSIVE 服务(距离限制为 0),通过 Line 912 的 > 0.0 检查(不满足),因此它能成为"漏网之鱼"拿到数据。
嫌疑犯 2:致命巧合------导航应用这一瞬正好进行了"重注册"
在这一秒前后发生:
- Line 96912 ~ 97224:高通/车机设备服务(deviceservice)频繁 remove/add 重注册;
- Line 102170(紧跟在这一秒数据结束后的下一行):导航应用自己也触发重注册。
在 Android 14 架构中,当应用重新注册时,onRegistrationReplaced(Line 2085)会把旧 Registration 的 getLastDeliveredLocation() 灌给新 Registration。然而,如果在重置期间底层报上一个"距离完全没变、速度近 0"的数据,新注册进来的 j01app 仍然会因为 distanceTo == 0 被完美过滤。
5.4 三类上游成因回顾
完整的"为什么丢一秒"体系,从源头看:
-
硬件限制:手机/车机 GNSS 芯片硬件刷新率上限通常就是 1Hz(1Hz=1秒一次),5Hz/10Hz 是高频模组。
-
时间对齐误差:如果代码强依赖"严格每秒整点回调",由于硬件计算耗时 + 线程调度延迟,实际间隔可能是 1.1s/1.2s,会导致某一物理秒内没有回调。
-
定位请求参数:
setIntervalMillis():期望间隔,设为 1000ms 系统会尽量但不保证严格每秒。setMinUpdateIntervalMillis():最快间隔,设得太接近 1000ms 会把微小延迟压成合并。setMinUpdateDistanceMeters():本次重点排查项,设了一段位移阈值,静止时就不回调。
-
卫星信号遮挡:"城市峡谷"、室内、高架桥、树荫下卫星信号瞬间被遮挡,芯片解算不出准确位置会直接丢弃这一帧。
-
Android 省电策略:Doze Mode、后台限速;主线程卡顿同样会导致回调丢失。
六、场景 B:时间抖动命中节流阀 too fast
更隐蔽的一个丢数现场:1780381622000 一秒推送来了大量 dropped delivery - too fast 日志。
6.1 现场日志片段
less
06-02 14:27:01.195 994 1144 I LocationManagerService: kexiang-onReportLocation:1780381621000--1540922525515
06-02 14:27:01.195 994 1144 I LocationManagerService: kexiang-getLastLocation:Location[gps 30.557632,104.224101 hAcc=1.19 et=+25m40s922ms alt=498.7 vel=5.147222E-4 bear=303.29 {Bundle[{satellites=7, maxCn0=45, meanCn0=32}]}]
06-02 14:27:01.196 994 1144 I LocationManagerService: xiangke-deliverOnLocationChanged operate-1:[Location[gps 30.557632,104.224101 hAcc=1.19 et=+25m40s922ms alt=498.7 vel=5.147222E-4 bear=303.29 {Bundle[{satellites=7, maxCn0=45, meanCn0=32}]}]]
06-02 14:27:01.198 994 1144 I LocationManagerService: xiangke-deliverOnLocationChanged operate-1:[Location[gps 30.557632,104.224101 hAcc=1.19 et=+25m40s922ms alt=498.7 vel=5.147222E-4 bear=303.29 {Bundle[{satellites=7, maxCn0=45, meanCn0=32}]}]]
06-02 14:27:01.212 994 1144 I LocationManagerService: xiangke-deliverOnLocationChanged operate-1:[Location[gps 30.557632,104.224101 hAcc=1.19 et=+25m40s922ms alt=498.7 vel=5.147222E-4 bear=303.29 {Bundle[{satellites=7, maxCn0=45, meanCn0=32}]}]]
06-02 14:27:01.213 994 1144 I LocationManagerService: xiangke-permittedLocationResult.filter:gps provider registration 1000/com.fotaapp/B4EBA021 dropped delivery - too fast
06-02 14:27:01.223 994 1144 I LocationManagerService: xiangke-deliverOnLocationChanged operate-1:[Location[gps 30.557632,104.224101 hAcc=1.19 et=+25m40s922ms alt=498.7 vel=5.147222E-4 bear=303.29 {Bundle[{satellites=7, maxCn0=45, meanCn0=32}]}]]
06-02 14:27:01.225 994 1144 I LocationManagerService: xiangke-deliverOnLocationChanged operate-1:[Location[gps 30.557632,104.224101 hAcc=1.19 et=+25m40s922ms alt=498.7 vel=5.147222E-4 bear=303.29 {Bundle[{satellites=7, maxCn0=45, meanCn0=32}]}]]
06-02 14:27:01.320 994 1144 I LocationManagerService: kexiang-getLastLocation:Location[gps 30.557632,104.224101 hAcc=1.19 et=+25m40s922ms alt=498.7 vel=5.147222E-4 bear=303.29 {Bundle[{satellites=7, maxCn0=45, meanCn0=32}]}]
06-02 14:27:01.320 994 1144 I LocationManagerService: xiangke-permittedLocationResult.filter:passive provider registration 1000/android[SensorNotificationService]/C8FD3B3A dropped delivery - too fast
06-02 14:27:02.077 994 1144 I LocationManagerService: kexiang-onReportLocation:1780381622000--1541818426515
06-02 14:27:02.077 994 1144 I LocationManagerService: kexiang-getLastLocation:Location[gps 30.557633,104.224101 hAcc=1.21 et=+25m41s818ms alt=498.7 vel=0.0 bear=303.29 {Bundle[{satellites=7, maxCn0=45, meanCn0=32}]}]
06-02 14:27:02.077 994 1144 I LocationManagerService: xiangke-permittedLocationResult.filter:gps provider registration 10064/com.fawvw.vehicle.navi.j01app/4EA6A899 dropped delivery - too fast
06-02 14:27:02.077 994 1144 I LocationManagerService: xiangke-permittedLocationResult.filter:gps provider registration 10064/com.fawvw.vehicle.navi.j01app/1A310137 dropped delivery - too fast
06-02 14:27:02.078 994 1144 I LocationManagerService: xiangke-deliverOnLocationChanged operate-1:[Location[gps 30.557633,104.224101 hAcc=1.21 et=+25m41s818ms alt=498.7 vel=0.0 bear=303.29 {Bundle[{satellites=7, maxCn0=45, meanCn0=32}]}]]
06-02 14:27:02.079 994 1144 I LocationManagerService: xiangke-permittedLocationResult.filter:gps provider registration 1000/com.adayo.fotaapp/B4EBA021 dropped delivery - too fast
06-02 14:27:02.079 994 1144 I LocationManagerService: xiangke-permittedLocationResult.filter:gps provider registration 1000/com.adayo.aaop_deviceservice/A1B15784 dropped delivery - too fast
06-02 14:27:02.079 994 1144 I LocationManagerService: xiangke-permittedLocationResult.filter:gps provider registration 10064/com.fawvw.vehicle.navi.j01app/8ACD8D13 dropped delivery - too fast
06-02 14:27:02.079 994 1144 I LocationManagerService: kexiang-getLastLocation:Location[gps 30.557633,104.224101 hAcc=1.21 et=+25m41s818ms alt=498.7 vel=0.0 bear=303.29 {Bundle[{satellites=7, maxCn0=45, meanCn0=32}]}]
06-02 14:27:02.079 994 1144 I LocationManagerService: xiangke-permittedLocationResult.filter:passive provider registration 1000/android[SensorNotificationService]/C8FD3B3A dropped delivery - too fast
注意 maxCn0、meanCn0 都很正常,卫星数也正常(7 颗),并不是信号问题。几次连续帧的 et(elapsedRealtimMillis)偏差是:
- 上一帧 et =
+25m40s922ms - 这一帧 et =
+25m41s818ms← 间隔约896ms
也就是说两帧之间只有 896ms,命中了节流红线,被拦截。
6.2 节流阀代码拆解(源码 Line 888-921)
scss
@Override
public boolean test(Location location) {
if (getRequest().getMinUpdateIntervalMillis() == 0) {
mPreviousLocation = location;
return true; // ★ 厂商魔改:0,0 直接放行
}
if (mPreviousLocation != null) {
// ① 真实时间差
long deltaMs = location.getElapsedRealtimeMillis()
- mPreviousLocation.getElapsedRealtimeMillis();
// ② 抖动宽容值(动态计算)
long maxJitterMs = min(
(long)(FASTEST_INTERVAL_JITTER_PERCENTAGE * getRequest().getIntervalMillis()),
MAX_FASTEST_INTERVAL_JITTER_MS);
// ③ too fast 判定
if (deltaMs < getRequest().getMinUpdateIntervalMillis() - maxJitterMs) {
if (D) Log.v(TAG, "... dropped delivery - too fast");
return false;
}
// ④ too close 判定
double smallestDisplacementM = getRequest().getMinUpdateDistanceMeters();
if (smallestDisplacementM > 0.0
&& location.distanceTo(mPreviousLocation) <= smallestDisplacementM) {
if (D) Log.v(TAG, "... dropped delivery - too close");
return false;
}
}
mPreviousLocation = location;
return true;
}
第一步:计算 deltaMs(Line 895-896)
deltaMs = 当前帧 elapsedRealtime - 上一帧 mPreviousLocation.elapsedRealtime,这是两帧之间的真实物理间隔。
第二步:计算 maxJitterMs(抖动宽容值,Line 897-899)
arduino
private static final double FASTEST_INTERVAL_JITTER_PERCENTAGE = 0.10f; // Line 163
private static final long MAX_FASTEST_INTERVAL_JITTER_MS = 30 * 1000; // Line 166
long maxJitterMs = min(
(long)(0.10 * getRequest().getIntervalMillis()),
30000);
含义:
0.10:系统认为合理的硬件时间误差比例(10%);30000ms:抖动宽容的封顶绝对值,避免长间隔时被无限放大。
举两个极端例子理解这个 min 上限:
| 场景 | intervalMillis | 10% | 30 秒封顶 | maxJitterMs |
|---|---|---|---|---|
| 高频(1Hz 导航) | 1000ms | 100ms | 30000ms | 100ms |
| 低频(每小时一次) | 3600000ms | 360000ms | 30000ms | 30000ms |
也就是说:高频时宽容度由比例决定;低频时被 30 秒天花板封住,防止宽容被无限放大。这正是 Android 团队给你留的"30 秒防线"。
第三步:核心拦截 too fast(Line 900-907)
kotlin
if (deltaMs < getRequest().getMinUpdateIntervalMillis() - maxJitterMs) {
return false; // 拦截
}
字面解读:如果"实际时间间隔"小于"APP 能忍受的最快间隔 − 抖动折扣",判为"太快,丢弃"。
6.3 用具体数字理解阈值
假设高德导航申请:interval=1000ms,MinUpdateInterval=1000ms。
maxJitterMs = 0.10 * 1000 = 100ms,所以拦截红线变成 900ms。
情景 A --- 硬件准时 上一帧 0 秒,这一帧 1005ms 上报。deltaMs = 1005ms,不小于 900,放行。
情景 B --- 前有一帧解算略快 上一帧 0 秒,这一帧 950ms 上报。deltaMs = 950ms,不小于 900ms,依然放行!这就是 Jitter 机制在保护这帧不被误杀。
情景 C --- 底层抽风/被动共享逻辑塞过快 上一帧刚发走,仅 800ms 就塞过来。deltaMs = 800ms < 900ms。系统判定:
"APP 明确说最快 1000ms,我虽然给了硬件 100ms 宽容,但你 800ms 来要太过分了!"
执行 dropped delivery - too fast,这一帧直接扔进垃圾桶。导航 APP 根本不知道这个数据的存在。
本质 :这是一个带弹性的节流阀 Throttling Valve,不是死卡时间,而是画了一条"绝对底线",跌破就斩杀。
6.4 896ms 这一帧实际的命中
- 上一帧 et =
+25m40s922ms - 这一帧 et =
+25m41s818ms,间隔约896ms; maxJitterMs = 100ms,红线1000-100=900ms;896 < 900→ 命中拦截,回报dropped delivery - too fast。
更进一步:这一秒导航 j01app 同一瞬间弹出了三条 too fast(如 @EA6A899、1A310137、8ACD8D13)。原因是车机导航为了平滑通常会注册多个监听器(一个画车标,一个惯导推算,一个电子地平线),它们全部失血。这一秒全车机只有一个无名客户端(Line 7319) 因为它在更早帧被截过、deltaMs 远大于 1000ms,才成为漏网之鱼。
七、应用层已异化:0, 0 为什么拦不住?
工程师最后试应用层最严苛的请求:
ini
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 0, 0, locationListener);
参数对应(标准方法原型 requestLocationUpdates(provider, minTimeMs, minDistanceM, listener)):
minTimeMs = 0:表示"只要底层芯片有数据立刻报,框架不要做任何时间拦截节流";minDistanceM = 0:表示"原地 0 米位移也必须回调";provider = GPS_PROVIDER:直接激活 GNSS 天线,跳过网络定位等低精度源。
按朴素理解,这种配下来 MinUpdateIntervalMillis() 应该是 0,红线应该变成 0 - maxJitterMs = −100ms,物理时间差永远 > 0,判定永远不成立,永远不应被拦下。但日志里 deviceservice 仍被拦下 too fast。
这里有三个 Framework 层的"隐藏套路":
7.1 老接口的兼容黑盒:0 被偷偷改成 1000
调用传统 requestLocationUpdates(provider, 0, 0, listener) 时,系统不会 直接把 0 丢给底层,而是通过 LocationManager.createFromDeprecatedProvider(...) 隐藏方法转换:
- 因为使用
GPS_PROVIDER,被判定为高精度高功耗请求; - 为防止应用传 0 导致底层 Binder 被疯狂轰炸挂掉,框架在转换时强行把
mIntervalMillis(期望间隔)设为硬编码默认值 (通常 1000ms 或 500ms),只有mMinUpdateIntervalMillis(最快能接受)保持为 0。
所以 getRequest().getIntervalMillis() 拿到的不是 0,而是系统强加的期望值(比如 1000),maxJitterMs = 10% * 1000 = 100ms,红线 0 - 100 = -100ms,逻辑上不该拦截,但是...
7.2 原因一:多客户端 Request 合并(Coalescing)偷偷改了你的参数
LocationProviderManager 是全车机所有应用共享的。如果导航、仪表盘、HAL、Passive 任一方在同时段提了 1000ms 的请求,系统在分发时为了防止应用相互干扰,会通过内部融合 merge 机制 对齐到当前活跃的最高优先级请求,把当前通道的 MinUpdateIntervalMillis 临时对齐到 1000ms。
于是:
- 你的
MinUpdateIntervalMillis在运行时被偷改成 1000; - 公式实际变成:
deltaMs < 1000 - 100 = 900ms; - 你的
deltaMs = 896ms < 900ms,完美中枪被杀。
7.3 原因二:重注册导致时间戳跨越"时空"
更致命的"状态机潜规则"。注意那句致命日志:
makefile
14:27:02.077 dropped delivery gps provider registration too fast
这一秒不只是 deviceservice 被杀,连导航 j01app 也在这一秒触发了 gps provider added registration from 10064 j01app。
在重注册(Remove & Add)发生时,onRegistrationReplaced(Line 2085)会把旧 Registration 的 last delivered 灌给新 Registration。如果灌过来的是更早的那一帧,而底层刚报上"距离完全没变、速度近 0"的数据,新注册进来的应用会因为"时间间隔仍不足 / 距离 0"被完美过滤。
把状态机的"时空"和 7.1 的负数公式结合看:理论上负数公式永远不该拦截,但实际上多客户端合并 + 重注册错位让这一秒依然命中。
八、把隐藏日志逼出来:如何让 too fast / too close 现形
很多场景下 AOSP 用两层开关把这种"每秒都在发生"的事隐藏起来:
第一关:if (D) 编译期或运行期开关
源码 Line 40:
arduino
import static com.android.server.location.LocationManagerService.D;
D 通常来自:
arduino
private static final boolean D = Log.isLoggable(TAG, Log.DEBUG);
// 或硬编码
private static final boolean D = false;
车机正式量产版(User 版)下 D 默认 false,里面的代码根本不执行。Line 853、873、902、915 等处的 if (D) 都是这个开关在把守。
第二关:Log.v() Verbose 级限制
即便 D=true,节流分支里用的也是 Log.v()(Verbose 级,见 Line 904、917)。logcat 默认会过滤掉框架层海量 Verbose 日志(认为每秒都在发生,撑爆缓冲区)。
方案一:改代码最直接
把那两句 if (D) Log.v(...) 改成 Log.i / Log.e:
kotlin
// 注释掉 if (D)
// if (D) Log.v(...);
// 直接换成 Info 级,并加上你的专属前缀
Log.i(TAG, "xiangke-拦截原因:" + mName + " dropped delivery too close");
return false;
方案二:不改正代码,用 ADB 动态开 prop
shell
# 假设 TAG 是 "LocationManagerService"
adb shell setprop log.tag.LocationManagerService VERBOSE
# 如果 Android 12+ TAG 可能是 "LocationProviderManager"
adb shell setprop log.tag.LocationProviderManager VERBOSE
# 重启相关服务或整个车机让 prop 生效
adb shell stop && adb shell start
设置之后你会在 logcat 里看到每一帧是否命中节流红线,以及 getMinUpdateIntervalMillis() 实际值是否被偷偷改写。可在拦截分支里加一条诊断 Log:
less
Log.i(TAG, "xiangke-... deltaMs=" + deltaMs
+ ", maxJitterMs:" + maxJitterMs
+ ", 最终红线(右边):" + (getRequest().getMinUpdateIntervalMillis() - maxJitterMs));
你将看到以下两种结果之一:
getMinUpdateIntervalMillis()根本不是 0,而是被系统偷偷改成了 1000;deltaMs因底层硬件或状态机错乱,算出一个极其离谱的数字(甚至是负数)。
九、修复方案
9.1 致命 Bug 的反面教训:状态机必须同步
在改 test() 让 minUpdateIntervalMillis == 0 的请求直接放行时,最容易踩的状态机陷阱:
❌ 错误改法 (把 mPreviousLocation = location 移进 if (mPreviousLocation != null) 里):
typescript
@Override
public boolean test(Location location) {
if (getRequest().getMinUpdateIntervalMillis() == 0) {
// 如果把 mPreviousLocation = location 掉,
// 只留 return true; 移到里面的逻辑
return true;
}
if (mPreviousLocation != null) {
// ⚠️ 如果只把 mPreviousLocation = location 放到这里
// 第一次进来(开机第一帧)mPreviousLocation 为 null,
// 直接触发 if,方法弹出了
// mPreviousLocation = location 永远不会被执行!
// 导致死循环:每帧 mPreviousLocation 永远是 null → 永远 return true
}
// ...
}
为什么这件看似"永远放行挺好"的事很可怕?(对照源码 Line 465/469 setLastDeliveredLocation/getLastDeliveredLocation,Line 829 的 onActive 历史定位补发,Line 2089 的 onRegistrationReplaced)
LocationManagerService是个多应用复用管理器 ,这个test()不只服务你一家;mPreviousLocation永远是null,等于这个注册通道的"状态机"被彻底打死;- 在权限检查、被动定位共享(Passive)、
getLastDeliveredLocation()计算时,系统会误认为"这个监听器从来没有成功收到过任何一条定位数据",引发其他应用(导航/惯导/天气)的定位严重错乱或彻底停摆。
✅ 正确改法 :只要对当前应用 return true(放行数据),就必须在放行的同时更新状态,告诉系统"这一帧的时间已经作为对比基准":
scss
@Override
public boolean test(Location location) {
// 特殊处理:如果是 0, 0 请求,直接更新上一帧并放行
if (getRequest().getMinUpdateIntervalMillis() == 0
&& getRequest().getMinUpdateDistanceMeters() == 0) {
mPreviousLocation = location; // 必须先更新状态!
return true;
}
// ... 原过滤逻辑
}
把 mPreviousLocation = location 独立写在 if 块里,用于在提前结束方法时同步状态机。
9.2 方案 A:底层 HAL 层做时间平滑(推荐)
根源在于 GNSS HAL 上报存在抖动(一秒报 922ms、下一秒报 818ms)。可让 GPS 驱动团队在 GnssLocationProvider JNI 层上报时,对 ElapsedRealtimeNanos 做硬性微调,保证每次上报间隔绝对不低于 950ms。下层稳了,上层 Framework 绝不会拦截。
9.3 方案 B:Framework 层放宽限速门槛(最快见效)
在 test(Location) 最上面加一行死命令:只要包名是车机核心服务或导航,直接 return true 放行:
less
@Override
public boolean test(Location location) {
// 强力对策:如果是车机核心服务或导航,且 APP 明确要求高频(0,0),直接放行
if (getRequest().getMinUpdateIntervalMillis() == 0
&& getRequest().getMinUpdateDistanceMeters() == 0
&& (mName.contains("j01app")
|| mName.contains("com.adaayo.aaop.deviceservice"))) {
mPreviousLocation = location; // 同步状态机
return true;
}
// 原生 AOSP 的 min interval / min distance / too fast 节流逻辑保持原样
// ...
}
9.4 方向一(针对低卫星数过滤):修改或移除 "低卫星数过滤"
若导航在 3 颗星时也想强行收到数据,最直接的措施是改过滤条件,而不是去改卫星数量本身:
ini
// 找到分发过程(deliverToListeners 或 deliverOnLocationChanged)
Bundle extras = location.getExtras();
int satellites = extras != null ? extras.getInt("satellites", 0) : 0;
// !!! 原本是这行类似代码拦下你的导航 !!!
if (satellites < 4 && request.isHighAccuracy()) {
continue; // 注释掉这行即可,或降低阈值
}
9.5 方向二(原地"伪造"卫星数,欺骗上层)
如果过滤逻辑是高通/MTK/供应商写在更深层闭源处的,可以直接在 Framework 第一站截住 HAL 上报:
java
// GnssLocationProvider.reportLocation(...)
private void reportLocation(boolean hasLatLong, Location location) {
// 强制改 satellites 数量
Bundle extras = location.getExtras();
if (extras != null && extras.getInt("satellites", 0) < 4) {
extras.putInt("satellites", 10);
}
// ...
}
9.6 系统级别终极建议
既然确认是 896ms 抖动命中节流误杀,作为车机底层工程师:不要去和原生 Framework 的复用与抖动算法较劲。
车机有绝对稳定不丢数的业务诉求。最稳妥的办法是在
test()最上面加一行死命令:只要包名是你们车机核心服务或导航,直接return true放行,彻底绝后患。但前提是:放行的同一行内必须更新
mPreviousLocation = location,否则会让多应用复用管理器的状态机彻底错乱。
十、总结:把这次排查浓缩成一张排查地图
scss
┌─ 一句话定位:哪一秒没收到 onLocationChanged?
│
├─ 看 logcat 框架层
│ └─ grep wukexiang-onReportLocation → 这秒 HAL 上报了吗?
│ 上报了 = 问题在 Framework 分发
│ 没上报 = 问题在 HAL/硬件/遮挡
│
├─ 看 xiangke-deliverOnLocationChanged 打印次数
│ ├─ 全员都拿到 = 没问题,这一秒没被拦
│ └─ 只有 1 个拿到 = 大部分监听被 filter 拦截 → 进入 acceptLocationChange
│
├─ 看 Bundle/satellites
│ ├─ satellites ≥ 4 && 经纬度完全一致 → too close (smallestDisplacementM)
│ │ [源码 Line 910-919]
│ ├─ satellites < 4 → 高精度"卫星数哨兵"过滤
│ └─ 看 et 间隔 < 900ms → too fast (节流抖动拦截)
│ [源码 Line 900-907]
│
└─ 看附近时间是否发生 gps provider added/removed → 重注册与状态机错位
[源码 onRegistrationReplaced Line 2085]
本次拍板的三条教训
-
4 层心智模型必须分清:
- 上报链路是
onReportLocation(Provider 层, Line 2518) →acceptLocationChange(Registration 层, Line 867) →deliverOnLocationChanged(Transport 层, Line 207) → 应用层onLocationChanged。 - 任何一层返回
false/null都会让应用层那一秒没回调,必须先确定是在哪一层 return 的。
- 上报链路是
-
拦截发生在
LocationProviderManager.LocationRegistration.acceptLocationChange()中的 Predicate 里 ,节流公式deltaMs < MinUpdateIntervalMillis - maxJitterMs是带弹性的(FASTEST_INTERVAL_JITTER_PERCENTAGE = 0.10f+MAX_FASTEST_INTERVAL_JITTER_MS = 30*1000封顶)。低频场景由 30 秒兜底,高频 1Hz 场景的宽容度完全是 0.10 给出的 100ms。 -
改任何 Predicate,状态机的"上一帧"
mPreviousLocation必须在return true之前更新 。否则mPreviousLocation永远是null,会让全部依赖getLastDeliveredLocation()的逻辑(Line 829、2089 等)误判这个监听器从未收到过数据,污染其他应用(被动共享、权限合并)。
落地修复方案优先级
| 优先级 | 方案 | 修复点 | 优劣 |
|---|---|---|---|
| ★★★★ | HAL 层时间平滑 | GnssLocationProvider JNI 上报时做最小间隔微调 |
治本,但需驱动团队配合 |
| ★★★★ | Framework 强力白名单 | test() 顶部对核心 App(j01app/deviceservice)直接放行并同步状态机 |
见效快,是车载场景的最终落地方案 |
| ★★★ | 关闭低卫星数过滤 | LocationProviderManager 中 if (satellites < 4) continue; 注释或降阈值 |
解决场景 A 现场一,适用于车机硬件天线较弱的应用场景 |
| ★★ | HAL 层伪造 satellites | reportLocation(...) 处把 satellites 改大成 10 |
应急造假,存在精度变差风险 |
| ★ | ADB 动态 prop | setprop log.tag.LocationProviderManager VERBOSE |
不修改逻辑,仅打开 Verbose 日志做诊断 |
十一、附录:关键代码与文件位置速查
11.1 过滤逻辑所在文件
bash
frameworks/base/services/core/java/com/android/server/location/provider/LocationProviderManager.java
frameworks/base/services/core/java/com/android/server/location/LocationManagerService.java
frameworks/base/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
11.2 快速定位自定义魔改日志
bash
grep -rn "xiangke-deliverOnLocationChanged" \
frameworks/base/services/core/java/com/android/server/location/
11.3 关键常量(Line 163, 166)
arduino
private static final double FASTEST_INTERVAL_JITTER_PERCENTAGE = 0.10f; // 10%
private static final long MAX_FASTEST_INTERVAL_JITTER_MS = 30 * 1000; // 30 秒
11.4 关键拦截公式(Line 894-921)
ini
long deltaMs = location.getElapsedRealtimeMillis()
- mPreviousLocation.getElapsedRealtimeMillis();
long maxJitterMs = Math.min(
(long)(FASTEST_INTERVAL_JITTER_PERCENTAGE * getRequest().getIntervalMillis()),
MAX_FASTEST_INTERVAL_JITTER_MS);
// too fast
if (deltaMs < getRequest().getMinUpdateIntervalMillis() - maxJitterMs) {
return false;
}
// too close
double smallestDisplacementM = getRequest().getMinUpdateDistanceMeters();
if (smallestDisplacementM > 0.0
&& location.distanceTo(mPreviousLocation) <= smallestDisplacementM) {
return false;
}
mPreviousLocation = location; // ★ 必须在 return true 前同步
return true;
11.5 关键修复模板
ini
@Override
public boolean test(Location location) {
// ★ 车机白名单:核心 App + 0,0 高频请求,直接放行并同步状态机
if (getRequest().getMinUpdateIntervalMillis() == 0
&& getRequest().getMinUpdateDistanceMeters() == 0
&& (mName.contains("j01app")
|| mName.contains("com.adaayo.aaop.deviceservice"))) {
mPreviousLocation = location; // ★ 必须同步状态机
return true;
}
if (mPreviousLocation == null) {
mPreviousLocation = location;
return true;
}
long deltaMs = location.getElapsedRealtimeMillis()
- mPreviousLocation.getElapsedRealtimeMillis();
long maxJitterMs = Math.min(
(long)(FASTEST_INTERVAL_JITTER_PERCENTAGE * getRequest().getIntervalMillis()),
MAX_FASTEST_INTERVAL_JITTER_MS);
if (deltaMs < getRequest().getMinUpdateIntervalMillis() - maxJitterMs) {
return false;
}
double smallestDisplacementM = getRequest().getMinUpdateDistanceMeters();
if (smallestDisplacementM > 0.0
&& location.distanceTo(mPreviousLocation) <= smallestDisplacementM) {
return false;
}
mPreviousLocation = location;
return true;
}
11.6 同名 / 易混函数对照表
| 名字 | 位置 | 说明 |
|---|---|---|
LocationProviderManager.onReportLocation |
Line 2518 | HAL → Framework 的入口(实现 AbstractLocationProvider.Listener) |
ILocationListener.onLocationChanged |
AIDL | 客户端接口(应用层最终回调) |
LocationListenerTransport.deliverOnLocationChanged |
Line 207 | Transport 层中转,调用 mListener.onLocationChanged(...) |
LocationPendingIntentTransport.deliverOnLocationChanged |
Line 272 | PendingIntent 方式 Transport |
GetCurrentLocationTransport.deliverOnLocationChanged |
Line 334 | 单次定位 Transport |
LocationRegistration.acceptLocationChange |
Line 867 | 持续监听按客户端做时间/位移节流的入口 |
GetCurrentLocationListenerRegistration.acceptLocationChange |
Line 1271 | 单次定位入口(不做节流) |
getPermittedLocationResult |
Line 2695 | 根据 PERMISSION_FINE/COARSE 决定原样或加扰 (LocationFudger) |
getLastDeliveredLocation / setLastDeliveredLocation |
Line 469 / 465 | 每个 Registration 的"上次成功分发"状态机 |
getLastLocationUnsafe |
Line 1791 | 读 provider 级别"最近一次定位"缓存 |
setLastLocation |
Line 1845 | 写 provider 级别缓存 |
onRegistrationReplaced |
Line 2085 | 重注册时把旧 last delivered 灌给新 Registration |
deliverToListeners |
父类 ListenerMultiplexer | 遍历所有 Registration 执行返回的 ListenerOperation |
executeOperation |
父类 / Registration.flush | 直接执行一个 ListenerOperation,用于历史 Location 透传 |
写在最后
车机 GNSS 场景下"丢一秒"看上去是应用层的偶发现象,背后往往是 Framework 节流阀与多客户端复用机制的"刻意为之"------原生 AOSP 给手机的 10% 抖动宽容,在车机有重注册 + 多客户端合并 + HAL 抖动叠加时,远远不够用。
理解了 4 层心智模型(onReportLocation → acceptLocationChange → deliverOnLocationChanged → onLocationChanged)、理解了 0, 0 在老接口黑盒里经历了什么、理解了 mPreviousLocation 状态机不能被打死这三件事,这次问题就不再玄学。
排查是一段对话导出的成果------但走通一遍后会发现,Android 团队留下的 10% 宽容不是太少,而是手机场景偏少。车机要稳定不丢数,最终只能用针对性白名单绕过这条节流,让 GNSS 的 1Hz 真正成为业务侧的 1Hz。