ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

车机Android STR唤醒黑屏冻屏问题排查与遮罩机制分析

车机Android STR唤醒黑屏冻屏问题排查与遮罩机制分析 1. 从一次冬季早晨的冻屏说起去年冬天我接手了一个车机项目的问题排查。用户反馈的现象很统一早上冷启动车辆中控屏幕亮起来之后画面就卡在开机Logo或者最后一个界面不动了触摸没反应倒车影像也出不来得等两三分钟甚至更久才恢复正常。北方的用户投诉尤其集中南方偶尔也有但频率低很多。这个现象在车机行业里有个约定俗成的叫法——STR唤醒黑屏。STR是Suspend to RAM的缩写中文一般叫挂起到内存或者休眠唤醒。车机不像手机熄火之后不能直接断电关机因为用户下次上车希望中控屏能秒亮倒车影像要立刻可用。所以主流方案是熄火后让系统进入STR低功耗状态内存供电保持CPU和外设大部分断电下次点火时快速恢复现场。理论上这个过程应该在几百毫秒到一两秒内完成但实际项目里STR唤醒阶段的黑屏、冻屏、闪屏问题几乎是每个车机团队都会踩的坑。我拿到日志之后第一反应是去看唤醒时序。但很快发现一个有意思的事情用户看到的冻屏其实是被遮罩盖住了。系统在STR唤醒过程中会先显示一个开机动画或者黑屏遮罩等SurfaceFlinger把各个图层准备好之后再撤掉遮罩。用户看到的黑屏很多时候不是屏幕真的没输出而是遮罩层一直没被移除。而系统不给闪屏这个现象根因也查清楚了——是显示管线的某个环节在唤醒时没有正确恢复状态导致系统认为显示还没就绪于是拒绝提交新的帧。这篇文章就把这次复盘完整写下来。治本的两步最终没走通但教训留下来了。如果你也在做车机Android系统开发或者正在被STR唤醒相关的显示问题折磨这篇内容应该能帮你少走一些弯路。2. 车机STR唤醒的整体设计与思路拆解2.1 为什么车机非要用STR而不是直接关机先把这个前提说清楚不然后面的很多设计决策没法理解。车机和手机、平板最大的区别在于使用场景的连续性。用户上车、点火、挂倒挡这三个动作可能在两秒内完成。如果系统这时候还在走完整的冷启动流程倒车影像根本来不及出来这是安全问题不是体验问题。STR的核心价值就是保留内存现场。Android系统冷启动要重新加载Zygote、SystemServer、各种HAL服务整个过程在车机这种算力有限的平台上通常要十几秒甚至更久。而STR唤醒只需要恢复CPU上下文、重新初始化必要的外设、让各个服务从挂起状态恢复理论上可以做到一秒以内。但理论上这三个字在车机项目里往往意味着大量的工程妥协。内存保持供电意味着功耗不能做到零车辆长时间停放会导致蓄电池亏电所以很多项目会设置一个STR超时时间超过之后转成真正的关机。外设断电再上电的过程各个芯片的初始化时序、寄存器状态恢复、通信链路重建每一个环节都可能出问题。2.2 显示子系统在STR唤醒中的特殊地位显示子系统在STR流程里是最特殊的一个。其他外设比如音频、蓝牙、CAN唤醒慢一点用户可能感知不强但显示是用户第一眼就看到的东西。屏幕不亮用户就会认为车机坏了。Android的显示管线大致是这样的应用通过SurfaceFlinger提交图层SurfaceFlinger合成之后通过HWC交给显示驱动显示驱动再通过MIPI DSI或者LVDS把画面送到屏幕。STR唤醒时这条链路上的每一环都需要恢复Display驱动需要重新初始化时序、恢复PLL配置、重新建立与屏幕的通信HWC需要重新获取显示设备句柄、恢复图层合成配置SurfaceFlinger需要重新创建显示设备、恢复合成状态WindowManager需要重新计算窗口布局、恢复各个窗口的可见性任何一环没恢复到位SurfaceFlinger就可能认为显示设备不可用从而拒绝提交帧。这时候用户看到的就是黑屏或者冻屏。2.3 遮罩机制的设计意图与实际副作用车机系统在STR唤醒时会显示一个遮罩层这个设计的初衷是好的在显示管线还没完全恢复之前用一个静态画面盖住屏幕避免用户看到花屏、闪屏或者不完整的界面。等系统认为显示就绪之后再撤掉遮罩显示真正的桌面或者倒车影像。但问题就出在系统认为显示就绪这个判断上。如果判断条件过于严格或者某个状态标志没有被正确清除遮罩就会一直挂着。用户看到的是黑屏但实际上屏幕是有输出的只是输出的是遮罩层的内容。这就是我这次排查中发现的第一个关键点用户看到的冻屏是被遮罩盖住了。这个发现很重要因为它把问题从显示完全没工作缩小到了遮罩撤除逻辑有问题。排查方向一下子清晰了很多。3. 核心细节解析与实操要点3.1 STR唤醒的完整时序拆解要定位问题首先得把STR唤醒的时序搞清楚。我在项目里抓了一段正常的唤醒日志把关键节点标出来# 内核层STR唤醒起点 [ 12.345678] PM: suspend exit [ 12.345680] PM: resume from suspend # 显示驱动恢复 [ 12.350123] disp: resume start [ 12.352456] disp: pll restored [ 12.355789] disp: dsi phy ready [ 12.358012] disp: first frame sent # HWC恢复 [ 12.360234] hwc: display device reconnected [ 12.362345] hwc: layer stack restored # SurfaceFlinger恢复 [ 12.365678] SF: display device added [ 12.368901] SF: composition resumed # 遮罩撤除 [ 12.370123] bootanim: exit requested [ 12.372456] SF: boot animation stopped正常情况下的时序是内核先恢复然后显示驱动恢复并送出第一帧接着HWC重新连接显示设备SurfaceFlinger恢复合成最后撤除遮罩。整个过程在几十毫秒内完成。但出问题的日志里时序是这样的[ 12.345678] PM: suspend exit [ 12.350123] disp: resume start [ 12.352456] disp: pll restored [ 12.355789] disp: dsi phy ready [ 12.358012] disp: first frame sent [ 12.360234] hwc: display device reconnected [ 12.362345] hwc: layer stack restored [ 12.365678] SF: display device added [ 12.368901] SF: composition resumed # 然后就没有然后了遮罩撤除的日志一直没出现遮罩撤除的日志缺失说明系统认为显示还没就绪。但显示驱动和HWC的日志都显示恢复了问题出在SurfaceFlinger或者更上层的判断逻辑上。3.2 遮罩撤除的判断条件分析Android的遮罩撤除逻辑主要在BootAnimation和SurfaceFlinger里。BootAnimation负责播放开机动画播放结束后会通知SurfaceFlinger停止遮罩。SurfaceFlinger收到通知后会检查当前显示设备的状态如果认为显示就绪就撤除遮罩。判断显示就绪的条件通常包括显示设备已经添加并且处于活跃状态至少有一帧成功提交并显示没有待处理的显示配置变更我查了SurfaceFlinger的代码发现它在STR唤醒后会等待一个display ready的信号。这个信号由HWC在完成显示设备恢复后发出。但问题在于HWC发出信号的时机和SurfaceFlinger等待的时机之间有一个竞态条件。具体来说HWC在恢复显示设备后立即发出信号但SurfaceFlinger可能还没完成自己的恢复流程导致信号被错过。SurfaceFlinger会一直等这个信号遮罩就一直不撤。3.3 系统不给闪屏的根因定位系统不给闪屏这个现象指的是在STR唤醒过程中系统拒绝提交新的帧。这个问题的根因和遮罩问题是相关的但又不完全一样。我抓了一段SurfaceFlinger的trace发现它在STR唤醒后会进入一个skip composition的状态。这个状态的设计意图是在显示管线还没完全恢复之前跳过合成避免提交无效帧导致花屏。但问题在于这个状态的退出条件同样依赖于display ready信号。由于信号被错过SurfaceFlinger一直停留在skip composition状态所有新的帧提交都被拒绝。这就是系统不给闪屏的根因。注意这个竞态条件在冷启动时不会出现因为冷启动的时序是串行的HWC和SurfaceFlinger的初始化顺序是确定的。但STR唤醒是并行的各个模块同时恢复时序不确定所以才会出现信号错过的问题。4. 实操过程与核心环节实现4.1 问题复现与日志抓取要解决这个问题首先得能稳定复现。我在实验室里搭了一套环境用可编程电源模拟车辆点火信号控制车机进入STR和唤醒。复现步骤如下车机正常启动进入桌面通过ADB发送命令让系统进入STRecho mem /sys/power/state等待10秒确保系统完全进入STR通过电源控制模拟点火唤醒系统观察屏幕状态同时抓取kernel log和logcat复现的成功率大概在30%左右不是每次都能触发。这说明竞态条件的窗口很窄需要多次尝试才能抓到。抓日志的命令# 抓kernel log adb shell dmesg kernel.log # 抓logcat adb logcat -b all -v threadtime logcat.log # 抓SurfaceFlinger trace adb shell dumpsys SurfaceFlinger sf.log # 抓HWC状态 adb shell dumpsys hwc hwc.log4.2 关键状态标志的追踪在日志里我重点追踪了几个状态标志标志名称正常值异常值含义display_ready10显示设备是否就绪skip_composition01是否跳过合成bootanim_exit10遮罩是否已撤除hwc_connected11HWC是否已连接从日志里可以看到异常情况下display_ready一直是0skip_composition一直是1bootanim_exit一直是0。但hwc_connected是1说明HWC已经恢复了。这就验证了我的判断HWC恢复了但SurfaceFlinger没有收到display_ready信号。4.3 竞态条件的代码级分析我查了HWC和SurfaceFlinger的代码找到了信号传递的实现。HWC在恢复显示设备后会通过一个回调通知SurfaceFlinger// HWC侧代码 void HwcDisplay::onResume() { // 恢复显示设备 restoreDisplayDevice(); // 通知SurfaceFlinger if (mCallback) { mCallback-onDisplayReady(mDisplayId); } }SurfaceFlinger侧// SurfaceFlinger侧代码 void SurfaceFlinger::onDisplayReady(DisplayId displayId) { std::lock_guardstd::mutex lock(mStateLock); mDisplayReady true; mCondition.notify_all(); }问题在于HWC的回调是在HWC的线程里执行的而SurfaceFlinger的等待是在另一个线程里。如果HWC的回调在SurfaceFlinger开始等待之前就执行了那么mDisplayReady会被设置为true但SurfaceFlinger可能还没进入等待状态或者等待的条件变量已经被重置了。更具体地说SurfaceFlinger在STR唤醒后会重置mDisplayReady为false然后开始等待。如果HWC的回调在这个重置之前执行那么mDisplayReady会被重置为falseSurfaceFlinger就会一直等下去。4.4 临时规避方案的实施找到根因之后我先做了一个临时规避方案在SurfaceFlinger的等待逻辑里加一个超时。如果等待超过500毫秒还没收到信号就强制认为显示就绪撤除遮罩。// 临时方案加超时 bool SurfaceFlinger::waitForDisplayReady() { std::unique_lockstd::mutex lock(mStateLock); auto timeout std::chrono::milliseconds(500); if (mCondition.wait_for(lock, timeout, [this] { return mDisplayReady; })) { return true; } // 超时强制认为就绪 ALOGW(Display ready timeout, force ready); mDisplayReady true; return true; }这个方案实测下来很稳复现率从30%降到了0。但我知道这不是治本因为超时时间设成多少合适设短了可能误判设长了用户体验还是不好。而且这个方案掩盖了真正的竞态问题以后可能会在其他场景下暴露出来。5. 治本的两步为什么没走通5.1 第一步修复信号传递的竞态条件治本的第一步应该是修复HWC和SurfaceFlinger之间的信号传递竞态。正确的做法是让信号的设置和等待在同一个锁的保护下并且确保重置和等待的顺序是确定的。我尝试的修改方案是在SurfaceFlinger重置mDisplayReady之前先获取HWC的当前状态。如果HWC已经就绪就直接设置mDisplayReady为true不再等待。// 治本方案第一步 void SurfaceFlinger::onResume() { std::lock_guardstd::mutex lock(mStateLock); // 先查询HWC状态 if (mHwc-isDisplayReady(mDisplayId)) { mDisplayReady true; } else { mDisplayReady false; // 开始等待 } }但这个方案有个问题isDisplayReady这个接口在HWC里没有现成的实现需要新增。而且HWC的状态查询本身也可能有竞态因为HWC的恢复是异步的。我尝试在HWC里加了这个接口但测试发现在某些情况下HWC的恢复还没完成isDisplayReady返回falseSurfaceFlinger开始等待然后HWC恢复完成发出信号但信号又因为锁的问题被错过了。问题只是从总是错过变成了偶尔错过。5.2 第二步统一显示就绪的判断标准治本的第二步应该是统一整个系统里显示就绪的判断标准。目前的问题是HWC、SurfaceFlinger、BootAnimation各自有各自的判断逻辑标准不统一导致状态不一致。我设想的方案是定义一个全局的显示状态机所有模块都从这个状态机里读取状态而不是各自维护自己的标志。// 治本方案第二步全局显示状态机 class DisplayStateMachine { public: enum State { DISPLAY_OFF, DISPLAY_RESUMING, DISPLAY_READY, DISPLAY_ERROR }; void setState(State state) { std::lock_guardstd::mutex lock(mLock); mState state; mCondition.notify_all(); } State getState() { std::lock_guardstd::mutex lock(mLock); return mState; } bool waitForReady(std::chrono::milliseconds timeout) { std::unique_lockstd::mutex lock(mLock); return mCondition.wait_for(lock, timeout, [this] { return mState DISPLAY_READY || mState DISPLAY_ERROR; }); } private: State mState DISPLAY_OFF; std::mutex mLock; std::condition_variable mCondition; };这个方案理论上可以彻底解决问题因为所有模块都从同一个状态机读取状态不存在状态不一致的问题。但实施起来工作量很大需要修改HWC、SurfaceFlinger、BootAnimation等多个模块而且需要重新设计模块间的接口。5.3 为什么最终没有实施治本方案治本方案没走通原因有几个时间窗口不够。这个项目已经到了量产前的最后阶段任何大的修改都需要重新做完整的回归测试时间上不允许。风险太高。修改HWC和SurfaceFlinger的核心逻辑影响面太大可能会引入新的问题。而且这两个模块的代码复杂度很高修改之后很难保证没有遗漏。收益不明显。临时规避方案已经能把复现率降到0用户体验上已经没问题了。治本方案的收益主要是代码质量上的对于项目交付来说优先级不高。依赖上游。HWC的代码有一部分是芯片厂商提供的我们只能改自己维护的部分芯片厂商的那部分改不了。统一状态机的方案需要芯片厂商配合协调成本太高。所以最终的决定是临时规避方案上线治本方案记录在案留给下一个项目或者后续的版本迭代。6. 常见问题与排查技巧实录6.1 STR唤醒黑屏问题速查表现象可能原因排查方法解决思路屏幕完全无输出显示驱动未恢复查kernel log里disp resume日志检查显示驱动STR回调屏幕有背光但无画面遮罩未撤除查bootanim exit日志检查遮罩撤除条件画面卡住不动SurfaceFlinger跳过合成查skip composition标志检查display ready信号唤醒后闪屏显示时序未恢复查PLL和DSI配置恢复显示时序参数倒车影像出不来显示管线未就绪查HWC连接状态检查HWC恢复流程6.2 排查STR问题的通用思路排查STR相关问题我总结了一个通用的思路先分层再定位。STR涉及内核、HAL、Framework、应用多个层次不要一上来就盯着最上层看。先从内核log确认STR是否正常进入和退出再看各个HAL的恢复日志最后看Framework层的状态。先时序再状态。STR问题的核心往往是时序问题。把正常和异常的时序图都画出来对比关键节点的先后顺序往往能发现竞态条件。先复现再分析。STR问题很多是概率性的不能稳定复现就很难分析。想办法提高复现率比如控制温度、控制电源时序、多次尝试。先规避再治本。项目时间紧的时候先上规避方案保证交付治本方案可以后续再做。但规避方案一定要记录清楚不能让它变成技术债。6.3 几个容易踩的坑坑一只看logcat不看kernel log。STR的很多问题出在内核层logcat里看不到。一定要同时抓kernel log。坑二忽略温度影响。低温下STR问题更容易复现因为芯片的初始化时序会变慢。实验室复现的时候要注意控制温度。坑三以为改了就能好。STR问题的修改往往需要多次迭代改一次不一定能完全解决。要有耐心每次改完都要做完整的回归测试。坑四忽略芯片厂商的差异。不同芯片厂商的STR实现差异很大别人的解决方案不一定适用于你的平台。要结合自己平台的实际情况来分析。坑五不记录规避方案。临时规避方案如果不记录过一段时间就忘了后面的人接手会一头雾水。一定要在代码里加注释说明这是临时方案以及治本方案应该怎么做。6.4 实操心得分享这次排查下来我最大的心得是STR问题不要只盯着显示看。显示是最终表现但根因可能在电源管理、时钟管理、甚至通信链路上。我这次的问题表面上是显示问题实际上是HWC和SurfaceFlinger之间的信号传递问题再往深了说是STR唤醒时各模块并行恢复导致的时序问题。另一个心得是日志要抓全。我一开始只抓了logcat看了半天没头绪。后来把kernel log、SurfaceFlinger dump、HWC dump都抓了对比着看很快就定位到了问题。还有一个心得是不要怕临时方案。临时方案不是坏事关键是要清楚它是临时的并且记录好治本方案。项目交付是第一位的技术债可以后续还。7. 这个问题的后续扩展与思考虽然治本方案没走通但这个问题给我留下了很多思考。STR唤醒的时序问题不是个例在车机行业里普遍存在。随着车机功能越来越复杂参与STR唤醒的模块越来越多时序问题只会更严重。我觉得后续可以从几个方向去改进建立STR唤醒的时序规范。定义清楚各个模块的恢复顺序和依赖关系避免并行恢复导致的竞态。引入统一的电源状态管理。让所有模块都从一个统一的电源状态机读取状态而不是各自维护自己的标志。加强STR的自动化测试。在实验室里用自动化设备反复做STR唤醒测试提高问题的复现率和发现率。和芯片厂商建立更紧密的合作。很多STR问题需要芯片厂商配合才能解决建立良好的沟通渠道很重要。这次的问题虽然治本方案没走通但至少把根因查清楚了临时方案也保证了项目交付。教训留下来下一个项目就能少踩一些坑。如果你也在做车机STR相关的开发希望这篇复盘能给你一些参考。
返回列表