RK平台 Android12 动态调整屏幕方向(persist 属性方案,免重新编译)
适用场景:RK 平台 Android 12 / Android 14,整机为固定安装方向(竖屏广告机、工控一体机、自助终端、门禁机等),无 gsensor,需要在不重新编译烧录整包的前提下调整系统开机默认方向。
0. 先说结论
原生方案用的是ro.surface_flinger.primary_display_orientation。它是只读属性,值在编译期就固化进build.prop,所以每改一次方向都要重新编译、重新烧录整包 —— 对经常要换屏、换安装方式的整机项目来说,这个代价太高。
本文的改法是把属性名换成persist.surface_flinger.primary_display_orientation。persist.前缀的属性运行时可写,setprop之后重启即生效,一个整机一个值,不用动编译产物。
但这条路有个很容易踩空的前提:persist.属性是落盘在/data/property/下的。如果/data因为分区加密被推迟到 late-fs 才挂载,而 SurfaceFlinger 启动得更早,那它读到的永远是默认值0—— 补丁打了、setprop 也设了,方向就是不变。第 2.2 节讲的就是这个坑。
⚠️ 两件事先讲清楚,避免预期错位:
- 这个方案解决的是「不用重新编译」,不是「不用重启」。SurfaceFlinger 只在启动时读一次这个属性,改完仍需重启。
- 第 2.2 节的修复要放弃
/data分区加密,这是有安全代价的。只适合开发板、内部样机、数据不敏感的设备。量产出货机型请走第 5 节的替代思路。
1. 为什么原生方案不够用
先把两个属性的差别摆清楚,后面所有改动都围绕这张表:
| 对比项 | ro.surface_flinger.primary_display_orientation(原生) | persist.surface_flinger.primary_display_orientation(本文方案) |
|---|---|---|
| 可写性 | 只读,编译期固化 | 运行时可写(setprop) |
| 取值来源 | SurfaceFlingerProperties::primary_display_orientation() | property_get()直接读属性 |
| 存储位置 | /system/build.prop或vendor/build.prop | /data/property/下 |
| 生效时机 | 重新编译 + 重新烧录 | setprop后重启 |
| 前置依赖 | 无 | /data必须已挂载 |
| 恢复出厂后 | 不受影响 | 被清掉,回落到默认值 |
关键点在于「存储位置」这一行。ro.属性住在系统分区,怎么重启都在;persist.属性住在数据分区,这既是它能被动态修改的原因,也是它读不到的原因 —— 同一个特性,两面性。
2. Android 12:改属性来源
2.1 修改方向属性判断
改动点在frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp的构造函数里:把原来那段编译期取值整段注释掉,换成读persist属性。
--- a/frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp +++ b/frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp @@ -427,7 +427,7 @@ SurfaceFlinger::SurfaceFlinger(Factory& factory) : SurfaceFlinger(factory, SkipI }(); useContextPriority = use_context_priority(true); - +/* using Values = SurfaceFlingerProperties::primary_display_orientation_values; switch (primary_display_orientation(Values::ORIENTATION_0)) { case Values::ORIENTATION_0: @@ -442,6 +442,25 @@ SurfaceFlinger::SurfaceFlinger(Factory& factory) : SurfaceFlinger(factory, SkipI internalDisplayOrientation = ui::ROTATION_270; break; } +*/ + char value_private[PROPERTY_VALUE_MAX]; + property_get("persist.surface_flinger.primary_display_orientation", value_private, "0"); + int temp = atoi(value_private); + ALOGD("primary_display_orientation is: %d",temp); + + switch (temp) { + case 0: + break; + case 90: + internalDisplayOrientation = ui::ROTATION_90; + break; + case 180: + internalDisplayOrientation = ui::ROTATION_180; + break; + case 270: + internalDisplayOrientation = ui::ROTATION_270; + break; + }几个实现细节:
- 默认值传
"0"。/data没挂上、或者整机还没设过这个属性时,property_get会回落到"0",atoi得到 0,走case 0保持ui::ROTATION_0—— 也就是原生默认行为。这样补丁不会让没配置过的机器行为发生意外变化,很重要。 case直接用角度 90 / 180 / 270,而不是 1 / 2 / 3。现场调试、和客户对参数时不用在脑子里做映射,少一类低级错误。- 加一行
ALOGD。整个方案最怕的就是「属性没读到」,有这行日志能立刻分辨是「读到的值不对」还是「读到了但方向逻辑没生效」。本文第 4 节的验证步骤会用到它。
2.2 关键坑:data 分区加密导致开机过程中读不到属性
现象:补丁打好了,setprop persist.surface_flinger.primary_display_orientation 90也执行了,getprop能查到这个值,但重启后方向纹丝不动。而如果把值写回ro.属性重新编译,一切正常。
这说明属性写进去了,但 SurfaceFlinger没读到。根因链条是这样的:
- SurfaceFlinger 在构造函数里执行
property_get("persist.surface_flinger.primary_display_orientation", ...); persist.系列属性的持久化文件存放在/data/property/下;- 而在默认 fstab 里,
userdata分区带了fileencryption=...和latemount标记 —— 它需要等密钥服务就绪、在 late-fs 阶段才被挂载; - 挂载时机和 SurfaceFlinger 的启动时机对不上,
property_get执行时/data还不可用,属性服务拿不到值,返回默认"0"; - 结果就是
temp = 0,方向保持 0 度。
修复思路:让/data在 SurfaceFlinger 读到属性之前就可用。需要改两个文件。
① 挂载时机:把 early / late 两段拆分的挂载合并成一次完整挂载
diff --git a/device/rockchip/common/rootdir/init.mount_all_early.rc b/device/rockchip/common/rootdir/init.mount_all_early.rc index 200d053ebf..6ec6af3d3b 100644 --- a/device/rockchip/common/rootdir/init.mount_all_early.rc +++ b/device/rockchip/common/rootdir/init.mount_all_early.rc @@ -4,16 +4,7 @@ on early-fs start vold on fs - mount_all /vendor/etc/fstab.${ro.hardware} --early + mount_all /vendor/etc/fstab.${ro.hardware} on late-fs # Start services for bootanim - start servicemanager - start hwcomposer-2-1 - start gralloc-2-0 - start surfaceflinger - start bootanim - - exec_start wait_for_keymaster - # Mount RW partitions which need run fsck - mount_all /vendor/etc/fstab.${ro.hardware} --late原来on fs阶段只挂--early那一批,userdata这类latemount分区要等到on late-fs才挂 —— 而on late-fs里同时还在拉起surfaceflinger。改完之后,on fs阶段一次mount_all(不带 flag)把分区挂齐,时序就不会倒挂。
② 分区加密:去掉 userdata 的 fileencryption 标记
diff --git a/device/rockchip/common/scripts/fstab_tools/fstab.in b/device/rockchip/common/scripts/fstab_tools/fstab.in index 2ec6c26565..b8cef766e9 100755 --- a/device/rockchip/common/scripts/fstab_tools/fstab.in +++ b/device/rockchip/common/scripts/fstab_tools/fstab.in @@ -23,6 +23,6 @@ ${_block_prefix}odm /odm ext4 ro,barrier=1 ${_flags},first_stage_mount # For sdmmc /devices/platform/${_sdmmc_device}/mmc_host* auto auto defaults voldmanaged=sdcard1:auto # Full disk encryption has less effect on rk3326, so default to enable this. -/dev/block/by-name/userdata /data f2fs noatime,nosuid,nodev,discard,reserve_root=32768,resgid=1065 latemount,wait,check,fileencryption=aes-256-xts:aes-256-cts:v2+inlinecrypt_optimized,keydirectory=/metadata/vold/metadata_encryption,quota,formattable,reservedsize=128M,checkpoint=fs +/dev/block/by-name/userdata /data f2fs noatime,nosuid,nodev,discard,reserve_root=32768,resgid=1065 latemount,wait,check,keydirectory=/metadata/vold/metadata_encryption,quota,formattable,reservedsize=128M,checkpoint=fs # for ext4 #/dev/block/by-name/userdata /data ext4 discard,noatime,nosuid,nodev,noauto_da_alloc,data=ordered,user_xattr,barrier=1 latemount,wait,formattable,check,fileencryption=software,quota,reservedsize=128M,checkpoint=block去掉fileencryption=aes-256-xts:aes-256-cts:v2+inlinecrypt_optimized之后,/data不再需要解密即可挂载,也就不会卡在 late-fs 阶段等密钥。
两处改动是配套的:只改 rc 不改 fstab,/data依然要等加密密钥;只改 fstab 不改 rc,/data可能仍然被排在 late-fs 挂载序列里。要一起上。
⚠️这一步是有代价的,必须说清楚:注释掉fileencryption等于放弃/data分区加密。用户数据、账号 token、本地缓存全部明文可读。建议只在开发板、内部样机、或设备本身不承载敏感数据的场景(比如公网广告机)使用。如果整机要出货给客户、涉及账号 / 支付 / 客户数据,不要照抄这一段,请走第 5 节的替代思路。
3. Android 14:改动点变了
Android 14 上不能照搬 Android 12 的改法,因为物理显示方向的取值位置和来源都变了:从构造函数移到了SurfaceFlinger::getPhysicalDisplayOrientation(),而且原来的值是从 HWC(getHwComposer().getPhysicalDisplayOrientation(*id))上报上来的,不是 ro 属性。
diff --git a/frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp b/frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp index 53f6d379416..87cfc400b71 100644 --- a/frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp +++ b/frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp @@ -2795,15 +2795,20 @@ ui::Rotation SurfaceFlinger::getPhysicalDisplayOrientation(DisplayId displayId, if (!id) { return ui::ROTATION_0; } + + char value_private[PROPERTY_VALUE_MAX]; + property_get("persist.surface_flinger.primary_display_orientation", value_private, "0"); + int temp = atoi(value_private); + ALOGD("primary_display_orientation is: %d",temp); if (!mIgnoreHwcPhysicalDisplayOrientation && getHwComposer().getComposer()->isSupported( Hwc2::Composer::OptionalFeature::PhysicalDisplayOrientation)) { - switch (getHwComposer().getPhysicalDisplayOrientation(*id)) { - case Hwc2::AidlTransform::ROT_90: + switch (temp) { + case 90: return ui::ROTATION_90; - case Hwc2::AidlTransform::ROT_180: + case 180: return ui::ROTATION_180; - case Hwc2::AidlTransform::ROT_270: + case 270: return ui::ROTATION_270; default: return ui::ROTATION_0;改动本质很简单:switch的入参从「HWC 上报的物理方向」换成「我们自己读的属性值」,其余控制流不动。属性名、取值范围(0/90/180/270)、默认值都和 Android 12 保持一致 —— 一套配置在两个版本上通用,这一点对做多版本 SDK 的项目很省事。
⚠️ 有一个坑要注意:if条件里的mIgnoreHwcPhysicalDisplayOrientation由属性debug.sf.ignore_hwc_physical_display_orientation控制。它一旦为true,整个if块会被跳过,你新加的switch根本不会执行,函数会直接落到return ui::ROTATION_0。
RK 官方文档在 Android 14 的ro属性方案里建议加上这个属性,但那是给框架的另一条取值路径准备的。采用本文的 persist 方案时,请确认这个属性没有被设成true(或者干脆把if条件里这个判断一并去掉),否则会出现「属性读到了、日志也打了、方向还是不动」的二次踩坑。这一点建议在你的 SDK 上实测确认一遍。
4. 验证步骤
改完之后按这个顺序验证,每一步都能定位到具体环节:
# 1) 写入属性(需要 root 权限)adb root adb remount adb shell setprop persist.surface_flinger.primary_display_orientation90# 2) 确认值写进去了adb shell getprop persist.surface_flinger.primary_display_orientation# 3) 重启adb shellreboot# 4) 看 SurfaceFlinger 是否读到了值(就是补丁里那行 ALOGD)adb shell logcat-ball|grep"primary_display_orientation is"# 5) 看最终生效的物理方向adb shell dumpsys SurfaceFlinger|grep-iorientation失败情况分两种,对应的问题完全不同:
情况 A:第 1 步setprop就被拒(Failed to set property)。说明这个新属性名没有在 SELinux 的property_contexts里登记,被策略拦了。先用adb shell setenforce 0复测一次:如果放行后能写进去,就确认是 SELinux 问题,补一条该属性名的 context 即可(persist.前缀允许运行时写,但具体哪个域能写仍由 SELinux 决定,这一点容易漏)。
情况 B:第 1 步成功、第 2 步能查到值,但第 4 步日志里temp是 0。说明属性没被读到,直接回到第 2.2 节 ——/data挂载时机没对上,或者/data被清过。
另外提醒一句:persist.属性存在/data/property/下,恢复出厂设置会连同/data一起清掉,方向会掉回默认值 0。如果需要「出厂永久固定方向、怎么恢复都变不回来」,那最终还是得回落到编译期的ro属性方案,persist 方案只适合「运行时现场调整」这个诉求。
5. 这个方案的边界与替代思路
能解决什么:一台设备不用重编整包就能改开机方向,现场换屏、换安装方式时非常省事。
不能解决什么:
- 不能免重启 —— SurfaceFlinger 只在启动时读一次;
- 不能免
/data依赖 —— 属性住在数据分区,这是结构性约束; - 恢复出厂会重置;
- 如果要绕开
/data加密,就得分清这是开发设备还是出货设备。
替代思路(本文未实测,供参考):如果不想动/data加密,可以让属性值换一个「更早就能读到」的存放位置。思路是走 uboot 传参链路,把方向值作为androidboot.*之类的参数带进内核 cmdline,SurfaceFlinger 改为读/proc/cmdline里的这个参数。这样/data加密保留、/data挂载时序也不用改,改方向只需重烧 uboot 环境变量,比整包重编小得多。适合需要保留数据加密的量产机型。代价是多一条传参链路要维护,量产阶段要确认 uboot 与内核的参数约定稳定。
6. 完整改动清单
| Android 版本 | 文件 | 改动内容 |
|---|---|---|
| 12 | frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp | 构造函数:把 ro 属性的编译期取值整段注释,改为property_get读 persist 属性后 switch |
| 12 | device/rockchip/common/rootdir/init.mount_all_early.rc | on fs阶段合并为一次完整mount_all,去掉--early/--late拆分 |
| 12 | device/rockchip/common/scripts/fstab_tools/fstab.in | userdata 去掉fileencryption(⚠️ 放弃 /data 加密) |
| 14 | frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp | getPhysicalDisplayOrientation():switch入参由 HWC 上报值改为属性值 |
7. 小结
整件事的核心其实只有一句话:把「编译期只读的ro属性」换成「运行时可写的persist属性」,然后保证读属性的时候/data已经挂上了。
坑也都集中在这句话的后半段:
persist属性住在/data/property/,而/data因为加密被排到 late-fs,比 SurfaceFlinger 启动还晚 —— 这是「补丁有效但方向不变」的唯一原因;- 修它要动挂载时机和分区加密两处,而放弃
/data加密是有安全代价的,得区分开发设备和出货设备; - Android 14 的改动点从构造函数挪到了
getPhysicalDisplayOrientation(),而且多了一个debug.sf.ignore_hwc_physical_display_orientation可以让整段逻辑失效。
深耕 Android 嵌入式与 RK 平台系统级开发多年,日常处理显示、存储加密、开机时序这类系统级问题。如果你在做 RK 平台整机项目时也遇到系统级的疑难问题,欢迎留言或私信交流。