
1. 属性系统到底解决什么问题1.1 从一次系统调试说起先讲个实际场景。有次我在调一个设备休眠唤醒的问题现象是机器休眠后偶尔会自己醒过来抓了kernel日志发现是某个外设的中断把它唤醒了。我当时的处理思路是写个脚本在系统进入休眠前关掉这个外设的电源唤醒后再打开。但问题是这个外设的驱动在kernel态而我的控制逻辑放在应用层两边怎么通信有人会说用socket、用binder都可以但这些都是“重量级”方案。前者需要维护一个常驻服务后者需要注册系统服务对于这种“设置一个标志让另外一方知道”的简单需求Android其实早就提供了一个轻量级方案——属性系统Property System。属性系统是Android里一个非常基础但又特别容易被忽视的机制。它本质上是一个全局的键值对key-value存储服务任何进程都可以通过它来写入或读取配置项。从底层驱动的开关控制到上层应用读取设备型号、系统版本走的基本都是这套东西。1.2 属性系统在整个Android里的定位很多人第一次接触属性系统是在写build的时候往build.prop里加东西或者在adb shell里敲getprop、setprop。但属性系统的覆盖范围远不止这些。从架构层级上看它横跨了这么几层Native层property_get/property_set这是最底层的C接口init进程、zygote、各种native daemon都在用。Framework层JavaSystemProperties.get/SystemProperties.set本质上是通过JNI调用到native的接口。应用层通过android.os.SystemProperties隐藏API或者Build类间接读取。跨进程广播属性变化后系统通过PropertyChanged机制通知监听方这其实是一条隐藏的“消息总线”。我个人更愿意把它理解成两个东西的结合体一个全局配置中心用来存各种系统级的配置和运行时状态一个轻量级事件通知机制属性变了以后能实时通知到关心它的人。这样一来你就知道为什么它这么重要了kernel、init、native服务、Java层、App层所有层级都在用同一套东西做状态同步而且这套东西是全局共享、实时生效的——不需要重启进程不需要重建缓存写进去立刻就能读到。1.3 全网都在搜“属性系统”但搜到的资料多半是零散的说来也巧我最近在查一些材料的时候看到好多人在搜“Android属性系统”“property system”搜出来的结果要么是讲getprop命令怎么用要么是讲属性文件怎么编辑很少有把“原理”讲透的。这篇博文我打算从最基础的用法出发一步步拆到它在内存里到底长什么样、权限是怎么控制的、init进程做了什么、上层是怎么监听的。目标很明确让你读完以后既能解决日常的配置需求也能在遇到属性相关的疑难问题时知道从哪一层、哪个环节去排查。这篇文章适合的读者包括做Android系统开发、BSP驱动调试、框架层开发的同学以及想往系统方向深入的App开发。如果你只是偶尔改个build.prop也可以看看至少能知道改的东西最终去了哪里、为什么有时候改了不生效。2. 属性系统的核心机制与实现原理2.1 属性的基本形态全局共享的键值对先不看代码我们从概念上理解。Android属性系统维护了一个全局的哈希表或者更准确地说一个共享内存中的属性映射区。每个属性由“名称”和“值”组成名称通常是点分形式类似ro.product.model、persist.sys.timezone、sys.boot_completed。值就是一个字符串。从使用者的角度看它的API非常简单// C层 #include cutils/properties.h char value[PROPERTY_VALUE_MAX]; property_get(ro.product.model, value, unknown); property_set(persist.sys.timezone, Asia/Shanghai);// Java层 import android.os.SystemProperties; String model SystemProperties.get(ro.product.model, unknown); SystemProperties.set(persist.sys.timezone, Asia/Shanghai);你可能会觉得这不就是一个全局Map嘛有什么可讲的关键在于它的实现细节这不是一个普通的进程内Map而是一块所有进程共享的内存区域。任何进程写入属性其他进程不需要通过IPC就能直接读到最新值。那它是怎么做到跨进程同步的谁负责保证并发安全权限又是怎么控制的这几个问题才是属性系统的精髓。2.2 属性存储区域共享内存与mmap属性的存储核心在property_area这是一块通过mmap映射的共享内存。整个流程大概是这样的init进程启动时创建一块匿名共享内存ashmem大小由系统配置决定。init通过__system_property_area_init初始化这块内存的结构建立属性区的基础布局。后续的每个属性都会被分配到一个prop_info结构体里存放在这块共享内存中。其他进程通过__system_property_area_init的映射关系把同一块共享内存映射到自己的进程空间。这就解释了为什么getprop读属性那么快它根本没有走任何序列化、IPC的流程本质上是直接读自己进程里那块共享内存的某个地址。读就是一次内存拷贝的事。我也顺便说明一下属性值在共享内存里是按顺序排布的新添加的属性会追加到属性区的尾部。如果属性区满了setprop就会失败错误码通常是PROP_ERROR_INVALID_VALUE或者没有反应。早期Android版本的属性区只有4KB经常有人反馈“属性写不进去”后来才慢慢扩大新版本里已经放宽到128KB甚至更大。2.3 属性的写入流程谁在把关读简单写就复杂一点。你调property_set最终会走到libc的__system_property_set。这时候并不是直接把值写进共享内存而是要通知init进程来做这件事。为什么不直接写因为属性系统需要统一的权限管理不能让随便一个App都能把ro.开头的只读属性给改了更不能让应用随便改掉系统安全相关的属性。所以写入的流程必须过一道“审核”。写入流程大致是调用方请求设置属性namevalue。libc通过socket连接通常是/dev/socket/property_service把请求发给init进程。init进程收到请求后做一系列的检查和过滤具体检查项见下文。检查通过后init进程才真正往共享内存里写入值。写入的同时init还会检查有没有注册了该属性的“触发动作”trigger有的话就执行。这里面最核心的就是第3步的检查我把常见的检查项整理成了一个表格检查项说明失败表现属性名合法性必须以点分形式命名字符集受限setprop报invalid name属性名前缀是否允许设置ro.开头的只读属性静默失败或报权限错误SELinux权限根据property_contexts中定义的type对应权限报avc: denied属性值长度值不能超过PROPERTY_VALUE_MAX92字节新版为92字符设置失败保活属性默认不允许普通属性驻留persist.有特殊处理重启后丢失其中容易被踩坑的有两个ro.前缀的属性一旦被设置就不能再修改除非重启。如果你想在运行时动态改变只读属性抱歉不行。persist.前缀的属性init会额外把它们同步写入/data/property/持久化目录重启后自动恢复。这也是Android系统能记住时区、音量、各种“记住上一次状态”的基础。2.4 属性上下文件SELinux如何为你把关很多人对属性系统“写不进去”的问题非常头疼我见过最多的就是SELinux权限导致的失败。Android从5.0以后全面启用了SELinux属性系统当然没有被放过。在property_contexts文件通常是/system/etc/selinux/plat_property_contexts和/vendor/etc/selinux/vendor_property_contexts里每个属性前缀都被映射到一个SELinux类型上。举个例子ro.build.type u:object_r:build_prop:s0 persist.sys.timezone u:object_r:timezone_prop:s0 sys.boot_completed u:object_r:sys_prop:s0也就是说某个进程如果想去设置一个属性它的SELinux domain必须对这个属性的type有set权限。如果权限不够property_set调用会触犯SELinux策略然后被打上一条avc: denied日志。这种设计是刻意为之的权限控制必须发生在“入口处”而不是依赖每个进程自觉。所以排查“属性写不进去”的问题时第一步永远是先看logcat里有没有SELinux denied记录再去怀疑别的。提示在开发阶段你可以临时用adb shell setenforce 0关闭SELinux但如果关掉以后问题就消失说明八成是属性权限的问题。千万别在生产环境这么干安全回调是底线。2.5 属性变化监听一条隐藏的事件总线属性系统最有意思的地方是它自带的“事件通知”能力。init进程维护了一个轮询线程专门监听有没有属性发生变化。一旦发现某个属性被设置它就会在属性区更新对应的值。检查属性触发动作property trigger形如on property:sys.boot_completed1的自定义动作。如果有匹配的动作就在init的上下文里执行对应的命令列表。这套机制被大量用于系统启动流程。举个最常见的例子很多系统服务要等“开机完成”这个状态就是在等sys.boot_completed被置为1。zygote、system_server、各种自定义native服务都能通过这种方式实现启动顺序的控制。对于上层AppFramework也提供了属性监听的封装核心在SystemProperties的addChangeCallback机制。比如在Java层你可以这样做SystemProperties.addChangeCallback(new Runnable() { Override public void run() { // 属性变化后触发注意这里是native回调不保证在UI线程 } });不过要提醒一句这个callback只能告诉你“有属性变了”不会告诉你具体是哪个属性你得自己再去get一遍看是不是你关心的那个。这也是框架层在实际使用中一个很反直觉的地方。3. 属性系统的工程实践从配置到调试3.1 添加一个自定义属性从源码到底层的完整链路如果你想在系统里自定义一个属性来传递状态或者给固件加一个“出厂序列号”完整的链路是什么样的以最常用的“编译时固化”为例第一步在源码中定义。如果是平台级的配置通常写在build/make/core/main.mk或build.prop生成相关的mk文件里PRODUCT_PROPERTY_OVERRIDES \ ro.custom.version1.0 \ persist.custom.featuretrue这会把属性固化到/system/build.prop或/vendor/build.prop系统启动时init会把它们加载进属性区。第二步在SELinux策略中添加访问权限如果是运行时设置的话。如果只是只读的ro.属性不需要额外权限因为ro.属性只能由init加载其他进程只有读权限。但如果要运行时可写你需要在property_contexts里给属性添加一个typepersist.custom.feature u:object_r:custom_prop:s0在te文件里授予对应domain写权限allow system_app custom_prop:property_service set;第三步验证。编译刷机后用getprop检查属性是否存在用setprop测试是否能改如果是persist属性记得重启验证持久化是否生效。这套流程我在多个项目里反复走过核心心得是属性不是“写个key-value就完了”它牵涉到权限体系、持久化策略、启动时机任何一个环节没想清楚后续调试都会很痛苦。3.2 启动阶段各分区属性加载顺序属性系统在开机时的加载顺序是很多人容易忽略但又非常重要的一点。简单梳理一下init进程最早启动解析/init.rc等脚本同时开始加载属性。首先加载的是/system/build.prop、/vendor/build.prop、/odm/etc/build.prop这些“编译期固化”的属性。接着加载/default.prop或者叫/prop.default新版本里路径有调整。再加载/data/property/下的持久化属性这些是从上一次运行保留下来的。最后init会执行rc脚本里定义的on property:xxx动作逐步拉起相关服务。这个顺序决定了你的属性在哪个阶段可见。如果你硬要在/system/build.prop加载之前去读一个属性结果自然是空的。实操中驱动或native服务经常会遇到“初始化时读不到某属性”的问题排查的第一步就是确认这个属性是在哪个阶段加载的。3.3 常用调试命令与排查手法属性系统的调试主要靠几个命令命令作用常见用法getprop查看所有属性getprop、getprop ro.product.modelsetprop设置属性setprop persist.sys.timezone Asia/Shanghaiwatchprops实时监听属性变化调试时打开观察ls -lZ /dev/__properties__查看属性区共享内存确认属性区大小和权限我平时排查属性类问题时固定套路是先确认“谁能改”查property_contexts里这个属性前缀对应的SELinux type。再确认“是不是被拒绝”logcat -b all | grep avc看看有没有denied记录。再确认“改完去哪了”区分这个属性是ro.只读重启前不可变还是persist.持久化到/data还是普通属性重启后消失。最后确认“有没有触发动作”如果设置了属性但init没有反应去查init.rc里对应的on property:块是不是没配对、时机是不是不对。这个排查思路我用了很久基本能覆盖90%的属性问题。3.4 属性持久化与写放大问题这里单独讲一下persist.属性因为它牵扯到存储。persist.属性会被同步写入到/data/property/目录下的持久化文件里。如果业务方频繁地去set一个persist.属性每次变更都会触发一次文件写入这在嵌入式设备上会带来“写放大”问题长期来看对flash寿命有影响。所以我的建议是不要把频繁变化的值做成persist.属性除非真的有重启后恢复的需求。如果确实需要频繁持久化考虑用SettingsProvider或数据库它们的批量写入策略比属性系统更友好。属性值本身是字符串长度有限制别往里面塞大块数据。这一点看似不起眼但在量产设备上踩过坑的人会懂——换了新flash结果一年半载就报“存储损坏”回头查日志发现是某个服务几小时就写一次属性谁顶得住。4. 属性系统常见问题与避坑实录4.1 属性设置后“立刻读不到”问题出在哪我见过最多的问题是一个进程刚setprop完立刻去getprop结果还是旧值甚至读不到。这个现象的根本原因是属性写入并非一个原子同步过程。setprop通过socket发给initinit再把值更新到共享内存。在写入方看来只要socket发送成功函数就返回了但读取方如果在这之前就读共享内存可能读到的还是旧值。另外setprop本身并不是同步阻塞的它只保证“请求已发出”不保证“已生效”。如果业务对时序敏感比如设置完马上做判断需要自己加一个读回确认的循环或者干脆加一点重试逻辑。实操经验设置完属性后先循环getprop读到新值再往下跑。如果三秒内还没读到多半是权限或SELinux的问题不要再干等。4.2 SELinux权限导致的“静默失败”还有一种情况更坑setprop执行了返回值是0成功但属性根本没变。原因基本只有一个SELinux拒绝但libc层没把错误透传回来。尤其在高版本Android上这种“假成功”的体验非常让人崩溃。排查手段我前面提过这里再强调一遍立刻看adb shell dmesg | grep avc。要么看logcat -b events | grep avc。确认denied日志里涉及的source context和target context。如果发现缺权限就去property_contexts里核对type然后给对应domain加allow规则。4.3 属性区不够用老设备的“写满”陷阱在Android 9之前的不少设备上属性区默认大小并不大。曾经有段时间社区里流行“往build.prop里堆自定义属性”结果把属性区塞满了然后系统各种奇怪问题有的属性写不进去、init动作不触发、甚至系统起不来。到Android 10以后Google已经把属性区大小做了扩展并且引入了“GKI”之后对属性区管理做了更多优化但老设备、定制ROM上依然可能遇到。遇到属性写不进去可以先看属性区剩余空间adb shell cat /dev/__properties__/property_info不过这个文件不是所有版本都有更通用的做法是看一次性写太多属性的时候有没有报PROP_ERROR_INVALID_VALUE或者日志里出现property area full字样。如果确属空间不足那就得减少自定义属性、统一命名清理或者在源码可控的情况下扩大属性区配置。4.4 “改了build.prop重启就没”你改对地方了吗很多初学的朋友会直接改/system/build.prop改完重启发现又变回去了。原因很简单/system分区在大多数量产设备上是只读的OTA升级会覆盖它/data分区才是真正的“持久化区域”。如果你想让一个属性在设备上“永久生效”正确的做法是如果只是临时验证adb shell setprop重启后会丢。如果是编译期固化改源码里的PRODUCT_PROPERTY_OVERRIDES重新编译system镜像。如果是升级后仍保留用persist.前缀并且确认SELinux权限允许运行时写入。这三点看着简单但能省下大量“为什么重启就没了”的返工时间。4.5 属性监听回调不触发一个隐藏的时序坑Framework层的addChangeCallback看似简单实际有一个很隐蔽的坑它注册的是native层的callback触发时机是“属性变化后的下一次looper轮询”而且它不区分属性名。如果你在同一时间有多个属性在变你的回调会被触发多次但回调里并不告诉你哪个属性变了。所以业界通常的做法是SystemProperties.addChangeCallback(() - { String v SystemProperties.get(persist.custom.feature); if (true.equals(v)) { // do something } });回调里自己重新读取关心的属性对比旧值再决定要不要做事。千万别在回调里直接执行重量级操作属性变化可能非常频繁回调是native线程上下文你一不小心就在非主线程操作UI当场闪退。5. 属性系统未来的演进与扩展思考5.1 新版Android里属性系统的变化如果你只接触过老版本Android看到新系统里的属性系统可能会有一点陌生因为Google一直在对它做改造。印象比较深的变化有这么几个属性区扩容Android 10以后属性区内存布局做了调整支持更大的属性空间。属性信息描述新增了property_info相关的描述文件让属性与SELinux的对应关系更清晰。系统性能优化属性读取走共享内存写入走socket的架构一直没变但在init进程内部针对频繁属性变化的情况做了批量处理和触发动作合并。GKI化Google正在收紧内核与vendor接口属性系统作为系统与vendor之间传递配置的关键通道未来在权限和namespace隔离上会有更严格的控制。从我自己的开发体感来说属性系统整体是越来越“重管理、轻使用”对开发者更透明同时安全边界划得更清。这也意味着早年那种“随心所欲改属性就能让系统干活”的时代正在逐渐远去。5.2 属性系统还能怎么玩偏门但有用的场景在聊完原理和实践后我也分享几个属性系统比较冷门但实际很实用的场景算是我这几年攒下来的私藏经验。第一个是隐藏开关。很多时候你在系统里加一个调试总开关不想通过SettingsProvider暴露给用户就可以用一个persist.属性UI层只是间接读取它。这样既不影响正常的用户界面也方便工程人员在adb里快速切换。第二个是跨进程状态同步。比如你的App需要知道系统当前是否处于“省电模式”系统层可以定时/实时地更新一个属性App通过SystemProperties读取即可。当然更正规的路径是去查PowerManager的API但属性系统在“快速、约定俗成、轻量同步”的场景下有它独特的优势。第三个是兼容适配探测器。检测设备是否支持某项新特性可以直接定义ro.config.support_xxxtrue然后在App里读取。Google自己也用了大量ro.属性来做feature flag。不过我也要泼一点冷水不要把所有状态都放到属性系统里。它毕竟是“全局共享、人人可写”的权限边界一旦没控制好很容易成为安全事故的源头。用属性是为了各层级间快速交换轻量配置不是为了替代数据库、替代跨进程架构。5.3 写在最后的个人体会属性系统在我接触过的Android各个子系统里属于“麻雀虽小五脏俱全”的那种。它看着简单但背后牵扯到共享内存、进程通信、SELinux、init触发机制、持久化策略……把这些点串起来你基本上就把Android系统的启动骨架和跨层通信机制摸到了一大半。如果你正被某个属性问题卡住我的建议是不要急着改几百行代码先花半小时搞清楚当前属性是在哪一层定义、哪一层写入、哪一层读取、权限链路上有谁在把关。把这几件事理清楚了问题基本就已经解决一半了。最后再分享一个我自己的小习惯每次在新项目里需要加自定义属性时我会顺手把属性名、用途、权限类型、定义文件位置统一记到项目的维护文档里。等三个月后别人来问你“这个persist定制的开关是谁加的”时你会感激当初那个记得写文档的自己。