☰
Android系统属性完全指南:从getprop到SELinux权限与避坑实践
2026/9/30 4:51:51 网站建设 项目流程

做Android开发这几年,系统属性可以说是绕不开的一个基础机制。不管你是做ROM定制、系统应用,还是写自动化测试脚本,多多少少都会碰到getprop和setprop这俩命令。我最早被这东西折腾还是在做系统级功能适配的时候,想判断一个机型配置、动态开关某个特性,绕来绕去都是在跟Android系统属性打交道。

这篇内容我打算把系统属性的读取和设置掰开揉碎讲清楚。从最基础的命令操作,到Java、Native层的调用方式,再到背后的权限机制和常见坑位,一次性整理出来。适合Android系统开发、ROM适配、自动化测试的同学参考,应用层开发偶尔遇到属性相关问题时也能拿来应急。

1. 先搞清楚:Android 系统属性到底是个什么机制

1.1 一个全局键值对仓库

Android系统属性说白了就是一个全局的键值对仓库,格式就是key=value,比如ro.build.version.sdk=34、ro.product.model=Pixel 8。它在系统里承担的任务很杂:硬件信息、系统版本、运行时开关、调试状态、性能调优参数,全都塞在这个仓库里。

你可以把它类比成Windows的注册表,或者Linux下的环境变量增强版。跟普通配置文件最大的不同是,系统属性是常驻内存的,读取效率非常高,而且有一套独立的权限控制机制。内核、init进程、系统服务、Java层和Native层都能访问它,所以它成了Android系统里跨层级传递信息的一个关键通道。

这套机制从Android早期就一直存在,发展到现在已经非常成熟。你在命令行里敲一个getprop,屏幕上能刷出上百行属性,这些属性有的是编译时就定死的,有的是设备启动过程中动态写入的,有的则是运行时根据状态实时变化的。理解它们各自的特性,是你用好系统属性的前提。

1.2 属性名前缀里藏的门道

系统属性的键名不是随便起的,命名规则里带着语义。看到前缀基本就能猜到它的生命周期和用途,下面这张表是我平时用得最多的几类:

前缀含义写后是否持久化常见例子
ro.只读属性,编译期或启动早期写入不持久化,重启重新加载ro.build.version.sdk、ro.product.model、ro.debuggable
persist.持久化属性,保存到 /data/property重启保留persist.sys.usb.config、persist.sys.timezone
sys.运行时系统状态属性不持久化,重启后重新生成sys.usb.config、sys.boot_completed
vendor.vendor分区相关的属性视具体前缀而定vendor.display.xxx、vendor.audio.xxx
init.svc.init进程维护的服务状态不持久化init.svc.zygote、init.svc.adbd
net.网络相关状态不持久化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脚本里要注意判空。

设置属性用setprop:

setprop 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_prop,ro.secure映射的是secure_prop。一个进程要对某个属性执行set_prop操作,必须同时满足两条:它在SELinux里被允许对该属性类型做写入,并且属性服务端的代码也对它开放。

最典型的场景,普通App或者untrusted_app域进程去设置persist.sys.xxx,大概率会触发类似下面的日志:

avc: denied { set } for property=persist.sys.xxx scontext=u:r:untrusted_app:s0 tcontext=u:object_r:default_prop:s0 tclass=property_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.debuggable=1且id显示uid=0(root),才有完整写入能力。零售版设备ro.debuggable=0,shell 用户是uid=2000(shell),此时不要浪费时间硬改,老老实实走镜像修改路线。

在允许root的设备上,用adb root之后shell权限就会变成root:

adb root adb remount adb shell setprop persist.sys.demo_test 1

如果adb root都提示失败,那说明当前固件根本没有在userdebug模式下编译,也就不存在免解锁改系统属性的捷径。

4.3 属性名和属性值的格式限制

属性名字符串和属性值不是无限长的,虽然在不同Android版本上具体数值略有差异,但开发时要遵循一个基本的安全范围:属性名别超过31字节(新版本有更大长度),属性值保守控制在92字节以内。我印象里有些新版本允许更大长度,但实际兼容性很微妙,没必要去赌边界。

除了长度,还有几个容易踩的坑:

  • 属性值里尽量别塞空格。有些版本允许,有些版本只截取到空格前。我读某个属性时发现值只有半个单词,排查了半天才发现是写入时带了空格。
  • 属性名里不要乱放特殊字符。点号、下划线是常规用法,但某些符号在SELinux配置里不好匹配,也容易引起解析问题。
  • 自定义属性名最好带项目标识,比如persist.demo.foo=1,不要用persist.abc=1这种太短的键名,既不规范,也容易跟厂商已有的内部属性冲突。

有一次我为了快速调试,写了个属性值是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.xxx=1,编译系统会自动把属性写进生成镜像里的build.prop。运行期不要试图修改只读属性,在源码层把它定死,是唯一干净可控的方式。

4.5 附:常见问题速查表

把这段时间总结的经验整理成一张速查表,遇到问题先对号入座:

现象可能原因处理方式
getprop 查不到某个属性属性名拼错或尚未写入用 getprop
setprop 后立刻读没变化无写入权限、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一下就知道哪些属性变了。这个操作简单,但确实帮我解决过不少“明明没动过它怎么坏了”的冤案。属性系统用好的时候是你调试路上的加速器,用不好就是坑你时间的大坑,希望这篇内容能帮你把它控制在自己手里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询