ARTICLE DETAIL

资讯详情

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

Android系统属性完全指南:从getprop到SELinux权限与避坑实践

Android系统属性完全指南:从getprop到SELinux权限与避坑实践 做Android开发这几年系统属性可以说是绕不开的一个基础机制。不管你是做ROM定制、系统应用还是写自动化测试脚本多多少少都会碰到getprop和setprop这俩命令。我最早被这东西折腾还是在做系统级功能适配的时候想判断一个机型配置、动态开关某个特性绕来绕去都是在跟Android系统属性打交道。这篇内容我打算把系统属性的读取和设置掰开揉碎讲清楚。从最基础的命令操作到Java、Native层的调用方式再到背后的权限机制和常见坑位一次性整理出来。适合Android系统开发、ROM适配、自动化测试的同学参考应用层开发偶尔遇到属性相关问题时也能拿来应急。1. 先搞清楚Android 系统属性到底是个什么机制1.1 一个全局键值对仓库Android系统属性说白了就是一个全局的键值对仓库格式就是keyvalue比如ro.build.version.sdk34、ro.product.modelPixel 8。它在系统里承担的任务很杂硬件信息、系统版本、运行时开关、调试状态、性能调优参数全都塞在这个仓库里。你可以把它类比成Windows的注册表或者Linux下的环境变量增强版。跟普通配置文件最大的不同是系统属性是常驻内存的读取效率非常高而且有一套独立的权限控制机制。内核、init进程、系统服务、Java层和Native层都能访问它所以它成了Android系统里跨层级传递信息的一个关键通道。这套机制从Android早期就一直存在发展到现在已经非常成熟。你在命令行里敲一个getprop屏幕上能刷出上百行属性这些属性有的是编译时就定死的有的是设备启动过程中动态写入的有的则是运行时根据状态实时变化的。理解它们各自的特性是你用好系统属性的前提。1.2 属性名前缀里藏的门道系统属性的键名不是随便起的命名规则里带着语义。看到前缀基本就能猜到它的生命周期和用途下面这张表是我平时用得最多的几类前缀含义写后是否持久化常见例子ro.只读属性编译期或启动早期写入不持久化重启重新加载ro.build.version.sdk、ro.product.model、ro.debuggablepersist.持久化属性保存到 /data/property重启保留persist.sys.usb.config、persist.sys.timezonesys.运行时系统状态属性不持久化重启后重新生成sys.usb.config、sys.boot_completedvendor.vendor分区相关的属性视具体前缀而定vendor.display.xxx、vendor.audio.xxxinit.svc.init进程维护的服务状态不持久化init.svc.zygote、init.svc.adbdnet.网络相关状态不持久化net.dns1、net.eth0.dns1这里最需要记住的是ro.开头的属性。它在系统启动早期加载后就变成只读了运行时你用setprop去改基本无效因为属性服务的写入逻辑会直接拒绝你。要改ro.属性只能在源码编译时改或者把对应分区重新打包刷入。很多新手在ro.debuggable上栽过跟头就是这个原因。另外一类要注意的是persist.前缀。它的价值在于重启不丢系统会把持久化属性写入到/data/property/目录下对应文件里。但这也意味着一旦写入就会占用存储并影响后续启动改的时候要谨慎。平时调试用persist.sys.xxx来临时保存自己的参数是个很方便的做法前提是你得有权限。1.3 系统属性最典型的几个使用场景系统属性的应用场景我总结下来主要有下面几种第一是硬件和系统信息采集。App或者系统服务通过ro.product.board、ro.hardware这类属性判断当前设备平台从而走不同的逻辑分支。做机型适配的时候第一步就是把这些属性拉出来存档。第二是系统行为的动态开关。有些功能不想重新编译整个系统就可以通过persist或者sys前缀的属性在运行时控制。比如切换USB模式、控制日志输出级别、开启某些调试服务都是靠设置属性触发的。第三是跨进程的信息共享。例如sys.boot_completed这个属性开机完成后由SystemServer写入其他模块读它就能判断开机流程是否跑完。这种“一个进程写多个进程读”的模式在整个Android系统里非常普遍。我在实际项目里还常用它做自动化测试的环境标识。跑测试用例之前先setprop写入一个自定义标志被测模块读到这个标志后切换成测试模式测完再恢复。这种方式侵入性小不需要改业务代码逻辑特别适合埋点验证和灰度开关测试。2. 系统属性读取与设置的三种常用姿势2.1 命令行三板斧getprop、setprop 和管道过滤先讲最直接的Android设备连上ADB之后在电脑终端或者设备shell里可以用的几个命令。getprop不接参数会列出所有属性符合某个关键字就用grep过滤这个最常用adb shell getprop | grep ro.build.version我以前适配新设备时习惯先把整机属性拉到本地存档adb shell getprop device_props_$(date %Y%m%d).txt别小看这一步。不同厂商的ROM差异很大有了这份属性快照后面排查问题能省不少事。你怀疑某个功能跟机型有关直接对比两台设备的属性表差异一目了然。读取单条属性的姿势是getprop ro.build.version.sdk输出就是34这种纯值。写脚本时如果值不存在getprop会返回空串不会报错这一点在shell脚本里要注意判空。设置属性用setpropsetprop persist.sys.xxx 1 getprop persist.sys.xxx正常情况下第二条命令会打印出1。但这里有个大前提你得有权限。普通零售设备上在adb shell里直接setprop经常会出现“设置成功”但实际没生效的情况或者干脆提示failed to set property。这个后面第3章我专门讲原因。getprop配合shell的循环还能一次性批量读取。比如我想知道所有跟相机相关的属性for p in $(getprop | grep camera | cut -d[ -f2 | cut -d] -f1); do echo $p $(getprop $p) done实测下来这种脚本在做驱动适配和功能调试时非常管用。2.2 Java 层读取android.os.SystemProperties 与反射在App层Android并没有把系统属性操作封装成公开SDK接口真正的实现类在android.os.SystemProperties但它被hide注解标记了普通SDK编译期根本看不到。不过Java的反射机制给了我们一个后门。我自己在测试工具里经常这么写public class SystemPropertiesCompat { private static Class? clazz; private static Method getMethod; private static Method setMethod; static { try { clazz Class.forName(android.os.SystemProperties); getMethod clazz.getMethod(get, String.class); setMethod clazz.getMethod(set, String.class, String.class); } catch (Exception e) { e.printStackTrace(); } } public static String get(String key) { try { return (String) getMethod.invoke(null, key); } catch (Exception e) { return ; } } public static void set(String key, String value) { try { setMethod.invoke(null, key, value); } catch (Exception e) { e.printStackTrace(); } } }用的时候String sdk SystemPropertiesCompat.get(ro.build.version.sdk);反射读属性在普通App里基本都能成功因为读属性本身没有太多限制。但set方法就不一定了你调用它可能不报异常但实际上属性没变。这是因为底层的property_set会做权限校验没有系统签名或root权限写入会被静默丢弃。所以我做测试工具时会额外加一个读回校验SystemPropertiesCompat.set(persist.sys.my_switch, 1); String check SystemPropertiesCompat.get(persist.sys.my_switch); if (!1.equals(check)) { // 说明写入失败可能是权限不足或关键字不被允许 }这个校验逻辑看起来笨但非常实用。很多“以为设置成功”的假象都能被它一眼识破。如果你的应用是系统应用带有系统签名或者作为系统Privileged App预置到/system/priv-app那可以直接依赖系统源码编译结果不需要反射。不过绝大多数第三方应用没有这个条件反射加读回校验是最稳妥的方案。2.3 Native 层操作property_get / property_set系统层面做开发尤其是写C/C的Native服务可以直接调用libc提供的属性接口。包括property_get和property_set两个函数头文件在系统源码里对应sys/system_properties.h像这样#include sys/system_properties.h #include cstdio #include cstring int main() { char value[PROP_VALUE_MAX] {0}; // 读取属性 int len __system_property_get(ro.build.version.sdk, value); if (len 0) { printf(sdk version: %s\n, value); } // 尝试设置属性 int ret __system_property_set(persist.sys.demo, 1); if (ret ! 0) { printf(property set failed, ret%d\n, ret); } return 0; }某些项目里也会用到旧的cutils/properties.h头文件它提供的是property_get和property_set封装底层最终都会走到上面的接口。编译链接时注意加上对应库平台不一样依赖会略有差异Android.bp里经常要写libcutils或者libc。这里我特别想提一句Native层接口本身不做太多参数合法性检查长度和格式校验主要靠属性服务端。而且__system_property_set的返回码很重要它返回0才表示属性服务接受了你的请求但接受也不代表一定会生效因为SELinux可能在更早阶段就拦截了。这也是很多系统开发者排查问题的盲区。3. 底层机制与权限边界为什么你 setprop 不成功3.1 属性域、共享内存与 property_service很多人用属性用得很熟但从来没想过它底层是怎么运作的。简单来说属性系统分为两部分一块共享内存区域以及一个负责属性写入的守护服务。共享内存区域叫property_area系统启动早期由init进程初始化。所有进程读取属性时其实都是在直接读这块共享内存速度极快这也是为什么getprop几乎没有任何延迟。每个属性在内存里以固定格式存储除了键值本身还带有序列号、权限上下文等元信息。写入的路径则不一样。当进程调用property_set时数据不会直接写进共享内存而是通过socket发给init里的property_service。服务收到请求后先校验调用方的SELinux权限再校验属性名的前缀和上下文最后才更新共享内存同时通知等待该属性变化的进程。这就是为什么“读”和“写”两端的体验差别这么大读是本地操作写要过服务端的重重检查。这个架构设计得很有意思。如果所有进程都能直写共享内存权限控制就形同虚设了。引入一个统一的服务端做裁决才能保证属性系统的整体可信度。代价就是写属性多了几次进程间通信开销但对系统属性的使用频率而言这完全可以接受。3.2 SELinux 属性上下文对写入的限制Android从5.0开始全面应用SELinux系统属性在SELinux里也有自己的一套访问规则。每个属性名都会被映射到一个SELinux类型这个映射关系定义在/system/etc/selinux/下的property_contexts文件里。举个例子persist.sys.timezone属性映射的类型可能是timezone_propro.secure映射的是secure_prop。一个进程要对某个属性执行set_prop操作必须同时满足两条它在SELinux里被允许对该属性类型做写入并且属性服务端的代码也对它开放。最典型的场景普通App或者untrusted_app域进程去设置persist.sys.xxx大概率会触发类似下面的日志avc: denied { set } for propertypersist.sys.xxx scontextu:r:untrusted_app:s0 tcontextu:object_r:default_prop:s0 tclassproperty_service这条日志是什么意思简单理解一个“不受信任的应用”想给属性设置新值被SELinux策略拦截了。解决办法要么是给属性名配置对应的property_contexts映射并编写允许该进程写入的te规则要么干脆确保该操作由系统进程来发起。第三方应用想绕过这套机制基本不可能。我自己做系统开发时如果需要在debug版本上放行某个自定义属性一般的做法是这样在property_contexts或者是新版Android里的property_contexts生成规则里把属性名映射到一个自定义类型然后在 sepolicy 的te文件里allow对应域的写权限。整个过程要重新打包boot镜像或system镜像不是改一行配置能搞定的。这块内容如果你不是专门做系统开发的先理解“有这层限制存在”就够了。3.3 分区属性隔离与命名空间规范从Android 9、10开始Google对系统属性又加了一重管理按分区隔离命名空间。为什么要做这个因为系统解耦之后vendor、product、system各自独立升级如果system里的服务去依赖vendor分区定义的属性很容易出现版本错乱。所以引入了一套规则vendor分区的属性必须带vendor.前缀product部分带product.前缀system自己多数的只读属性还是ro.前缀。实际开发中最直观的感受是在较新版本上如果你在源码里新加了一个属性却忘了按分区规范取名字编译或者运行阶段可能会报属性权限的AVC警告。Android 12 AOSP里的CTS还会专门检查属性命名是否符合分区要求。简单说就是不要凭感觉瞎起属性名该加前缀加前缀该放哪个分区就放哪个分区。这套规范同样影响了setprop的使用习惯。我在新版本设备上调试时如果需要新增一个临时属性会优先用persist.vendor.xxx或者persist.system.xxx这样既符合命名规范也更容易在SELinux侧匹配到合适的上下文。随便起一个persist.abc.xxx很可能被默认策略标记成default_prop写入权限反而不够明确。如果你不需要深入底层做权限定制了解这层机制至少可以帮你快速判断一个问题某个属性设置失败到底是操作姿势不对还是分区策略限制了。4. 实操避坑指南改属性不生效的排查思路4.1 先分清 ro、persist 和动态属性的差异我收到过很多类似的提问“我明明setprop了怎么一重启就没了”“为什么ro.xxx一直改不了”。这些问题百分之八十是因为没分清属性类型。ro.开头的属性在系统启动之后基本是写不动的。你setprop ro.xxx 1命令可能没有直接报错但你再读一遍值一点变化没有。就算某些版本让你写进去了也只改了内存副本重启后一切还原。所以对ro.属性唯一正规的修改途径是在源码或镜像层改改完重新刷机。persist.开头的属性写成功后是希望保留的但它也不是万无一失。持久化属性在正常流程下会写入/data/property/如果这个目录所在分区满了、损坏了或者SELinux不允许进程写那么设置后重启照样会丢。我遇到过的问题是自定义属性名跟系统已有条目冲突写入时自己被误导读出来的值却是旧的。剩下那些没有特殊前缀的属性像sys.usb.config、init.svc.zygote纯粹是内存里的临时状态。重启后全部清零完全不要指望它们做任何持久化保存。所以写代码前先问自己一句这个值重启后还需要吗需要就用persist不需要就用普通属性不要混着来。4.2 setprop 没报错却没有任何效果多半是权限掉了近几年我在调试时遇到最隐蔽的问题是setprop命令看起来执行成功终端既不报错也不输出任何提示但属性值始终没变。这个事必须展开说因为太容易误导人了。根本原因还是权限。当你用的shell不是root或者系统不是userdebug/eng版本的时候setprop请求会先到属性服务权限检查直接拦截然后静默丢弃。有些设备厂商会在自己的版本上做定制导致setprop返回非零甚至退出码但谷歌原生的表现往往是“无响应”。快速判断权限情况的办法先看两个属性adb shell getprop ro.debuggable adb shell id一般返回ro.debuggable1且id显示uid0(root)才有完整写入能力。零售版设备ro.debuggable0shell 用户是uid2000(shell)此时不要浪费时间硬改老老实实走镜像修改路线。在允许root的设备上用adb root之后shell权限就会变成rootadb root adb remount adb shell setprop persist.sys.demo_test 1如果adb root都提示失败那说明当前固件根本没有在userdebug模式下编译也就不存在免解锁改系统属性的捷径。4.3 属性名和属性值的格式限制属性名字符串和属性值不是无限长的虽然在不同Android版本上具体数值略有差异但开发时要遵循一个基本的安全范围属性名别超过31字节新版本有更大长度属性值保守控制在92字节以内。我印象里有些新版本允许更大长度但实际兼容性很微妙没必要去赌边界。除了长度还有几个容易踩的坑属性值里尽量别塞空格。有些版本允许有些版本只截取到空格前。我读某个属性时发现值只有半个单词排查了半天才发现是写入时带了空格。属性名里不要乱放特殊字符。点号、下划线是常规用法但某些符号在SELinux配置里不好匹配也容易引起解析问题。自定义属性名最好带项目标识比如persist.demo.foo1不要用persist.abc1这种太短的键名既不规范也容易跟厂商已有的内部属性冲突。有一次我为了快速调试写了个属性值是JSON串结果读回来被截断。后来学乖了像这种长内容就不要走系统属性要么写文件要么走Settings.Global属性系统干不了这个活。4.4 修改 build.prop 的正确姿势如果你确实需要修改ro.开头的属性比如ro.product.model或者自己加一条自定义只读配置那必须回到镜像文件层面操作。最常见的对象是/system/build.prop厂商也可能在vendor分区提供对应文件。大致流程是adb root adb remount adb pull /system/build.prop /tmp/build.prop # 在本地修改 build.prop追加或者修改属性行 adb push /tmp/build.prop /system/build.prop adb reboot这中间有一个关键风险点remount在某些版本上会把分区重新挂载成可写但它需要解锁bl状态或debuggable固件支持。修改build.prop时最好先备份原文件到电脑改坏了还能推回去。还有别手动加一些奇奇怪怪的空格、注释符号格式不对会导致启动阶段属性解析失败。如果你做的是源码级开发那更推荐直接在产品配置的mk或bp文件里新增PRODUCT_PROPERTY_OVERRIDES ro.product.xxx1编译系统会自动把属性写进生成镜像里的build.prop。运行期不要试图修改只读属性在源码层把它定死是唯一干净可控的方式。4.5 附常见问题速查表把这段时间总结的经验整理成一张速查表遇到问题先对号入座现象可能原因处理方式getprop 查不到某个属性属性名拼错或尚未写入用 getpropsetprop 后立刻读没变化无写入权限、SELinux拦截查看logcat/dmesg里是否有avc denied检查ro.debuggable和id重启后属性丢失使用了非persist前缀或persist写入失败改用persist.开头检查/data分区状态和SELinux策略修改ro.属性无效ro.属性运行时只读走源码编译或修改build.prop后刷机App里反射设置属性不报错但没生效普通App无系统属性写权限升级为系统应用或通过root进程执行属性值被截断/变成乱码超过长度限制或含特殊字符缩短属性值避免空格和特殊符号persist.sys.xxx能读不能写该属性上下文可能映射到受保护类型系统开发时调整property_contexts映射并放行这张表基本覆盖了我日常工作中遇到的大多数属性异常。说实话系统属性本身不复杂复杂的是它和权限、分区、SELinux这些机制缠在一起后出现的各种“玄学”现象。但只要抓住“读走共享内存、写走服务校验”这条主线再结合日志里的avc提示大多数问题都能在几分钟内定位到根因。最后分享一个我自己的习惯拿到一台新设备或者新固件第一件事不是急着看代码而是先把getprop全部导出存档命名带上日期和版本号。后面不管你改了什么、调了什么再导一份新版出来diff一下就知道哪些属性变了。这个操作简单但确实帮我解决过不少“明明没动过它怎么坏了”的冤案。属性系统用好的时候是你调试路上的加速器用不好就是坑你时间的大坑希望这篇内容能帮你把它控制在自己手里。
返回列表