十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

安卓虚拟机显示驱动 property 排障:从黑屏到 HWC/EGL 加载链路

安卓虚拟机显示驱动 property 排障:从黑屏到 HWC/EGL 加载链路 1. 从模拟器开机卡在黑屏说起一条 property 如何决定显示能不能起来前段时间调一个安卓虚拟机的显示问题现象特别典型模拟器能开机、能adb shell、logcat也能刷出日志但屏幕一直停在开机动画或者干脆黑屏只有背光。logcat里 SurfaceFlinger 反复重启报的错大概长这样E SurfaceFlinger: Failed to load hwcomposer module E HWComposer: loadHwcModule: Failed to load module F DEBUG : Abort message: couldnt find an EGLConfig matching the screen format折腾了半天最后定位到的原因既不玄乎也不底层ro.hardware.hwcomposer这个 property 指向的 HAL 变体名和虚拟机镜像里/vendor/lib64/hw/目录下实际存在的那个.so名字对不上差了一个字符。这件事让我意识到安卓虚拟机显示驱动 property这串东西看着像一堆零散的keyvalue实际上是安卓显示栈的接线图。安卓的图形显示链路不是一个巨大的一体化程序而是一串按名字插拔的模块应用把图层交给 SurfaceFlingerSurfaceFlinger 通过 HWCHardware Composer做合成用 gralloc 分配图形缓冲区用 EGL/Vulkan 把东西真正画出来。这四个环节每一个环节用哪个实现都是靠一串ro.hardware.*属性在开机时决定的。为什么安卓要搞成属性驱动而不是写死因为同一份系统镜像要跑在手机、平板、车机、电视盒子和虚拟机模拟器上。硬件差异不可能都编进代码必须外置成配置让 init 在启动阶段读进来再动态拼出.so的文件名去dlopen。这串属性就是安卓硬件描述语言的入口而显示驱动相关的那几条是入口里最关键的一类。这篇文章我会把显示驱动相关的 property 拆开讲清楚它从哪来、谁在读、写错了会怎么炸、以及怎么用几条命令快速判断虚拟机当前到底走了哪条渲染路径。适合正在折腾安卓模拟器、虚拟机镜像、定制 ROM 或者做显示问题定位的同学有安卓 HAL 基础会读得更顺但没基础也能跟着命令一步步走下来。2. property 在安卓里到底是怎么被读出来的2.1 property 不是文件是一块 mmap 出来的共享内存很多人第一次接触时会以为 property 存在某个文件里改文件就生效。实际上不是。安卓的属性系统在 init 启动的极早期就初始化了一块共享内存区域路径在/dev/__properties__/下面。属性读取走的是 mmap 直读几乎零开销属性写入则要走/dev/socket/property_service这个 socket 交给 init 处理是串行化的。这个设计直接决定了两件事。第一属性读取非常便宜系统里到处property_get也没关系所以显示、音频、相机各种地方都敢在热路径上读属性。第二属性写入比较贵不要在高频循环里反复setprop尤其是调试脚本里那种每帧改一个属性的写法会把 property_service 拖住。还有两个硬限制值得记一下属性名最长 32 字节属性值最长 92 字节PROP_VALUE_MAX。所以别指望往属性里塞一长串 GPU 参数或者复杂的逗号分隔列表塞不进去。更麻烦的是超长的名字被截断时不会有明确报错你只会看到属性莫名其妙设不上然后花半天怀疑人生。我踩过一次一个带前缀的调试属性名写了 34 个字符怎么设都设不进去getprop里连影子都没有。2.2 谁在什么时候把 ro.hardware.* 写进去ro.hardware这一类属性的来源有好几条路按加载时间排序大概是init 最早读bootconfig安卓 10 以后是/proc/bootconfig之前是/proc/cmdline把androidboot.xxxyyy映射成ro.boot.xxx。然后按顺序加载各个分区的build.prop/system/build.prop、/system_ext/build.prop、/vendor/build.prop、/odm/etc/build.prop、/product/build.prop。老版本还有/system/etc/prop.default和/default.prop。接着执行 init 脚本*.rc里的setprop以及property_override指令。最后 SurfaceFlinger 等进程启动时去读这些已经定好的值。注意顺序很重要后加载的会覆盖先加载的但只对非ro.属性成立。ro.前缀是一次性写入谁先写成谁赢。所以如果你在vendor/build.prop里写了ro.hardware.egl而 init 的early-init阶段已经在init.ranchu.rc里设过同一个名字那你的 build.prop 是白写的而且不会有任何警告。2.3 所谓只读的 ro. 到底有多只读ro.属性的只读不是文件系统权限只读而是 property_service 层面的规则只有 init 域以及被 SELinux 策略放行的上下文能写ro.前缀的属性并且一个ro.属性在整个开机生命周期里只能被成功写入一次。直接后果就是adb shell setprop ro.hardware.egl emulation # Failed to set property ro.hardware.egl to emulation这条命令在绝大多数情况下会失败。哪怕你已经adb root、甚至把 SELinux 设成 permissivero.属性也设不进去——因为拦你的不是 SELinux是 property_service 里ro.只允许写一次的硬逻辑。真正能改它的只有在它被第一次写入之前介入也就是改 build.prop 或者改 init 脚本。相比之下debug.*和persist.*就友好得多。debug.*是运行时可写的调试开关persist.*会落到/data/property/目录下持久化跨重启保留。所以做显示调试时凡是能用debug.sf.*、debug.hwui.*表达的需求都别去动ro.属性。2.4 SELinux 给每条属性都贴了标签还有一层很多人忽略的东西property_contexts。安卓 8 以后属性访问是受 SELinux 管控的每条属性必须先在plat_property_contexts、vendor_property_contexts这类文件里有匹配规则否则写入会被拒logcat里出现avc: denied才算暴露出来。典型场景是你自己造了一条新属性persist.vendor.display.foo在init.rc里setprop失败日志里却只有一句含糊的错误。这时候要去确认vendor_property_contexts里有没有persist.vendor.display.这个前缀的规则。加规则本身不难难的是不知道有这回事会误以为是属性名写错了。3. ro.hardware.hwcomposer 这个典型样本的完整生命周期3.1 它的值是怎么被消费掉的拿ro.hardware.hwcomposer当样本因为它是显示驱动属性里最有代表性的一条。传统 HAL 加载逻辑大致是这样调用方传一个模块类名比如hwcomposer加载器先拼一个属性名ro.hardware.类名去查查到了就用这个值当变体后缀拼出hwcomposer.变体.so查不到就退回全局的ro.hardware两个都没有就走hwcomposer.default.so。也就是说这里有一个三级回退链优先级属性拼出的模块名说明1ro.hardware.hwcomposerhwcomposer.值.so精确指定最常用2ro.hardwarehwcomposer.值.so全局兜底影响所有 HAL3无hwcomposer.default.so默认模块三级回退的设计初衷是同一块板上多个 HAL 共享同一个变体名比如全是ranchu。但它带来一个很隐蔽的坑ro.hardware是所有 HAL 类别的共同兜底你改它一个音频、摄像头、蓝牙、传感器全跟着变。我在一个镜像上把ro.hardware从一个奇怪的值改成ranchu显示确实好了结果音频 HAL 加载失败了——因为音频那个变体名的.so在镜像里根本不存在。这条经验我建议刻在脑子里能改ro.hardware.hwcomposer就别改ro.hardware。另外要说明的是安卓 8 之后大量 HAL 转向 HIDL/AIDL改成向 servicemanager 注册服务由init.rc拉起VINTF manifest 声明接口。这时候ro.hardware.*可能依然存在但它是不是真的还在参与动态库加载得单独确认。别看到属性在就以为加载路径没变——这两套机制在过渡期的镜像里经常是并存的gralloc 走老路、HWC 走新路完全可能。3.2 虚拟机上这几条属性的典型取值下面这张表是我在几个不同安卓版本的虚拟机镜像上getprop抓到的典型结果。再次强调具体值随版本、随宿主平台的-gpu参数变化下面只当作参照坐标不要照抄属性典型取值它决定了什么ro.hardwareranchu所有 HAL 的全局变体兜底ro.hardware.hwcomposerranchu合成器 HAL 变体ro.hardware.grallocdefault或minigbm图形缓冲区分配器变体ro.hardware.eglemulation、angle、swiftshaderEGL 驱动实现ro.hardware.vulkanranchu、emulated也可能为空Vulkan 驱动实现ro.opengles.version196608即 0x30000表示 GLES 3.0上报给应用的 GLES 版本ro.kernel.qemu1标识我跑在虚拟机里ro.kernel.qemu.gles1或2guest 侧 GLES 翻译模式qemu.gles1或2与上面配对使用ro.boot.hardwareranchu来自 bootconfig只读看ro.opengles.version这个值的写法就能看出属性系统的局限它是个整数编码0x00030002表示 GLES 3.20x00030000表示 3.0。用整数是因为属性值只有 92 字节、类型系统又极简陋只能靠编码来表达版本。所以当你发现某个应用在虚拟机上降级渲染或者报不支持 GLES 3.x第一件事就是查这条属性而不是先怀疑 GPU 驱动。3.3 属性为空和属性写错后果完全不一样这是我在排障中总结出来的一条经验性判断法则能省很多时间属性为空或者没定义会走回退链也许还有个default模块恰好能撑住表现是能起来但性能差/花屏。属性写错指向一个不存在的变体名回退链会被打断dlopen直接失败HWC 初始化失败SurfaceFlinger 起不来表现是黑屏 反复重启。所以看到黑屏 SurfaceFlinger 崩溃优先怀疑属性写错了看到能起来但画面诡异优先怀疑属性指向的实现不对。这两种现象的排查方向完全不同先分类再动手效率差好几倍。4. 用几条命令把显示驱动到底走了哪条路摸清楚4.1 getprop 的正确筛法最朴素的命令是adb shell getprop但输出几百行眼睛会瞎。我的习惯是分两步筛# 第一步只看显示相关的关键属性 adb shell getprop | grep -E \[ro\.(hardware|opengles|boot) # 第二步单独确认几个配对使用的值 adb shell getprop ro.hardware.hwcomposer adb shell getprop ro.hardware.gralloc adb shell getprop qemu.gles adb shell getprop ro.kernel.qemu.gles这里有个小技巧grep的时候加个\[前缀能把搜索范围限制在属性名上否则像ro.hardware这种片段会命中一堆无关行。另外如果版本支持getprop -Z可以直接看到每条属性的 SELinux 上下文排查写入被拒的时候非常好用老版本没有这个选项只能去翻property_contexts。4.2 dumpsys SurfaceFlinger 里的自述信息属性告诉你打算走哪条路dumpsys告诉你实际走了哪条路。这两者不一致的情况非常常见一定要交叉验证adb shell dumpsys SurfaceFlinger | head -80在输出开头通常能找到 GLES 自述那一行格式大概是GLES: 厂商, 渲染器名, 版本。如果渲染器名里出现SwiftShader、Emulation这类字样说明当前走的是 guest 侧软渲染或者翻译层如果是宿主的真实 GPU 型号说明是直通模式。合成方式也在同一份输出里找HWC相关的字段能看出当前是硬件合成还是回退到 GPU 合成。这个信息量很大属性指向宿主直通但 dumpsys 显示走的是软渲染说明中间某一环回退了这时候再往下查具体在哪一环回退。4.3 看进程真正 dlopen 了哪个 so这是最接近真相的手段因为它不依赖任何自述# 看目录里到底有哪些模块 adb shell ls -l /vendor/lib64/hw/ | grep -E gralloc|hwcomposer # 看表面上真正加载了哪个 adb shell cat /proc/$(adb shell pidof surfaceflinger)/maps | grep -E gralloc|hwcomposer|libEGL/proc/pid/maps里出现的就是这个进程实际映射进来的动态库做不了假。我第一次用这个方法的时候发现dumpsys里报的是一个渲染器maps里加载的却是另一个版本的libEGL.so原因是镜像里同时存在/vendor/lib64/和/system/lib64/两份同名库加载顺序和我想的不一样。这种问题不看maps基本查不出来。4.4 logcat 里绕不开的那几个标签配合上面两条再看日志adb logcat -b all -d | grep -iE hwcomposer|gralloc|libEGL|gfxstream|vulkan|ranchu-b all是为了把 main、system、crash 几个缓冲区一起捞出来。显示类问题经常是 native crash 先发生在 system 缓冲区主缓冲区里反而干干净净只捞 main 会漏掉关键信息。5. 想改一条显示 property有三条路代价差很多5.1 改 build.prop最干净但要动镜像能改ro.属性的第一条路就是改build.prop。在虚拟机上的操作流程大概是emulator -avd 名字 -writable-system adb root adb remount adb pull /vendor/build.prop # 修改后推回去 adb push build.prop /vendor/build.prop adb shell stop adb shell start-writable-system是关键不加这个参数/system和/vendor都是只读挂载adb remount会失败。改动生效需要重启框架stopstart或者整机重启因为ro.属性在 init 阶段就定下来了。这里有个真实体验要提醒-writable-system下的改动在我的环境里重启后还能保留但换安卓版本、wipe data 或者换个 AVD 就没了。别把宿主机上的临时改动当成正式方案要长期稳定还是得重新打包镜像或者用 init 脚本固化。5.2 init.rc 里 setprop适合启动早期就必须定下来的属性第二条路是在init.hardware.rc里写setprop格式大概是这样on early-init setprop ro.hardware.hwcomposer ranchu setprop ro.opengles.version 196608这里最容易翻车的是时机窗口。early-init阶段太早某些属性服务可能还没完全就绪而且 SELinux 策略可能还没加载完写入会被拒boot阶段太晚SurfaceFlinger 早就启动了属性设了也白设只影响下一次。显示类的属性一般放在on init到on early-init之间比较稳具体哪个阶段能成功得靠logcat里 init 的日志去验证。我个人的经验是优先用 build.prop只有在需要按条件动态决定属性值的时候才用 init.rc。因为 build.prop 是声明式的、好审查init.rc 里能写逻辑也就意味着能写出难查的条件分支。5.3 adb setprop只能改非 ro. 的调试属性第三条路是运行时改也就是setprop。它只对debug.*、persist.*和一些非ro.的系统属性有效adb root adb shell setprop debug.hwui.renderer skiagl adb shell setprop debug.sf.latch_unsignaled 1debug.hwui.renderer这条挺实用可以切换应用侧的渲染后端比如 OpenGL 还是 Vulkan 后端虚拟机上 Vulkan 支持不全的时候用它切回 GL 后端起效很快。persist.前缀的属性会写进/data/property/重启保留做长期实验比较方便。注意改完这些debug.属性通常要重启对应的进程才生效。改渲染后端要杀掉应用进程重开改 SurfaceFlinger 相关的一般要adb shell stop adb shell start光改属性不重启是看不到变化的。5.4 property_override唯一能后悔的机制安卓 10 以后 init 提供了property_override可以在 init 阶段覆盖一个已经被写过的ro.属性on init property_override ro.hardware.egl swiftshader这是唯一能在ro.已经写过了之后仍然改掉它的正规手段但限制也很明确只能在 init 阶段用不能从adb shell触发而且覆盖的仍然是ro.属性那套一次性语义只是把第一次往后挪了。用它之前先确认镜像的 init 版本支持这个指令老版本会直接报语法错误导致 init 启动失败那就不是黑屏而是连 adb 都连不上了。6. property 完全正确但画面还是不对往哪查6.1 属性对了so 不存在先看 32/64 位目录ro.hardware.hwcomposerranchu完全正确但/vendor/lib64/hw/里只有hwcomposer.default.so一样会加载失败。更隐蔽的是 32/64 位混装/vendor/lib/hw/32 位里有hwcomposer.ranchu.so/vendor/lib64/hw/里没有而 SurfaceFlinger 是 64 位进程它只会去lib64找。表现形式是dlopen失败但错误信息很含糊可能只报找不到模块。所以排查顺序上看属性之前先确认模块文件在两个目录下都存在且架构匹配这一条能过滤掉相当一部分问题。6.2 so 存在也加载了但依赖缺了模块加载成功不代表能用。用readelf -d看它的NEEDED依赖adb shell readelf -d /vendor/lib64/hw/hwcomposer.ranchu.so | head -30显示类模块常见的缺失依赖是libEGL.so、libgbm、libc.so这几个。虚拟机镜像裁剪的时候很容易漏掉某个间接依赖因为dlopen报的错只会提到直接依赖间接依赖缺失往往表现为运行时某个函数指针为空然后崩掉。6.3 加载都正常画面却是斜条纹或者撕裂这是我遇到过的、最有迷惑性的一类问题属性对、模块对、依赖齐画面却是斜着一道道条纹。原因是 gralloc 和 HWC 对 buffer stride行跨距的对齐要求不一致。gralloc 按 4 字节对齐算 strideHWC 按 64 字节对齐去读读出来的每一行偏移都错一点累积起来就是斜纹。这类问题的排查线索是画面中静止的部分也在斜说明不是时序问题是数据布局问题。反过来如果只有运动物体撕裂那才是垂直同步或者 buffer 数量的问题方向完全不同。6.4 -gpu 参数和 guest 属性打架虚拟机宿主侧的-gpu参数和 guest 里的qemu.gles、ro.kernel.qemu.gles是配对的。如果你在 AVD 配置里写了-gpu host但 guest 镜像里的属性还是 guest 软渲染那套两边就会互相不理解表现为画面能出但性能极低或者 Vulkan 相关应用直接崩。验证方法是把这几条值摆在一起看是否自洽宿主设置期望的 guest 属性不一致时的表现-gpu hostqemu.gles2GLES 走直通性能异常低像软渲染-gpu guestqemu.gles1guest 侧翻译偶发花屏宿主负载很低-gpu swiftshader_indirectEGL 指向软渲染实现与属性不一致时启动慢但不崩-gpu off无显示加速应用报 GLES 不可用6.5 一张现象到属性的对照表排障的时候最耗时间的其实是从现象想到该查什么。把我常用的对照关系整理成表贴在工位上挺有用现象优先查的属性常见根因黑屏且 SurfaceFlinger 反复重启ro.hardware.hwcomposer变体名写错模块不存在开机动画能过但应用全黑ro.hardware.egl、ro.opengles.versionEGL 实现缺失或版本上报错误画面斜条纹ro.hardware.gralloc分配器与合成器对齐要求不一致性能极低像软渲染qemu.gles、ro.kernel.qemu.gles宿主直通参数与 guest 属性不匹配应用报不支持 Vulkanro.hardware.vulkan属性为空或指向不存在的实现属性设置无任何反应属性名前缀与property_contextsro.已写过或 SELinux 未放行7. 几个我反复踩到的坑以及一点个人体会第一个坑是用 diff 的方式批量改属性。我早期的做法是把两台机器的getprop输出导出逐条比对差异然后照搬。结果越改越乱。原因是属性之间有耦合比如ro.kernel.qemu和qemu.gles是一组ro.hardware.*和ro.boot.hardware又是一组单独搬一条过去等于破坏了另一台的内部一致性。后来我改成分组看、成组改先把显示相关的属性按链路分成分配、合成、渲染三组再决定动哪一组。第二个坑是属性值里带空格或者特殊字符。有的属性看着能设进去但读出来被截断或者后半段消失。属性系统对值的处理非常粗糙别指望它做转义。有复杂配置需求的话写文件、读文件别硬塞进属性。第三个坑也是我觉得最值得说的一条改动ro.hardware之前先确认所有 HAL 的变体模块都换了名字。我前面提到的音频那次翻车就是这么来的。后来我的习惯是只要涉及ro.hardware就先把/vendor/lib64/hw/和/vendor/lib64/目录完整列一遍确认所有以变体名结尾的模块都存在再动手。这个前置检查花不了一分钟能省掉一整晚。最后一个体会是关于心态的。显示问题看起来玄学其实链路非常确定属性选模块、模块提供能力、能力决定渲染路径一步扣一步。真正难的不是技术是在信息量爆炸的日志里先分类再定位。我现在的固定顺序是先看现象属于起不来还是起来但不对再看属性属于为空还是写错最后用dumpsys和/proc/pid/maps验证实际路径。按这个顺序走绝大多数问题都能在三轮以内收敛到具体那一条属性上。
返回列表