Android-车机 GNSS 定位数据接收问题排查

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 之后经历了较大重构。过滤逻辑位于:

  1. frameworks/base/services/core/java/com/android/server/location/provider/LocationProviderManager.java(核心分发类,本文重点)
  2. 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_CHANGEDlocation)和 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 lock
  • operate(listener) --- 在 executor 上跑:触发 deliverOnLocationChanged
  • onPostExecute(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 < 4hAcc 不满足要求时,会直接 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 三类上游成因回顾

完整的"为什么丢一秒"体系,从源头看:

  1. 硬件限制:手机/车机 GNSS 芯片硬件刷新率上限通常就是 1Hz(1Hz=1秒一次),5Hz/10Hz 是高频模组。

  2. 时间对齐误差:如果代码强依赖"严格每秒整点回调",由于硬件计算耗时 + 线程调度延迟,实际间隔可能是 1.1s/1.2s,会导致某一物理秒内没有回调。

  3. 定位请求参数

    • setIntervalMillis():期望间隔,设为 1000ms 系统会尽量但不保证严格每秒。
    • setMinUpdateIntervalMillis():最快间隔,设得太接近 1000ms 会把微小延迟压成合并。
    • setMinUpdateDistanceMeters()本次重点排查项,设了一段位移阈值,静止时就不回调。
  4. 卫星信号遮挡:"城市峡谷"、室内、高架桥、树荫下卫星信号瞬间被遮挡,芯片解算不出准确位置会直接丢弃这一帧。

  5. 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=1000msMinUpdateInterval=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(如 @EA6A8991A3101378ACD8D13)。原因是车机导航为了平滑通常会注册多个监听器(一个画车标,一个惯导推算,一个电子地平线),它们全部失血。这一秒全车机只有一个无名客户端(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));

你将看到以下两种结果之一:

  1. getMinUpdateIntervalMillis() 根本不是 0,而是被系统偷偷改成了 1000;
  2. 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]

本次拍板的三条教训

  1. 4 层心智模型必须分清

    • 上报链路是 onReportLocation(Provider 层, Line 2518) → acceptLocationChange(Registration 层, Line 867) → deliverOnLocationChanged(Transport 层, Line 207) → 应用层 onLocationChanged
    • 任何一层返回 false/null 都会让应用层那一秒没回调,必须先确定是在哪一层 return 的。
  2. 拦截发生在 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。

  3. 改任何 Predicate,状态机的"上一帧"mPreviousLocation 必须在 return true 之前更新 。否则 mPreviousLocation 永远是 null,会让全部依赖 getLastDeliveredLocation() 的逻辑(Line 829、2089 等)误判这个监听器从未收到过数据,污染其他应用(被动共享、权限合并)。

落地修复方案优先级

优先级 方案 修复点 优劣
★★★★ HAL 层时间平滑 GnssLocationProvider JNI 上报时做最小间隔微调 治本,但需驱动团队配合
★★★★ Framework 强力白名单 test() 顶部对核心 App(j01app/deviceservice)直接放行并同步状态机 见效快,是车载场景的最终落地方案
★★★ 关闭低卫星数过滤 LocationProviderManagerif (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 层心智模型(onReportLocationacceptLocationChangedeliverOnLocationChangedonLocationChanged)、理解了 0, 0 在老接口黑盒里经历了什么、理解了 mPreviousLocation 状态机不能被打死这三件事,这次问题就不再玄学。

排查是一段对话导出的成果------但走通一遍后会发现,Android 团队留下的 10% 宽容不是太少,而是手机场景偏少。车机要稳定不丢数,最终只能用针对性白名单绕过这条节流,让 GNSS 的 1Hz 真正成为业务侧的 1Hz。

相关推荐
蔬菜_1 小时前
前端转全栈-day5(数组、list、set)
java·前端·数据结构·list
萧瑟余晖1 小时前
Java深入解析篇二十二之虚拟线程
java·开发语言
OuO-21 小时前
笔试强训 Day 34:ISBN 号码、kotori 和迷宫、矩阵最长递增路径
java·算法·矩阵
油丶酸萝卜别吃2 小时前
Java 集合类全景介绍
java·开发语言
钱栈up2 小时前
"Flowable 工作流引擎进阶实战(高级篇):任务分配、流程变量与监听器"
java
豆沙沙包?2 小时前
c++中引用(P7-P11)
java·c++·算法
wuminyu2 小时前
虚拟线程底层ForkJoinPool的工作窃取算法机制
java·linux·c语言·jvm·c++
掉鱼的猫3 小时前
Solon 的日志设计哲学:不只是用 SLF4J 做统一门面
java
szephyr3 小时前
腾讯云 ADP 智能体的 Skills 配置变更后没有走灰度,直接全量替换出了问题怎么定位?
java·前端·腾讯云·腾讯云adp