首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
深入Android属性系统:从getprop到property_set的完整链路
📅 2026/9/14 3:33:55
✍️ 爱科研究院
👁 阅读 3,247
做Android系统开发几乎每周都能遇到属性没生效的怪事明明在build.prop里塞了ro.debuggable1刷完机adb root照样不给权限写了一个persist.sys.debug去控制开关重启之后值直接消失更邪门的是有些属性在eng版本随便写到了user版本就各种permission denied。这类问题排查到最后基本都会落到同一个底层机制上——Android属性系统。这篇是属性系统系列的第6篇继续系统源码分析的部分重点沿着读取、写入、权限校验、变化分发、持久化这几条主路径把源码里的关键分支逐个拆开搞清楚属性从getprop到内核共享内存再从property_set一路走到init服务端的完整链路。上一篇文章已经分析了init进程在启动早期如何初始化属性区域、如何创建/dev/socket/property_service这个socket以及属性服务线程的基本循环。这篇会顺着调用链继续往下挖深入到bionic库的属性访问接口、init服务端的权限校验实现、属性变化如何触发系统行为以及persist.*和ro.*这两类特殊属性的生命周期管理。内容上以Android 10以后的新版属性实现为主适当对照旧版设计的差异毕竟现在的系统源码里已经是serialized property area的天下了。1. 从getprop到共享内存属性读取的完整链路1.1 属性区域初始化之后读到的到底是什么先说结论属性不是一个一个独立存储的小文件而是整块映射到进程地址空间的共享内存。所有进程读属性本质上是直接读内存不需要经过任何跨进程通信这也是getprop能瞬间返回海量属性的根本原因。Android 10以后的属性区域初始化入口是__system_property_area_init它会创建一个名为/dev/__properties__/property_info的共享内存文件早期版本是匿名mmap然后在这块共享内存上建立serialized_property_data结构。这个结构用连续内存块维护了一份属性名与属性值的映射底层用二分查找定位属性项。对比Android 8.0时代的trie树实现序列化方案的好处是访问更规整属性区域的扩容也更灵活property_info这部分还单独用一套blob序列化格式存了一套名称到SELinux上下文的映射供权限校验使用。这里有个容易忽略的细节__system_property_area_init有的版本叫__system_property_area_init有的版本拆成了__system_property_serialized_area_init不管名字怎么变做的事情都是把一块mmap出来的内存交给属性注册表去管理和索引。属性写入不是简单往内存里塞字符串而是要经过PropertyRegistry::Add这类入口先在有序列化数据区寻找空间写入属性名和值再更新索引。所以如果你在源码里找prop_info结构体会发现它里面保存的其实是属性名的相对偏移量而不是裸指针这是为了让所有映射同一块共享内存的进程用各自的基址也能正确解引用。1.2 __system_property_find与属性读取的底层实现用户态进程读属性最常见的是通过C库的__system_property_get。这个函数的实现非常短核心逻辑就是先find再readint __system_property_get(const char* name, char* value) { const prop_info* pi __system_property_find(name); if (pi ! nullptr) { return __system_property_read(pi, nullptr, value); } value[0] \0; return 0; }__system_property_find才是真正的查找函数。它拿到属性名后会在共享内存的序列化数据区里先读名字再做一次二分查找找到对应的prop_info。如果找不到返回nullptr此时__system_property_get会往value首字节写\0并返回0。这也是为什么很多C代码里调用property_get后不需要判空——它永远会给你一个以\0结尾的字符串最多是空串。需要注意的是__system_property_get从名字上看是获取但对于ro.debuggable这类只读属性同一个函数也能读因为只读管的是写操作读在权限模型上不设门槛。任何进程只要能mmap到共享内存就能读全部属性这种设计在Android里一直如此所以在安全设计上写入权限才是真正需要层层设卡的地方。__system_property_read的返回值是属性值的字节长度。很多人在调用时忽略了这样一个细节如果属性名不存在返回0如果属性存在但值是空串返回0。这两种情况在返回值上无法区分所以在业务代码里不要用返回值判断属性是否存在要判断value[0]是否为\0。1.3 getprop输出顺序为什么不稳定用adb shell getprop输出全部属性时属性顺序在不同设备、不同启动阶段看起来不太一样。这是因为getprop走的是__system_property_foreach它会遍历共享内存里的属性数据区遍历顺序严格依赖属性写入时的内存排布和索引结构而不是按字典序或者按构建产物里的顺序。另外getprop命令本身由toolbox实现它会读取系统属性、boot属性以及用户自定义属性输出时会做一层排序但排序的规则在不同Android版本上有调整。调试时如果你发现某个属性在getprop输出里找不到不一定代表属性不存在更常见的情况是属性名写错了、名称超长被截断或者属性区域已经写满导致新属性根本没写入成功。后面会详细说这两个边界场景。2. 写属性过五关斩六将property_set到HandlePropertySet的权限链路2.1 客户端一次property_set走了哪些路从应用层通过SystemProperties.set(persist.sys.xxx, 1)设置属性到最终生效中间隔了哪几层先把调用链捋一遍Java层android.os.SystemProperties#set通过JNI调到native的property_set。native层调用bionic的__system_property_set(name, value)。bionic在__system_property_set内部做了三件事校验属性名合法性、连接/dev/socket/property_service这个socket、通过socket向init进程发送PROP_MSG_SETPROP消息。init进程的property service线程从socket取出消息解析出属性名和值进入HandlePropertySet做权限校验。校验通过后init内部调用__system_property_update属性不存在时走__system_property_add把属性写入共享内存。写入成功后如果需要init还会触发属性变化回调与rc触发器。关键点是普通进程没有权限直接修改共享内存里的属性所有的写操作都必须汇聚到init这个唯一权威服务端。为什么这么设计因为属性系统是所有进程共享的全局配置如果每个进程都能直接写共享内存那ro.*的不可变约束、SELinux的访问控制、状态一致性就全部崩了。集中到一个服务端做仲裁才能把权限模型落到一个可控的进程里。2.2 init服务端的权限校验链进入HandlePropertySet之后真正的硬骨头是权限校验。这段校验主要分四个层次层层递进第一层属性名合法性检查源码里对应is_legal_property_name。它会把每个字符都过一遍只允许大小写字母、数字、下划线、点号和几个指定符号同时检查名字长度不超过32字节。这个检查在客户端bionic层其实也做了一遍但服务端必须再校验一次因为socket进来的请求不能完全信任。第二层基于uid/gid的传统权限检查对应源码里的check_perms。系统在启动时会加载一个属性权限配置表里面记录了属性名前缀与允许的uid/gid组合。典型地ctl.开头的控制属性只允许root或system uid写ro.前缀的只允许init在早期设置persist.前缀的普通app也可能被拒绝。如果你的应用不是系统签名应用写persist.sys.xxx大概率就是在这一层被拦下的。第三层SELinux的MAC检查对应check_mac_perms。它会根据调用者的SELinux上下文和目标属性在property_contexts里映射的上下文做判断。这一层是现代Android属性权限的核心也是排查权限问题最容易忽略的地方。可以简单理解成uid/gid检查是身份认证SELinux检查是操作授权两者都过了才放行。第四层针对特殊属性前缀的专项处理。比如ro.前缀的属性如果已经存在直接拒绝更新persist.前缀的属性会走持久化写入逻辑ctl.start、ctl.stop这类控制属性会被转成init的service控制命令并不真正写入普通属性区域。2.3 SELinux的property_contexts与常见报错对应关系property_contexts文件在源码仓库里位于system/sepolicy/private/property_contexts编译后生成plat_property_contexts和vendor_property_contexts等文件分别对应system分区和vendor分区的属性上下文映射。文件内容大致是# 属性名支持前缀匹配 SELinux上下文 ro.build. u:object_r:build_prop:s0 persist. u:object_r:persist_prop:s0 sys.powerctl u:object_r:powerctl_prop:s0 vendor. u:object_r:vendor_prop:s0当init判断一个属性是否可写时会先根据属性名找到对应的SELinux类型再检查调用进程的域domain是否对该类型有set权限。所以你在logcat里看到类似avc: denied { set } for propertysys.powerctl ...的日志时说明SELinux层直接拒绝了操作。这个设计也解释了为什么有些属性在eng/unlock设备上能写换到user版本就写不了——user版本的SELinux策略更严格很多域的写权限被收紧。解决这类问题不是去改代码绕过校验而是要在对应的.te策略文件里为业务进程补充set_prop权限或者选一个有权限的代理进程去设置属性。注意property_contexts里用的tools.precompiled_selinux等编译产物在运行时不可直接编辑改属性权限必须重新编译SELinux策略否则重启后配置被恢复。3. 属性不只是状态变化分发与整机控制3.1 init如何把属性变化变成action触发器属性系统最巧妙的设计在于属性值一旦变化可以驱动整个系统做出联动反应。这个机制最典型的使用场景就是.rc文件里的on property:xxx触发器。on property:sys.boot_completed1 start some_service当init属性服务端的写操作完成后会去检查当前系统里所有注册的trigger凡是匹配property_namevalue的触发器都会被激活。这个过程的源码实现在init的ActionManager里大致的调用路径是HandlePropertySet-PropertySet-property_changed通知 -queue_builtin_action或trigger_property_triggers。需要特别指出属性触发器只在value发生变化或者从无到有时触发一次。如果服务端收到一个设置请求但值跟当前共享内存里存的完全一样这个trigger是不会重复触发的。写脚本或者做自动化测试时如果你反复设置同一个属性期望每次都能触发那就踩坑了必须在两次设置之间把值复位或删除。3.2 sys.powerctl一个属性完成整机控制属性系统里最能体现属性作为系统控制通道价值的是sys.powerctl这个属性。Framework要关机、重启或者进入bootloader时不是直接调用reboot系统调用而是通过SystemProperties.set(sys.powerctl, reboot,bootloader)往这个属性里写值。init收到这个属性写入后会走HandlePowerctlMessage的专项流程解析出命令reboot/shutdown、参数bootloader/recovery/用户自定义reason再执行真正的电源管理动作。这样做有几个明显好处所有电源操作都汇聚到init这个统一入口权限校验由属性系统的SELinux机制接管上层调用者不需要具备root权限也能请求重启而且可以在重启前让init完成清理动作。从源码阅读的角度看sys.powerctl的处理路径跟普通属性写入有本质区别它根本不走__system_property_update去更新共享内存里的值或者只是顺带记录一下而是直接解析命令字符串然后分流到对应的系统操作。这也解释了为什么sys.powerctl的属性值你getprop经常读不到——它更像一个命令通道而不是持久化配置。3.3 framework与native进程监听属性的通用模型除了init的rc触发器系统和应用还会通过属性变化监听来响应配置改变。Java层可以注册SystemProperties.addChangeCallbacknative层可以用__system_property_wait或者轮询__system_property_foreach。一个容易踩坑的点是addChangeCallback的回调并不保证在属性变化的瞬间立即执行它依赖binder的PropertyChangedListener分发回调会跑到binder线程。如果你的回调里做了耗时操作或者直接调用了SystemProperties.set很容易引起死锁或者回调风暴。native层的__system_property_wait则是阻塞等待属性区域的变化序列号它配合__system_property_serial可以做到在特定属性变化时唤醒但要注意这个接口等待的是任意属性变化而非指定属性变化唤醒后还需要自己重新读取目标属性做二次判断。很多三方库在监听属性时写法粗糙等唤醒后不校验属性名和值结果把其他属性的变化也当成自己关心的导致误触发。4. 持久化、只读与加载顺序属性生命周期的特殊节点4.1 persist.*的落盘与恢复机制普通属性写入共享内存后重启就没了。persist.*前缀的属性要跨重启保留原因是init在处理这类属性时多了一个持久化写入步骤。源码里persist.*属性的持久化存储文件通常位于/data/property/persistent_properties在部分Android版本上路径是/data/property目录下。写入时init先把属性更新到共享内存同时把属性名和值序列化写入这个文件。系统启动过程中init会在属性区域初始化完毕之后专门读取这个持久化文件把所有persist.*属性重新加载回共享内存保证上层在启动早期就能读到上次保存的值。这里有两个容易碰到的问题。第一persist.*属性的初始化时机晚于ro.*属性所以早期的native进程如果比init加载持久化属性还早去读读到的可能是空值。第二persist.*属性掉电丢失不全是玄学如果系统异常关机写文件的操作没完成数据就丢了。对可靠性要求高的场景不能把persist.*当成数据库用频繁写入还容易磨损存储应该有节流和容错。4.2 ro.*为何改不掉不可变规则的实现取舍ro.*前缀的属性在设计上不可变一旦写入就永远不能覆盖。这个不可变不是靠SELinux策略实现的而是init属性服务端在property_set处理逻辑里直接用字符串前缀判断如果属性名以ro.开头而且属性已经在共享内存中存在直接返回失败连SELinux检查都轮不到。这也带来一个经典问题我改完ro.debuggable或者ro.secure重新编译刷机为什么重启后getprop看到的还是旧值原因是ro.*属性在init启动早期从/system/build.prop、/vendor/build.prop等文件加载后就不会再更新了。如果你只是改了文件没改编译产物或者文件被重复加载但目标属性在共享内存中已经被占住那一切努力都是白费。更隐蔽的情况是有些厂商会把ro.*属性的值写死到boot image里的其他位置靠改build.prop根本覆盖不了。所以排查ro.*不生效问题正确思路是看属性值是从哪个文件里加载出来的而不是盲目修改某个prop文件。可以用adb shell getprop | grep ro.xxx看当前值再对照源码里的PropertyLoadBootDefaults加载文件顺序推断来源。4.3 build.prop不是全部属性加载优先级梳理属性系统里的属性不是在某个统一的地方一次性加载完的而是分来源、分阶段、分优先级进来的。以Android 11以上典型设备为例主要加载来源包括内核cmdline与bootconfig对应ro.boot.*系列属性。default.prop/system/build.prop系统级属性。vendor/build.prop、odm/build.prop、product/build.prop等分区属性。persist.*持久化属性恢复。init脚本里显式setprop设置的属性和rc触发器。源码中PropertyLoadBootDefaults的加载顺序决定了同名属性谁覆盖谁。一般原则是后加载的同名属性覆盖先加载的。ro.*因为是只读先加载的就赢了。所以想覆盖ro.*属性必须保证你的值出现在对应分区build.prop的最早加载路径里或者在init早期通过setprop设置成功。persist.*大部分是运行时动态产生的不参与build.prop冲突因为它更晚加载。5. 源码分析后更容易踩的属性坑5.1 长度、字符集与空值老生常谈但常犯错误的是长度限制属性名最大32字节属性值最大92字节部分版本对persist.*扩展过但通用上限还是PROP_VALUE_MAX92。源码层的is_legal_property_name和属性更新逻辑都会做长度校验超过限制直接拒绝。常见症状是SystemProperties.set返回正常其实内部吞掉了部分错误但getprop里面属性值被截断或者根本没有这个属性。另外属性值本质上是一个C字符串不能包含\0。你从一个文件里读出的配置如果自带\0或者非法UTF-8字符写进去的值会被截断或者直接导致属性区域的序列化结构异常。代码里设置属性之前最好先做一次长度与字符集的防御性校验不要指望系统帮你处理。5.2 用错监听方式的性能风险属性读取性能虽高纯内存操作但property_set是跨进程socket通信一次写入的耗时可能在几十微秒到一两毫秒之间波动。在高频逻辑里反复调用set或者写一个循环每秒设置同一个属性既会引起socket消息风暴也会触发大量属性变化回调拖慢init事件循环。如果需要监听多个属性建议不要注册多个addChangeCallback而是合并成一个回调统一过滤更不要用while (true) { property_get(...); sleep(...); }这种方式去轮询既费电又浪费CPU。官方没有提供指定属性变化回调的Java接口实际工程中一般用回调Handler去重或者自己包一层监听器。5.3 user版本、vendor属性与控制属性的权限盲区很多从eng/userdebug版本带入user版本的bug本质上都是权限模型不一样导致的。debug版本里adb root后是root域大部分属性都能写user版本的shell域、app域权限大幅受限persist.、debug.、sys.*都可能被SELinux拒绝。遇到这类问题第一时间去抓logcat -b all | grep avc看有没有avc: denied日志有的话就按属性名去对应.te文件里补权限。另一个盲区是vendor.*属性。Android 8以后system与vendor的SELinux策略分离system进程写vendor.*属性经常被拒因为vendor属性由vendor域的属性服务掌控。反过来vendor进程想写sys.*属性也可能因为system的property_contexts策略不允许而失败。跨分区写属性务必先确认目标属性属于哪个分区域别指望root权限能为所欲为。控制属性ctl.start、ctl.stop更特殊它们不落在普通属性存储区而是由init转换为service的start/stop动作。如果业务代码用SystemProperties.set(ctl.start, myservice)想拉起一个native服务注意属性值不是随意填的必须跟rc文件里定义的service名完全一致而且调用进程要有对应SELinux域的start权限。从ps里看到服务没起来时先看init日志再看SELinux审计日志基本能定位。最后说一个我自己的排查习惯。遇到属性系统相关的问题我不会急着翻业务代码而是先做三件事adb shell getprop看当前属性值adb shell getprop | grep 关键字确认属性是否有值再adb shell dmesg或者logcat -b all | grep -E avc|property看有没有权限相关的审计日志。如果在init早期还会去抓init进程自己的输出。把这三步做完问题基本能缩到没写进去写进去但重启丢了权限拒绝这三个大方向之一再回到源码里对应分支效率会高很多。这次把属性读取、写入、权限、变化分发、持久化几条链路梳理完之后下一篇文章我打算具体分析一个真实案例一个persist.vendor.属性在OTA升级后丢失的完整排查过程到时候会把perfconfig、idme、persist文件擦除时机这些点串起来讲。如果你也在做系统定制或者Framework开发遇到属性问题时可以直接按这套思路定位省得反复试错。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/9/14 3:33:55
WebSocket协议栈深度拆解:从握手心跳到生产级实践
2026/9/14 3:33:54
Harness 深度解析,这次用 TaoToken 让 Codex 走通仲裁代码
2026/9/14 3:33:54
C#实现PDF数字签名删除的技术解析与实践
2026/9/14 4:24:01
FOC算法MATLAB仿真:面向电机调试的透明化建模方法
2026/9/14 4:24:01
基于Python+OpenCV的车牌识别系统:从图像处理到GUI实现
2026/9/14 4:24:01
k-form-design 生成代码在 Vue 里报错?把报错贴给走 TaoToken 的 Codex 排查
2026/9/14 4:24:01
Cadence硬件设计实战:STM32最小系统PCB从原理图到量产全流程
2026/9/14 4:24:01
螺杆支撑座预压技术:精密机械传动的关键选择
2026/9/14 4:18:57
手表App开发选型指南:避开芯片、环境、蓝牙三大坑
2026/9/14 0:03:40
KCF目标跟踪算法与OTB工程实现:毕业设计实战解析
2026/9/14 0:03:40
Megatron-LM 推理实战指南:基于 Megatron Core 高层 API 的离线推理与 OpenAI 兼容服务
2026/9/14 0:03:40
语音情感识别实战:Keras实现LSTM、CNN、SVM与MLP多模型对比
2026/9/13 0:01:25
拯救者Y7000黑屏故障排查与维修实战指南
2026/9/14 2:50:57
AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验
2026/9/13 0:01:25
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化