Android系统属性SystemProperties全解:原理、权限与避坑指南
搞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.versionHW_V1.0 \ ro.product.firmware.versionFW_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_completed1 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启动时读取决定是否展示某个新页面。时间线是这样走的系统服务写入persist.sys.experimental.flag 1。属性值立即在共享内存生效同时异步持久化到 /data。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 propertypersist.sys.experimental.flag scontextu:r:system_app:s0 tcontextu:object_r:system_prop:s0 tclassproperty_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查看avcsetprop报权限错误非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开发还有那些需要在设备兼容性上做深度的项目帮助都不小。这一篇把原理、权限、实操和常见的坑都过了一遍后面大家自己动手加属性、改开关时照着顺序走基本就稳了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →