4. 属性分类与命名规范

Android 属性系统并不是一堆“随便起名字的键值对”。它之所以能在 init、property_service、SELinux、OTA、厂商定制之间稳定协作,核心就在于一套约定俗成的分类与命名空间规则。理解这些规则,才能写出可维护、可审计、可升级的属性配置,而不是把属性表变成一团无法追踪的字符串。

一、核心原理:属性分类的本质是“权限 + 生命周期 + 归属”

Android 属性的分类并不是语法层面的强制类型,而是通过命名前缀、访问权限、SELinux 标签和 init 触发规则共同约束出来的工程约定。一个属性属于哪一类,通常由三个维度决定:

  • 权限维度:谁可以读、谁可以写。只读属性通常由 init 在启动阶段设置一次,之后禁止写入;控制属性则允许特定进程或系统服务动态修改。
  • 生命周期维度:属性值是否跨重启保留。持久化属性会写入 /data/property 下的持久化文件,重启后恢复;普通系统属性仅存在于内存中,重启即丢失。
  • 归属维度:属性由 AOSP 定义、厂商扩展,还是设备专属。命名空间前缀是判断归属的第一线索。
一句话总结:属性分类不是“类型系统”,而是“命名空间 + 权限 + 持久化策略 + SELinux 标签”的组合契约。

二、六类属性的职责与命名约定

在实际工程中,我们通常把 Android 属性划分为以下六类。每一类都有典型的命名前缀和典型使用场景:

分类 典型前缀/示例 可写性 持久化 典型用途
系统属性 ro.*、sys.*、service.* ro 只读,sys 可写 否 系统版本、启动状态、服务状态
持久化属性 persist.* 可写 是 用户设置、调试开关、校准参数
只读属性 ro.* 仅 init 首次写入 否 硬件信息、编译版本、设备标识
控制属性 ctl.*、sys.* 特定进程可写 否 启动/停止服务、触发状态切换
厂商属性 vendor.*、ro.vendor.* 按需 按需 厂商定制、硬件抽象层配置
命名空间属性 ro.boot.*、ro.product.*、init.svc.* 多为只读 否 内核启动参数、产品信息、服务状态

需要特别注意的是,ro.* 并不等于“永远不能写”,而是“只能写一次”。init 在启动早期允许写入一次,之后 property_service 会拒绝再次写入。这个“一次写入”窗口是很多启动脚本能设置 ro.boot.* 的原因。

三、命名空间约定:前缀即契约

Android 对属性命名空间有明确的约定,前缀不仅用于分类,还直接影响 SELinux 策略和属性访问控制。常见命名空间如下:

  • ro.boot.*:来自内核命令行或 bootloader 的启动参数,如 ro.boot.serialno、ro.boot.verifiedbootstate。
  • ro.product.*:产品信息,如 ro.product.model、ro.product.name。
  • ro.build.*:编译信息,如 ro.build.version.sdk、ro.build.fingerprint。
  • init.svc.*:init 管理的服务状态,如 init.svc.zygote 的值为 running 或 stopped。
  • persist.*:持久化属性,写入 /data/property,重启后保留。
  • vendor.*:厂商扩展属性,通常与 vendor 分区和 vendor SELinux 域关联。
  • ctl.*:控制属性,用于触发 init 执行服务控制命令,如 setprop ctl.start bootanim。
命名空间不是“建议”,而是 SELinux 和 property_service 的硬约束。例如,普通应用进程通常只能读取 ro.* 和部分 sys.*,写入 persist.* 或 ctl.* 会被拒绝。

四、案例与场景:从启动到运行时的属性流转

下面这张图展示了属性从内核启动参数到系统运行时的典型流转路径,以及不同分类属性在其中的位置:

Android 属性分类与命名空间流转 Bootloader / Kernel ro.boot.* 启动参数 init 第一阶段 ro.* 只读属性写入 property_service persist.* 持久化加载 运行时服务 ctl.* / sys.* 控制 命名空间约定(前缀即契约) ro.boot.* 内核启动参数 ro.product.* 产品信息 persist.* 持久化存储 ctl.* 服务控制 vendor.* 厂商扩展 访问控制与生命周期 只读属性:一次写入 重启后重新初始化 持久化属性:跨重启 写入 /data/property 控制属性:动态触发 setprop ctl.start/stop SELinux 标签 按前缀分配域

以一次典型开机为例:bootloader 把 androidboot.serialno 传给内核,init 解析后生成 ro.boot.serialno;随后 init 读取 /data/property 中的持久化文件,恢复 persist.* 属性;系统服务启动后,通过 setprop ctl.start 触发服务控制,并通过 sys.* 属性反映运行状态。整个过程体现了“分类决定权限,命名空间决定归属”的设计。

五、误区与复盘:命名不规范带来的真实问题

常见误区一:把该持久化的属性写成普通属性。例如把用户校准参数写成 sys.calibration,重启后丢失,导致设备行为不一致。正确做法是使用 persist.vendor.calibration 或类似前缀。
常见误区二:滥用 ro.* 前缀。有些开发者把需要动态更新的属性命名为 ro.xxx,结果运行时无法写入,只能重启设备。只读属性应只用于启动阶段确定、运行期不变的信息。
常见误区三:忽略 SELinux 标签。即使属性名符合命名空间,如果 property_contexts 中没有对应标签,进程仍可能被拒绝访问。命名规范必须与 SELinux 策略同步维护。

复盘时可以用一个简单清单检查属性定义是否合理:

  1. 属性是否需要跨重启保留?是则使用 persist.*。
  2. 属性是否只在启动阶段写入一次?是则使用 ro.*。
  3. 属性是否用于控制服务启停?是则使用 ctl.*。
  4. 属性是否属于厂商定制?是则使用 vendor.* 或 ro.vendor.*。
  5. 属性是否来自内核或 bootloader?是则使用 ro.boot.*。
  6. 属性是否已在 property_contexts 中分配 SELinux 标签?
# 典型属性设置与读取
setprop persist.vendor.debug.level 3
getprop persist.vendor.debug.level

setprop ctl.start bootanim
getprop init.svc.bootanim

getprop ro.boot.serialno
getprop ro.product.model

掌握属性分类与命名规范,本质上是在掌握 Android 系统配置工程的“语法”。只有命名空间清晰、分类准确、权限与持久化策略匹配,属性系统才能在多厂商、多版本、多安全域的环境中保持可维护和可审计。