车机休眠唤醒后 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() 开会话)全部入口只有三个:
updateRequirements()------ 仅由onSetRequest()(客户端注册变化)和restartLocationRequest()触发;onHalRestarted()------ 仅 HAL 进程死亡重来时触发;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}]
这说明三件事:
-
14:22:04 之后,两个应用的注册在 LMS 里一直活着 ------导航应用跨深睡零动作(它进程活着,一直在等
onLocationChanged,SDK 还在打车速日志),设备服务也没动; -
15:47:38 那组间隔 10ms 的"先变小再变大"双连,是设备服务做了一次
removeUpdates+ 重新requestLocationUpdates的注册刷新------正是这次碰巧的注册变化触发重投递 →onSetRequest→startNavigating,GPS 才恢复。救场的是设备服务,与导航应用无关;yamlLine 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();
}
}
- 顺带一个细节:唤醒时亮屏了(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,或监听 CarPowerManager 的 STATE_SUSPEND_EXIT 直接刷新注册。
九、验证
复现步骤(现在已成为回归用例):
- 打开导航,确认 GPS 正常出定位(注册已存在);
- 正常熄车,确认 CPMS 打出
send sleep entry(进 STR 深睡,不是只熄屏); - 唤醒后不开导航,直接挂挡行车;
- 盯 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
- 使能 ≠ 会话 。HAL init/芯片上电只是开门,
gnss start才是开始接待;排查"GPS 没数据"先分清在哪一层断了。 - 注册层和引擎层是分离的,联动只在注册变化时发生,没有对账机制------任何"外部原因关掉引擎"的场景(电源管理、HAL 崩溃)都要问一句:谁来重新接上联动?
- 日志锚点 :排这类问题盯三行------
Resume GNSS requests(电源侧)、gps provider request changed(LMS 聚合侧)、setRequest/startNavigating(provider 侧)。引擎的动作永远跟在后两行后面;最后一行的时间减去前一行的时间,就是"等救场者"的时间。 - 先切时间线再下结论:一次日志密度统计(per-minute line count)就推翻了"28 分钟异常"的直觉,把问题收敛到 76 秒 + 3 分 28 秒两个真实窗口。
- 应用"注册一次躺着收"是合法设计,框架必须履约;但在缺陷修复铺开前,防御性重注册是合理的止损手段,定位是"临时缓解"而非应用认错。
- 测试盲区来自用户习惯:"上车开导航"的默认流程恰好掩盖了这类缺陷。回归用例要刻意走反直觉路径------"唤醒后不开导航直接行车"。
- 别急着怀疑自己的私改:先和上游逐版本 diff。本案代码与 AOSP 13 逐字一致,结论是官方缺陷(17 才修),直接 backport 官方修法最稳。