☰
Android系统属性SystemProperties全解:原理、权限与避坑指南
2026/10/1 3:35:37 网站建设 项目流程

搞Android开发的人,对SystemProperties应该都不陌生。翻日志会看到getprop ro.product.model,写兼容性适配方案会碰到persist.sys.usb.config,自己做ROM定制或系统裁剪时更躲不开它。它是Android系统里一个地位很高但存在感又很低的模块:平时没人提,一旦某些功能开关失灵、设备信息读取异常、SELinux权限报错,最后大概率都会追到系统属性这里来。

SystemProperties本质上是一套全局共享的键值对存储,常驻内存,从底层内核到上层App都能读。关键点在于:它能读得极快、能在开机极早期就工作、能跨进程共享,但写起来有各种限制,命名有规范,生命周期还分好几种。正是因为“看起来简单、用起来容易踩坑”,我把它单独拿出来写一篇,把原理、命名规则、权限模型、实操步骤和排查套路一次说清楚。

这篇文章适合三类人看:一类是做Framework和系统定制的工程师,需要新增系统属性给上层使用;一类是在做ROM、车机、平板等方案集成的开发者,经常要在build.prop里加料;还有一类是高级应用开发,虽然不直接写系统属性,但排查设备适配问题、读一些设备信息时能少走弯路。

先别急着去敲 setprop 命令,我们得先把SystemProperties到底是什么、背后有什么设计考量弄明白,否则后面踩了坑都不知道坑从哪里来的。

1. SystemProperties能干什么:不止是adb命令

系统属性这个名字看着高大上,实际可以理解成一张“全局白板”。任何进程往上面写一个键值对,其他所有进程都能立刻看到,不需要绑定关系,不需要第三方存储,也不依赖任何Java环境。它覆盖了Android系统里大量基础信息的传递和运行时开关控制。

1.1 开发者和用户每天都在悄悄使用它

很多人第一次接触系统属性,是看到adb shell getprop输出一大串ro.xxx、persist.xxx。但真正常见的用法远不止这些。

比如Build.MODEL、Build.VERSION.RELEASE这些大家每天都在调的API,底层很多字段就是从系统属性里读出来的。再比如开发者选项里的“USB调试”“无线调试”,打开或者关闭时,背后操作的就是一组persist.sys.usb.config、service.adb.tcp.port之类的属性。还有系统启动阶段,init进程要不要加载某个模块、系统服务要不要进入某种特殊模式,很多也是通过属性来传递信号的。

说得直白点,Android系统从开机到运行,一直在靠系统属性协调各种跨进程的信息同步。它有点像团队公用的“记事板”:谁需要什么信息,来这块板上找;谁想公开什么状态,写到这块板上。

1.2 为什么系统里已经有了Binder、数据库,还要有SystemProperties

有人会问:Android不是有Binder通信吗?有SharedPreferences、SQLite,为什么还要专门搞一个系统属性?

答案在于生命周期和读取成本。

Binder是面向服务调用的,适合一次请求一次返回,但如果只是为了读一个设备型号,每次调用都走一遍Binder链路,成本偏高。数据库和SP适合存业务数据,但它们在Java层,启动晚,而且都依赖文件读写,开机极早期根本用不了。

系统属性的设计目标完全不同:

  • 读路径极短,本质是访问共享内存映射,纳秒级返回。
  • 不依赖Java环境,Native层、内核cmdline、init阶段都能用。
  • 通过init进程统一管理写入,配合SELinux做权限管控。
  • 支持persist前缀持久化到 /data,重启不丢。

这也是为什么从开机引导到应用运行,每一个阶段都能看到它的身影。

1.3 适合存什么,不适合存什么

系统属性适合存的是:设备描述信息、全局开关、版本号、运行状态标志、跨进程同步信号。比如ro.serialno、persist.sys.debug_mode、sys.boot_completed。

不适合存的是:大段JSON、用户业务数据、日志内容、频繁变化的数据。属性区的共享内存非常宝贵,而且传统value长度限制只有92字节,往里塞大块数据早晚出事。后面我会专门讲这个问题。

2. 底层原理:属性怎么存、怎么读、怎么写的

要理解SystemProperties,先要理解它背后的三个核心角色:共享内存、property_service、SELinux权限管控。这三者决定了“读很快、写受限”的总体格局。

2.1 三个关键角色:共享内存、property_service、SELinux

第一个角色是共享内存区。系统属性不是散落在各个进程里的局部变量,而是放在一块通过mmap映射的共享内存里,路径通常叫/dev/__properties__。所有进程读属性时,直接访问这块内存映射就行了,所以读操作极快,不涉及跨进程通信。

第二个角色是init进程里的property_service。所有写入属性的请求,最终都要交给它来统一处理。它负责检查属性名格式、校验属性前缀、把值写入共享内存,并且根据属性是否带persist前缀做持久化操作。

第三个角色就是SELinux。写入属性是高风险操作,Android用SELinux对属性做了标签化管理。每个属性的key都会被赋予一个安全上下文,具体来说是一个“属性域”(property_context),进程要想写入某个属性,它的安全上下文必须在策略里被授权。这也是很多新手写属性“莫名其妙不生效”的根本原因。

2.2 读写链路完整走一遍

先看读链路,以Java层调用为例:

SystemProperties.get("ro.product.model")→ 进入Framework层JNI → 调用__system_property_get→ libc解析共享内存直接返回结果。

再看写链路:

SystemProperties.set("persist.sys.xxx", "1")→ JNI →__system_property_set→ socket/Binder与init进程里的property_service通信 → 经过SELinux权限校验、命名合法性检查 → 写入共享内存 → 如果带persist前缀,再异步写回 /data 下的持久化文件。

Native层则是直接使用<cutils/properties.h>里的property_get和property_set,绕过了Java/JNI,但底层走的逻辑和上面一样。

命令行操作常见的有:

# 查看所有属性 adb shell getprop # 查看某个属性 adb shell getprop ro.product.model # 设置属性 adb shell setprop persist.sys.experimental.flag 1 # 删除属性的“近似效果”(实际是把值清空,不会删除键本身) adb shell setprop persist.sys.experimental.flag ""

这里要敲黑板:写属性不是改了共享内存那么简单,每一步都带着权限和策略检查。所以如果你的目标进程没有权限,命令执行完了看着好像没报错,实际读出来还是旧值。

2.3 三种关键前缀和生命周期差异

系统属性的命名有很强规律,前缀直接决定生命周期和写入限制。这是所有使用系统属性的人第一个要搞明白的点。

前缀生命周期是否持久化典型用途
ro.启动后只读通常来自 /system/build.prop 等设备描述、固件版本
persist.跨重启持久化到 /data用户设置、运行时配置
sys.重启重置否系统运行状态
vendor.厂商分区加载视具体实现厂商定制、硬件状态
ctl.触发init动作否服务启停控制

先说ro.。它是只读属性,一旦系统启动并加载完毕,进程运行期间就无法再修改。所以带ro.前缀的属性,必须在编译期或者init早期就确定值,别指望运行时动态切换。常见的ro.build.version.sdk、ro.product.model就属于这一类。

再说persist.。它是少数能跨重启保留的属性。写入后property_service会把它异步写到/data/property目录下。下次开机,init在加载完基础属性后,会把持久化属性和默认值合并。因此常用于存“上次用户选择的模式”“实验开关的开关状态”。但要注意,persist.属性写在 /data 分区,恢复出厂设置或者清空 /data 后会全部丢失。

最后是sys.。它没有持久化,用来传系统运行时的临时状态,比如sys.boot_completed表示开机是否完成,sys.usb.config控制USB功能切换。重启即失,不占用持久化IO。

ctl.比较特殊,它实际上是给init下命令用的,比如ctl.start、ctl.stop,用来控制自启动服务。普通应用别碰,风险很大,而且SELinux默认也禁止非系统进程操作。

3. 实操:把一个设备信息属性从底层打通到App读取

光讲原理不过瘾,来一个实际场景。假设我们正在做一款面向行业定制的安卓设备,产品有A/B/C三个硬件版本,固件版本还想在“关于本机”页面显示出来。硬件版本号和软件版本号应该由系统统一提供,不能靠每个App自己写配置文件。

这个需求用系统属性来解决最合适:编译期注入、全进程可读、无需额外权限即可读ro.前缀属性。

3.1 为什么这个场景必须用系统属性

对比一下其他方案:

  • 放文件:App能读 /system 下的文件吗?普通应用受SELinux和挂载方式限制,大概率读不到。
  • 写进数据库:数据库在 /data 下,每个应用自己的数据库别人读不了。
  • 写死代码:换硬件版本就要重新出包,不现实。

而系统属性一旦在编译期注入,就存在 /system 分区的build.prop里,所有进程都能通过同一套API读取,无权限门槛。这个特点几乎是量身定制。

3.2 编译期注入默认值

在设备厂商的product配置里,通常用PRODUCT_PROPERTY_OVERRIDES或者product.prop文件注入属性。以PRODUCT_PROPERTY_OVERRIDES为例,在产品的BoardConfig或product.mk里追加:

PRODUCT_PROPERTY_OVERRIDES += \ ro.product.hardware.version=HW_V1.0 \ ro.product.firmware.version=FW_2.3.1

编译完成后,这些值会被写入/system/build.prop,系统启动时由init读取并注册到属性区。开机后执行:

adb shell getprop ro.product.hardware.version adb shell getprop ro.product.firmware.version

只要能看到输出,说明这步就通了。

另一种方式是init.rc里设置,常见于补丁性质或运行时初始化的场景:

on property:sys.boot_completed=1 setprop ro.product.hardware.version HW_V1.0

但注意,ro.前缀一旦过了启动早期阶段,再执行setprop ro.product.hardware.version往往会写入失败或没有任何效果。init.rc也不是所有阶段都能改ro.属性,要看节点触发时机。所以稳定做法还是编译期写入。

3.3 App和Framework怎么读

如果做的是系统内置应用,编译时直接引用Framework的隐藏API就行:

import android.os.SystemProperties; String hwVersion = SystemProperties.get("ro.product.hardware.version", "unknown");

但是普通第三方App无法直接在编译期访问android.os.SystemProperties,因为它是隐藏API。日常最稳妥的公开API是从Build接口读取系统已经封装好的字段,比如:

String model = Build.MODEL; String fingerprint = Build.FINGERPRINT;

如果必须读自定义属性,可以通过反射。反射虽然绕,但在部分场景下可以用,不过要注意Android P以后对隐藏API有访问限制,不一定稳定。更推荐的做法是:在Framework层做一层公开接口封装,比如新增一个系统服务或者往已有系统服务里加方法,应用通过Binder接口取属性。这属于Framework开发范畴,过程不复杂,但要走系统编译、系统签名、系统应用配套修改,工作量不小。

Native层要读属性就非常简单了,系统开发里直接:

#include <cutils/properties.h> char hwVersion[PROPERTY_VALUE_MAX]; property_get("ro.product.hardware.version", hwVersion, "unknown"); printf("hardware version: %s\n", hwVersion);

3.4 动态开关:从persist属性到功能生效

除了设备信息,系统属性最常用的是“动态开关”。比如产品想要一个“实验功能”开关,需求是:设置后跨重启保持,App启动时读取,决定是否展示某个新页面。

时间线是这样走的:

  1. 系统服务写入persist.sys.experimental.flag = 1。
  2. 属性值立即在共享内存生效,同时异步持久化到 /data。
  3. App启动时读取属性:
boolean enable = "1".equals(SystemProperties.get("persist.sys.experimental.flag", "0"));

这个方案的可取之处是设置和读取都极快,不依赖端口、不依赖网络,也不依赖其他服务。但要注意:属性变化没有Java层的标准监听回调。系统Framework内部有监听机制,但对普通App来说,最可靠的做法是“启动时读取”、“页面刷新时读取”,或者自己用轻量轮询。不要在业务代码里写死“属性变化马上通知我”的预期,这是很多人最开始容易误解的地方。

更稳妥的配置类玩法是:服务端下发开关 → 应用或系统服务写入属性 → 下次启动/前台切换时读到。高频实时刷新不适合用系统属性来做。

4. SELinux权限:为什么setprop总是被忽略

很多人在做系统定制时会碰到一个让人崩溃的现象:在adb里执行setprop persist.sys.xxx 1,终端看起来没有任何报错,但是getprop persist.sys.xxx读回来的还是空。用系统应用去写也一样,不生效。这时候八成就是SELinux拦住了。

4.1 系统属性权限体系是怎么运作的

Android对系统属性的写入做了双保险:第一个是传统的“谁能调SystemProperties.set”的API访问控制,第二个就是SELinux。真正难缠的是SELinux。

SELinux把属性key映射到不同的安全标签。映射规则一般在/system/etc/selinux/plat_property_contexts或/vendor/etc/selinux/vendor_property_contexts里。举个例子:

ro.product.hardware.version u:object_r:system_prop:s0 persist.sys.experimental.flag u:object_r:system_prop:s0

进程想要写入某个标签的属性,必须在自己对应的te文件里被授权。典型的授权写法是:

allow system_app system_prop:property_service set;

或者是用宏:

set_prop(system_app, system_prop)

具体路径一般是system/sepolicy/private/system_app.te、device/厂商/sepolicy等。

这里最容易踩的坑是:你以为system_app是万能的,其实不是。Android的SELinux策略是“最小授权”原则,每个进程能写的属性范围是明确列出来的,没有被allow的,一律拒绝。写入失败通常不会弹出明显错误,只会在kernel日志里留下一条avc denied记录。

4.2 快速定位SELinux拒绝问题

遇到属性写不进去,别急着改代码,先把证据链查清楚。

第一步,看有没有avc denied记录:

adb shell dmesg | grep avc adb logcat -b all | grep avc

正常的拒绝日志大概长这样:

avc: denied { set } for property=persist.sys.experimental.flag scontext=u:r:system_app:s0 tcontext=u:object_r:system_prop:s0 tclass=property_service

这行日志信息很关键:scontext是调用进程的安全上下文,tcontext是属性的安全上下文,tclass是操作类型,deniedset说明写权限没给。

第二步,确认属性在property_contexts里有明确的标签定义。如果属性key没有匹配到任何标签,SELinux通常会按默认域处理,而默认域往往没有写权限,同样会被拒。

第三步,确认授权规则有没有加对地方。系统权限和vendor权限隔离,不要试图跨域乱授。属性前缀如果带vendor.,通常要到device侧的sepolicy目录加规则。

第四步,临时验证时可以把SELinux切到Permissive模式:

adb root adb shell setenforce 0

设置后看属性能不能写入。能写入,基本就坐实了SELinux的问题。但那只是临时排查手段,正式产品必须写准策略并回到Enforcing模式,全局Permissive在真机上是不可接受的。

4.3 不同分区之间的权限差异

还有一个经常被忽略的点:Android只保证“读”相对开放,但“写”的分区隔离越来越严格。尤其从Android 8/10开始,system分区、vendor分区、product分区的属性被安排进了不同的属性区域,跨区写更难。

你可能会遇到这种情况:system_server要写一行属性,规则也加了,属性也有标签,但还是失败。这时候要检查是不是在vendor的sepolicy里想给system域授权,或者反过来。原则上,属性归哪个分区管,授权规则就放对应分区的sepolicy里,别在两套策略里混着写。

顺便提醒一下,很多ROM和方案厂商的文档里写着“把SELinux关了就行”,这是懒人逻辑,能省事一时,后续真机反馈、兼容性问题会头大。做系统底盘的人,规矩还是要守的。

5. 常见坑与避坑清单

我整理了平时群里问得最多的几类问题,这些问题基本覆盖了90%的系统属性使用“翻车”场景,大家可以直接对照排查。

5.1 属性写不上,到底是谁在拦

把这几个可能的原因按出现概率排个序:

现象大概率原因确认方式
setprop后没变化,无报错SELinux拒绝dmesg查看avc
setprop报权限错误非root或系统签名不足检查adb权限
ro.属性在启动后无法修改ro前缀本身只读换persist/sys前缀
App进程写入被静默忽略应用沙箱+SELinux双重限制logcat搜索avc/system_property
厂商分区进程写system属性失败分区隔离策略检查property_contexts分区归属

我的建议是:遇到写不进去,第一件事不是改代码,而是把SELinux和研究写入发起方的安全上下文搞清楚。任何属性写入问题,八成都能在avc: denied { set }日志里找到答案。

5.2 persist属性“不生效”的真实原因

用persist.前缀最容易出问题的是“值写了,重启后变成旧值”或者“完全没持久化”。拆开说:

第一个常见原因:属性名变更过。比如以前用persist.sys.exp.flag,后来改成persist.sys.experiment.flag,旧的值可能还留在 /data/property 里,新属性却从没写入成功。这时候先看SELinux日志,再清理旧持久化文件。

第二个常见原因:写入和读取的时机不对。persist.属性虽然在写入瞬间已经生效,但只对当前分区内其他进程生效。如果服务A写入后,进程B已经在运行且之前读过旧值,B不会主动感知变化,需要重启B或者B重新读取。

第三个常见原因:/data 分区不稳定。持久化本质是落盘,如果 /data 分区异常、剩余空间不足、文件系统只读,持久化也会失败。这时往往不只是属性问题,其他文件写入也会报错。

有一个快速排查经验:直接重启看值是否还在。如果重启后新值丢了,重点查SELinux和/data;如果重启后还在,但某个进程读不到,重点查进程读取时机和缓存。

5.3 别再往系统属性里塞大字段了

这个必须单独拎出来讲,踩的人太多了。

传统Android属性有两个硬限制:key长度不能超过32字节,value长度不能超过92字节。虽然Android 8以后引入了长属性支持,部分场景可以突破这个上限,但它内部是单独处理的,而且很多老接口、老进程、老脚本并不兼容。

更重要的是,系统属性的共享内存总量非常有限,整个属性区加起来也就几百KB级别。这是给系统控制信令用的“仪表盘”,不是让你当配置文件用的“仓库”。

实际项目中我看到过有人把整个Base64编码的图片压缩串塞进系统属性,结果不但写不进去,还拖慢了启动速度。正确做法是:大字段放文件,属性里只放文件路径或一个标志位。想存结构化配置,用 /data 下的文件或数据库,系统属性只负责“告诉别人这里有个配置、配置在哪”。

5.4 高频写属性:性能陷阱

还有一类反面教材:在循环里或者频繁触发的事件里调用SystemProperties.set。

读属性确实是共享内存直接读,很快,但写属性不是。写属性要经过property_service的权限校检、值更新、持久化调度,如果带persist.前缀,还会触发落盘写操作。高频写属性会让system_server的负载直接上升,日志里全是属性更新记录,严重时整个系统都会卡顿。

正确姿势是:高频统计类数据放内存里聚合,隔一段时间再写一次属性;一些过程状态不放系统属性,改成进程内全局变量。系统属性适合低频小数据,这个底线不要破。

5.5 读完这篇之后实操建议

照着我自己的使用习惯,送大家三句话:

第一,编译期能定的设备信息,用ro.前缀,编译期写入,别想运行时改。 第二,运行时开关想跨重启保持,用persist.前缀,但务必先确认SELinux授权,并写清楚配置的清理规则。 第三,临时通知用的状态,用sys.前缀,重启即失,干净利落,不给系统留“记忆包袱”。

从项目经验角度讲,我还想多啰嗦一句:别把系统属性当万能配置中心。它解决的是“全局共享、早期可用、跨进程同步”的问题,不是给你存用户数据、业务日志、超长配置的地方。属性用得好,项目清爽还不容易出问题;用得烂,排查问题的时候真的会被它折磨到怀疑人生。

系统属性这个设计在Android里看起来朴素,但它承担的角色极其重要。把它的脾气摸透,对做系统定制、Framework开发,还有那些需要在设备兼容性上做深度的项目,帮助都不小。这一篇把原理、权限、实操和常见的坑都过了一遍,后面大家自己动手加属性、改开关时,照着顺序走基本就稳了。

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

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

立即咨询