Android-休眠唤醒后onLocationChanged没有数据问题排查

车机休眠唤醒后 28 分钟onLocationChanged没有 数据上报?------记一次 Android GNSS 电源恢复缺陷的完整排查

一、问题现象

测试反馈的现象非常"诡异":

makefile 复制代码
15:19:53.729  D CarLocationService: Resume GNSS requests.     ← 车机唤醒,电源策略恢复 LOCATION
    ......(28 分钟)......
15:47:38.004  D GnssLocationProvider: startNavigating          ← GPS 引擎才真正启动
15:47:38.404  D GnssLocationProvider: TTFF: 398                ← 之后才有 GPS 数据

底层团队的反馈是"这段时间没打开 GPS"。唤醒后近 28 分钟才出定位,中间发生了什么?

二、先把时间线切准:28 分钟 ≠ 28 分钟异常

第一件事不是看代码,是把日志按分钟做密度统计(cut 出时间 + uniq -c),立刻发现 15:21~15:44 一行日志都没有------那 23 分钟 AP 在深睡(STR),属于正常,不算问题。真正的时间线切成三段:

时段 时长 发生了什么 性质
15:19:53 ~ 15:21:09 76 秒 唤醒#1:无用户操作的后台唤醒,Resuming after suspending → 策略切 all_on;随后因 display timeout 重新睡回去(Going to sleep due to display_groups_turned_off) 异常窗口①:醒着,GPS 未开
15:21:09 ~ 15:44:10 23 分钟 AP 深睡,日志完全空白 正常
15:44:10 ~ 15:47:38 3 分 28 秒 唤醒#2:亮屏、Launcher 到前台,车已起步行驶(车速 9.8→37 kph),GPS 仍无数据 异常窗口②:行车中,GPS 未开

所以真正要解释的是:醒着的两段时间里,为什么 GPS 引擎不开

三、唤醒后 framework 到底做了什么

CarLocationService 的电源策略监听器在 LOCATION 组件恢复时只做一件事:

scss 复制代码
// CarLocationService.java
boolean isOn = accumulatedPolicy.isComponentEnabled(PowerComponent.LOCATION);
if (isOn) {
    logd("Resume GNSS requests.");
    locationManager.setAutomotiveGnssSuspended(false);   // ← 只是解除挂起标志
}

这个调用在 GnssLocationProvider 里走到哪:

java 复制代码
// GnssLocationProvider.java (Android 14 原版)
public void setAutomotiveGnssSuspended(boolean suspended) {
    synchronized (mLock) {
        mAutomotiveSuspend = suspended;
    }
    mHandler.post(this::updateEnabled);       // ← 只 post 了 updateEnabled,到此为止
}
scss 复制代码
// GnssLocationProvider.java (Android 14 原版)
​
private void updateEnabled() {
      
        boolean enabled = false;
​
        // Generally follow location setting for visible users
        LocationManager locationManager = mContext.getSystemService(LocationManager.class);
        Set<UserHandle> visibleUserHandles =
                mContext.getSystemService(UserManager.class).getVisibleUsers();
        for (UserHandle visibleUserHandle : visibleUserHandles) {
            enabled |= locationManager.isLocationEnabledForUser(visibleUserHandle);
         
        }
​
        // .. but enable anyway, if there's an active bypass request (e.g. ELS or ADAS)
        enabled |= (mProviderRequest != null
                && mProviderRequest.isActive()
                && mProviderRequest.isBypass());
​
        // .. disable if automotive device needs to go into suspend
        synchronized (mLock) {
            enabled &= !mAutomotiveSuspend;
 
        }
 
        // ... and, finally, disable anyway, if device is being shut down
        enabled &= !mShutdown;
​
​
        if (enabled == isGpsEnabled()) {
            return;
        }
​
        if (enabled) {
            handleEnable();
            //restartLocationRequest();          //修改方案(原生代码没有)
        } else {
            handleDisable();
        }
    }
scss 复制代码
 // GnssLocationProvider.java (Android 14 原版)
 private void handleEnable() {
        if (DEBUG) Log.d(TAG, "handleEnable");
​
        boolean inited = mGnssNative.init();
​
        if (inited) {
            setGpsEnabled(true);
            mSupportsPsds = mGnssNative.isPsdsSupported();
​
            // TODO: remove the following native calls if we can make sure they are redundant.
            if (mSuplServerHost != null) {
                mGnssNative.setAgpsServer(GnssNetworkConnectivityHandler.AGPS_TYPE_SUPL,
                        mSuplServerHost, mSuplServerPort);
            }
            if (mC2KServerHost != null) {
                mGnssNative.setAgpsServer(GnssNetworkConnectivityHandler.AGPS_TYPE_C2K,
                        mC2KServerHost, mC2KServerPort);
            }
​
            mBatchingEnabled = mGnssNative.initBatching() && mGnssNative.getBatchSize() > 1;
            if (mGnssVisibilityControl != null) {
                mGnssVisibilityControl.onGpsEnabledChanged(/* isEnabled= */ true);
            }
        } else {
            setGpsEnabled(false);
            Log.w(TAG, "Failed to enable location provider");
        }
    }

updateEnabled()handleEnable()mGnssNative.init()(HAL 初始化、芯片上电),然后就没有然后了 。而真正让芯片开始定位的 startNavigating()(内部才会调 mGnssNative.start() 开会话)全部入口只有三个:

  1. updateRequirements() ------ 仅由 onSetRequest()(客户端注册变化)和 restartLocationRequest() 触发;
  2. onHalRestarted() ------ 仅 HAL 进程死亡重来时触发;
  3. onCapabilitiesChanged() ------ 仅 HAL 能力变化时触发。

"电源恢复"不在任何一条入口上。 这就是结构性缺口:唤醒路径只重建"使能",不重建"会话"。

四、应用 request 与系统的关系:注册层 vs 引擎层

这里有个常见误解要先厘清:应用"只调 onLocationChanged"------不对,onLocationChanged 是系统回调 应用的;应用主动做的唯一动作是注册 (requestLocationUpdates)。完整链路:

css 复制代码
app: requestLocationUpdates()  (binder)
  → LocationManagerService.registerLocationListener()        [binder 线程]
  → LocationProviderManager.registerLocationRequest()         [每个 provider 一个管理器]
      mergeRegistrations(): 把所有注册聚合成一个 ProviderRequest
  → setProviderRequest(request)
      日志: "gps provider request changed to ProviderRequest[... WorkSource{...}]"
  → AbstractLocationProvider.Controller.setRequest()
      mExecutor.execute(() -> onSetRequest(request))          [切到 FgThread]
  → GnssLocationProvider.onSetRequest()
  → updateRequirements() → startNavigating() → gnss start()   [芯片开会话]
  → 定位上报 → reportLocation → 按 binder 记录分发 → app 的 onLocationChanged()
scss 复制代码
[应用]                    [system_server 进程]                        [GNSS HAL/芯片 gps_hdbd]
     |                              |                                          |
     | ① requestLocationUpdates()   |                                          |
     |   (binder,带参数:精度/间隔)    |                                          |
     |─────────────────────────────▶| LocationManagerService                   |
     |                              |  登记一条注册记录(listener+参数+uid)       |
     |                              |  把所有注册聚合成 ProviderRequest          |
     |                              |────② onSetRequest(聚合请求)──▶ GnssLocationProvider
     |                              |        │ updateRequirements()            |
     |                              |        └─③ startNavigating()             |
     |                              |              └──────────────────────────▶| ④ gnss.start()
     |                              |                                          |    芯片开session,搜星
     |                              |                                          |         |
     |                              |◀──────────── ⑤ 定位结果上报 ─────────────|         |
     |                              | reportLocation → 按注册记录分发            |
     |◀── ⑥ binder 回调 onLocationChanged() ────|                              |

两个关键设计决定了本 bug:

  • 注册是一次性登记,不是心跳。应用注册一次后可以永远不再调任何 API,一直等回调;注册只在应用主动 remove、进程死亡、请求自然过期时消失。导航应用"初始化注册一次、长期监听"是完全合法的标准用法。
  • 系统没有对账机制。引擎的启停只由"注册集合变化"驱动(注册增删、定位开关、用户切换、亮灭屏重评估等事件),不存在"定期核对注册表与引擎状态是否一致"的机制。

用三层状态看 suspend/resume 动了什么,一目了然:

状态 休眠时 唤醒后
① LMS 注册表(应用的注册) 原样保留(STR 只冻结内存) 原样
② provider 的 mProviderRequest 原样保留(handleDisable 不清它) 原样,还是那条 active 请求
③ 引擎(使能 + 会话) 全部销毁 只重建了使能 (HAL init),会话没人开

唤醒后系统停在"①②说有客人等着,③门开了但没开始接待"的自相矛盾状态,直到某个应用碰一下注册表。

五、日志实锤

整个 84 分钟窗口内,LMS 侧的注册变化日志(gps provider request changed)总共只有 4 条:

vbscript 复制代码
14:22:04.292  gps provider request changed to ProviderRequest[HIGH_ACCURACY, WorkSource{10xxx com.xxx.navi}]
14:22:04.301  gps provider request changed to ProviderRequest[HIGH_ACCURACY, WorkSource{1000 com.xxx.deviceservice, 10xxx com.xxx.navi}]
      ......(跨两次深睡、两次唤醒,一条都没有)......
15:47:38.003  gps provider request changed to ProviderRequest[HIGH_ACCURACY, WorkSource{10xxx com.xxx.navi}]
15:47:38.013  gps provider request changed to ProviderRequest[HIGH_ACCURACY, WorkSource{1000 com.xxx.deviceservice, 10xxx com.xxx.navi}]

这说明三件事:

  1. 14:22:04 之后,两个应用的注册在 LMS 里一直活着 ------导航应用跨深睡零动作(它进程活着,一直在等 onLocationChanged,SDK 还在打车速日志),设备服务也没动;

  2. 15:47:38 那组间隔 10ms 的"先变小再变大"双连,是设备服务做了一次 removeUpdates + 重新 requestLocationUpdates 的注册刷新------正是这次碰巧的注册变化触发重投递 → onSetRequeststartNavigating,GPS 才恢复。救场的是设备服务,与导航应用无关;

    yaml 复制代码
    Line 170919: 09-17 15:47:38.004   911  1122 D GnssLocationProvider: setRequest ProviderRequest[@0, HIGH_ACCURACY, WorkSource{10060 com..navi.}]
        Line 170921: 09-17 15:47:38.004   911  1122 D GnssLocationProvider: stopBatching
        Line 170922: 09-17 15:47:38.004   911  1122 D GnssLocationProvider: startNavigating
        Line 170923: 09-17 15:47:38.005   911  1122 D GnssLocationProvider: setting position_mode to standalone
        Line 170966: 09-17 15:47:38.014   911  1122 D GnssLocationProvider: setRequest ProviderRequest[@0, HIGH_ACCURACY, WorkSource{1000 com.deviceservice, 10060 com.navi.app}]
        Line 170968: 09-17 15:47:38.014   911  1122 D GnssLocationProvider: stopBatching
scss 复制代码
 GnssLocationProvider.java
@Override
    public void onSetRequest(ProviderRequest request) {
        mProviderRequest = request;
        updateEnabled();
        updateRequirements();
    }
​
    // Called when the requirements for GPS may have changed
    private void updateRequirements() {
        if (mProviderRequest == null || mProviderRequest.getWorkSource() == null) {
            return;
        }
​
        if (DEBUG) Log.d(TAG, "setRequest " + mProviderRequest);
        if (mProviderRequest.isActive() && isGpsEnabled()) {
            // update client uids
            updateClientUids(mProviderRequest.getWorkSource());
​
            if (mProviderRequest.getIntervalMillis() <= Integer.MAX_VALUE) {
                mFixInterval = (int) mProviderRequest.getIntervalMillis();
            } else {
                Log.w(TAG, "interval overflow: " + mProviderRequest.getIntervalMillis());
                mFixInterval = Integer.MAX_VALUE;
            }
​
            int batchIntervalMs = max(mFixInterval, MIN_BATCH_INTERVAL_MS);
            long batchLengthMs = Math.min(mProviderRequest.getMaxUpdateDelayMillis(),
                    MAX_BATCH_LENGTH_MS);
​
            // apply request to GPS engine
            if (mBatchingEnabled && batchLengthMs / 2 >= batchIntervalMs) {
                stopNavigating();
                mFixInterval = batchIntervalMs;
                startBatching(batchLengthMs);
            } else {
                stopBatching();
​
                if (mStarted && mGnssNative.getCapabilities().hasScheduling()) {
                    // change period and/or lowPowerMode
                    if (!setPositionMode(mPositionMode, GNSS_POSITION_RECURRENCE_PERIODIC,
                            mFixInterval, mProviderRequest.isLowPower())) {
                        Log.e(TAG, "set_position_mode failed in updateRequirements");
                    }
                } else if (!mStarted) {
                    // start GPS
                    startNavigating();         //只有这里才会去真正打开Native gps
                } else {
                    // GNSS Engine is already ON, but no GPS_CAPABILITY_SCHEDULING
                    mAlarmManager.cancel(mTimeoutListener);
                    if (mFixInterval >= NO_FIX_TIMEOUT) {
                        // set timer to give up if we do not receive a fix within NO_FIX_TIMEOUT
                        // and our fix interval is not short
                        mAlarmManager.set(ELAPSED_REALTIME_WAKEUP,
                                SystemClock.elapsedRealtime() + NO_FIX_TIMEOUT, TAG,
                                mTimeoutListener, mHandler);
                    }
                }
            }
        } else {
            updateClientUids(new WorkSource());
            stopNavigating();
            stopBatching();
        }
    }
  1. 顺带一个细节:唤醒时亮屏了(15:44:10)但没有任何投递------证明亮屏只是"重评估",注册集合没变就不投递。

对"底层说没打开 GPS"的澄清 :日志显示唤醒后 GNSS 芯片其实在自主定位(第一次唤醒后 6 秒、第二次唤醒后 2 秒,守护进程就输出了有效 NMEA)。但 framework 没调 gnss start 开会话,HAL 就不会向 framework 上报 location------底层看到"无会话",上层看到"无数据",双方都没说错。使能 ≠ 会话

六、为什么偶现:缺陷必现,掩盖随机

这个缺陷在 framework 层面是 100% 必现的------只要"STR 唤醒 + 有注册存活",会话就一定不会自动重启。"有时正常"全靠这些救场者及时到场:

救场者 机制
用户上车打开导航 / 导航自动回前台 app onResume 重新 request → 秒级恢复(日常最常见的掩盖)
应用进程在停车期间被杀 binder 断 → 注册注销 → 唤醒后 app 重启必然重新注册(进程被杀反而掩盖缺陷,冻结存活反而暴露)
设备服务的注册刷新恰好早到 remove+request 触发重投递,空窗缩短到无感
HAL 恰好重启/能力重报 onHalRestarted/onCapabilitiesChanged 自带 restartLocationRequest()
冷启动上电 所有进程新起,必重新注册
根本没走 suspend 只熄屏或策略未关 LOCATION,会话从未停过

本案当天三个救场者集体缺席:导航一直在后台(前台是桌面/车控/倒车影像)、设备服务的刷新 3 分 28 秒后才来、HAL 全程没死(同一 pid 跨两次深睡)------才让缺陷完整暴露。这也解释了为什么常规测试几个月都"没问题":"上电→开导航→开车"的流程每一步都在掩盖它,最多表现为"偶尔定位慢几秒",没人深究。

一个可以复用的调试判据:每一条 startNavigating 的上方,必然是三选一------setRequest(某应用注册变化)、restartLocationRequest(HAL 重启/能力变化)、hibernate 唤醒闹钟;绝不会只跟在 Resume GNSS requests 后面。拿它去翻历史"正常"的日志,能立刻找出每次是哪个救场者在起作用。

七、上游对照:这是 AOSP 官方后来才修的坑

逐一对比各版本 AOSP 的 setAutomotiveGnssSuspended:

AOSP 版本 恢复(resume)时是否重放定位请求
Android 13(本例基线) 否------置标志 + post updateEnabled,与本项目代码逐字一致(非私改引入)
Android 15 / 16
Android 17(当前 main) ------新增 isExitingSuspend 判断,配合 Flags.gnssRestartLocationRequestOnResume(),在退出挂起且 GPS 使能时调 restartLocationRequest()
scss 复制代码
Android17源码
/**
     * Set whether the GnssLocationProvider is suspended. This method was added to help support
     * power management use cases on automotive devices.
     */
    public void setAutomotiveGnssSuspended(boolean suspended) {
        if (DEBUG) {
            Log.d(TAG, "setAutomotiveGnssSuspended: " + suspended);
        }
        boolean isExitingSuspend;
        synchronized (mLock) {
            isExitingSuspend = mAutomotiveSuspend && !suspended;
            mAutomotiveSuspend = suspended;
        }
        mHandler.post(() -> {
            updateEnabled();
            if (Flags.gnssRestartLocationRequestOnResume()) {
                // When exiting suspended mode, if GPS is enabled, restart the location request.
                if (isExitingSuspend && isGpsEnabled()) {
                    restartLocationRequest();
                }
            }
        });
    }
arduino 复制代码
Android14源码
/**
     * Set whether the GnssLocationProvider is suspended. This method was added to help support
     * power management use cases on automotive devices.
     */
    public void setAutomotiveGnssSuspended(boolean suspended) {
        synchronized (mLock) {
            mAutomotiveSuspend = suspended;
        }
        mHandler.post(this::updateEnabled);
    }
​

也就是说:手机产品线没有 automotive suspend 这条路径,而真车场景下"用户上车开导航"的 UX 习惯把缺陷长期掩盖,Google 自己也拖了三个大版本才在最新代码里补上(还挂在 feature flag 后面)。这是 AOSP 官方确认过的同一个坑。

八、修复

对齐上游思路,在 Android 13 树上 backport:

改动 1(核心):updateEnabled() 的"关→开"迁移分支重放现有请求

scss 复制代码
if (enabled == isGpsEnabled()) {
    return;                       // 这个守卫必须保留,否则每次请求刷新都会重启 session
}
​
if (enabled) {
    handleEnable();
    restartLocationRequest();     // 新增:重放仍 active 的 ProviderRequest
} else {
    handleDisable();
}

改动 2(配套清理):onHalRestarted() 删掉外层显式的 restartLocationRequest() (改动 1 生效后它会造成连续两次 startNavigating)。

scss 复制代码
 @Override
    public void onHalRestarted() {
        reloadGpsProperties();
        if (isGpsEnabled()) {
            setGpsEnabled(false);
            updateEnabled();
            //restartLocationRequest();  //注释这里
        }
​
        // Re-register network callbacks to get an update of available networks right away.
        synchronized (mLock) {
            if (mInitialized) {
                mNetworkConnectivityHandler.unregisterNetworkCallbacks();
                mNetworkConnectivityHandler.registerNetworkCallbacks();
            }
        }
    }

边界情况逐一核对过,全部安全:

  • 开机初始化路径会走一次 restart,但此时 mProviderRequest == null,updateRequirements() 第一行就 return;
  • 定位总开关 OFF→ON 路径:本就依赖 LMS 重投递,这里只是更及时,行为更正确;
  • handleEnable() 失败(init 返回 false)时,restartLocationRequest 走 else 分支只做清理,无错误状态;
  • 线程安全:onSetRequest、电源恢复、settings observer、HAL 回调全部在 FgThread 同一线程上顺序执行,setGpsEnabled(false) + updateEnabled() 的组合保证守卫必然不拦(先压 false 再算,transition 必然成立);
  • 与 Android 17 官方修法语义等价,只是挂的位置从 setAutomotiveGnssSuspended 挪到 updateEnabled 迁移分支,顺带把定位开关等同类"关→开"路径一起堵上。

应用侧不强制改动("注册一次长期监听"在 API 契约内无责);补丁铺开前如需止损,可加防御性重注册:预期有定位(车速>0/导航中)但 10 秒没收到回调时做一次 remove+request,或监听 CarPowerManagerSTATE_SUSPEND_EXIT 直接刷新注册。

九、验证

复现步骤(现在已成为回归用例):

  1. 打开导航,确认 GPS 正常出定位(注册已存在);
  2. 正常熄车,确认 CPMS 打出 send sleep entry(进 STR 深睡,不是只熄屏);
  3. 唤醒后不开导航,直接挂挡行车;
  4. 盯 logcat:未修复版本在 handleEnable 后长时间静默;补丁后紧跟:
vbnet 复制代码
CarLocationService: Resume GNSS requests.
GnssLocationProvider: handleEnable
GnssLocationProvider: 7updateEnabled--->true
GnssLocationProvider: restartLocationRequest      ← 补丁新增
GnssLocationProvider: setRequest ProviderRequest[... WorkSource{...}]   ← 重放的旧请求
GnssLocationProvider: startNavigating             ← 唤醒后立即出现
GnssLocationProvider: TTFF: xxx ms                ← 芯片本来就热,百毫秒级

HAL 重启路径 (root 下 kill -9 GNSS 守护进程模拟),期望:

vbnet 复制代码
gnss hal died - restarting shortly...
GnssLocationProvider: handleEnable
GnssLocationProvider: 7updateEnabled--->true
GnssLocationProvider: restartLocationRequest
GnssLocationProvider: startNavigating
GnssLocationProvider: TTFF: ...

两组序列均验证通过。

十、Takeaways

  1. 使能 ≠ 会话 。HAL init/芯片上电只是开门,gnss start 才是开始接待;排查"GPS 没数据"先分清在哪一层断了。
  2. 注册层和引擎层是分离的,联动只在注册变化时发生,没有对账机制------任何"外部原因关掉引擎"的场景(电源管理、HAL 崩溃)都要问一句:谁来重新接上联动?
  3. 日志锚点 :排这类问题盯三行------Resume GNSS requests(电源侧)、gps provider request changed(LMS 聚合侧)、setRequest/startNavigating(provider 侧)。引擎的动作永远跟在后两行后面;最后一行的时间减去前一行的时间,就是"等救场者"的时间。
  4. 先切时间线再下结论:一次日志密度统计(per-minute line count)就推翻了"28 分钟异常"的直觉,把问题收敛到 76 秒 + 3 分 28 秒两个真实窗口。
  5. 应用"注册一次躺着收"是合法设计,框架必须履约;但在缺陷修复铺开前,防御性重注册是合理的止损手段,定位是"临时缓解"而非应用认错。
  6. 测试盲区来自用户习惯:"上车开导航"的默认流程恰好掩盖了这类缺陷。回归用例要刻意走反直觉路径------"唤醒后不开导航直接行车"。
  7. 别急着怀疑自己的私改:先和上游逐版本 diff。本案代码与 AOSP 13 逐字一致,结论是官方缺陷(17 才修),直接 backport 官方修法最稳。
相关推荐
周GZ2 小时前
简单讲解线程池的4种拒绝策略
java
白远山2 小时前
24小时自助健身房系统开发实战:从需求分析到完整指南
java·开发语言·数据库·数据挖掘·需求分析
暮雨哀尘2 小时前
JAVA基础练习(四):SpringBoot 实现简易登录功能
java·spring boot·eclipse·maven
光依旧3 小时前
MCP实战手记系列(二):跑通第一个 MCP Server(文末附github源码链接)
java·spring ai·mcp·ai 开发·源码实战
白远山3 小时前
上海24小时自助健身房系统软件开发实战指南:从需求到部署
java·架构·uni-app·需求分析
MayBaymax3 小时前
Spring AI Alibaba Graph 快速上手:黑板、节点、边
java·spring·ai·ai编程
斑鸠喳喳4 小时前
可重入锁 ReentrantLock
java·源码
IT枫斗者枫哥4 小时前
Spring AI 聊天记忆落库:重启后,怎样接上上一轮对话
java