1. PropertyService 是什么,为什么值得读源码
先交代一个背景:PropertyService(属性服务)是 Android 系统里最“不起眼”却最核心的系统服务之一,运行在 system_server 进程中,通过 Binder 对外提供系统属性的读写能力。你在终端里敲的getprop、setprop,底层全部走到它这里。
接触 Android 越久,越会发现很多“莫名其妙的问题”最终都跟属性服务有关:setprop设置了不生效、某些属性只有 root 权限才能改、某个属性改了之后没触发预期动作、应用层读到的属性值和 shell 里不一致……这些现象背后,全是 PropertyService 的代码逻辑在起作用。所以把这份源码吃透,对搞系统开发、做 ROM 定制、排查运行时问题都有实打实的帮助。
这篇文章适合三类人:一是刚接触 Android 系统服务、想找一份“不太复杂又能打通 Binder 全链路”的源码来学习的人;二是做系统定制、需要深入理解属性权限和 init 联动机制的人;三是在线上遇到过属性类疑难问题、想搞明白底层原理的开发和运维。我会从整体架构、Binder 链路、核心服务端逻辑、通知机制、问题排查这五个维度来拆,尽量把每个关键节点的“为什么”也讲清楚。
1.1 从一个最直观的入口看起
我建议所有第一次读这份源码的人,先从命令行入手:
adb shell getprop ro.build.version.sdk adb shell setprop debug.myapp.test 1这两条命令背后分别是属性服务的读和写。顺着getprop的结果反查代码,你会发现它并不是直接“读一个全局变量”,而是经历了:客户端 API → Binder 驱动 → system_server 中 PropertyService → 属性存储区域 → 返回结果 这样一条链。整条链路虽然长,但每一段代码都不算复杂,非常适合作为学习 Binder 系统服务的入门范本。
2. 整体架构与初始化流程:PropertyService 是如何被“点亮”的
读源码最怕一上来就陷进细节。我的习惯是先建立一张“地图”,知道这个服务从哪里注册、被谁调用、数据存在哪,再深入具体函数。
2.1 服务注册入口
PropertyService 并不是由 system_server 直接 new 出来的,而是通过 ServiceManager 注册,服务名是"property"。以 Android 9/10 时期的代码为例,在 system_server 启动流程中,有一个类似这样的调用:
// frameworks/base/services/java/com/android/server/SystemServer.java try { Slog.i(TAG, "StartPersistenceWipe"); ... PropertyServiceHelper.init(); // 或者更早的版本是 ServiceManager.addService("property", new PropertyService()); } catch (Throwable e) { Slog.e(TAG, "Failure starting PropertyService", e); }不同 Android 版本的入口位置有差异,但本质是一样的:在系统服务启动阶段,构造 PropertyService 对象,并把它注册到 ServiceManager。注册完成之后,客户端就能通过ServiceManager.getService("property")拿到它的 Binder 代理。
这里有一个非常值得新手注意的点:属性服务的“数据存储区”和“服务对象”是分离的。PropertyService 本身只是一个 Binder 壳,真正的属性数据放在一块共享内存里,这块共享内存在 init 进程早期就被映射好了。也就是说,即使你不调用任何 Binder,在 native 层直接读属性区域,也能拿到大部分属性值——这也是为什么getprop命令在系统服务还没完全启动时就能用的原因。
2.2 初始化阶段的属性区域映射
属性区域的建立发生在 init 进程早期。init 会调用__system_property_area_init()创建一块共享内存(通常挂在/dev/__properties__目录下),然后通过__system_property_area_serialize()之类的机制把属性信息写入。
这块共享内存的基本布局是:
- 头部(property_area_header):包含 magic、version、serial 等元数据
- 若干个 property_info 条目:每个条目由 name、serial、value 三部分组成
- 空闲空间管理:以 linked list 或类似方式维护
客户端进程在使用property_get之前,会先通过__system_property_area_ensure_initialized()把这块共享内存映射到自己的地址空间。映射成功后,读属性就是直接读内存,不需要经过 Binder。这是一个很重要的性能设计:写属性走 Binder,读属性走共享内存。
2.3 为什么读走共享内存、写走 Binder
你可能想问:为什么不干脆都走 Binder?原因很简单:
- 读操作频率极高。系统里随手一个模块初始化就要读几十个属性,如果都走 Binder,跨进程开销会非常大。
- 写操作频率低。属性值通常只在开机阶段或配置变更时被修改,完全可以用更重的 Binder 通路换取权限控制和通知机制。
- 属性值需要全局可见。共享内存天然满足“一个进程写、所有进程可见”的需求,且无需额外的同步协议。
所以 Android 的这种设计是“双通道”:查询走共享内存通道,修改走 Binder 通道。理解这一点,后面读代码时就不会对“为什么服务端逻辑这么少”感到困惑。
3. Binder 接口与客户端调用链:setprop 到底经历了什么
现在进入最关键的链路分析。我们用setprop命令入手,看它如何一步步到达 PropertyService 的服务端。
3.1 AIDL 接口定义
PropertyService 的接口其实非常少,核心就两个:get和set。在早期版本里它甚至没有独立的 .aidl 文件,而是直接用 Parcel 手动封装。后来逐步演进,Java 侧有了类似这样的接口:
// frameworks/base/core/java/android/os/IPropertyService.aidl package android.os; interface IPropertyService { String get(String key); boolean set(String key, String value); }就这么简单。一个读、一个写,只传递 key-value 字符串。但你别小看这两个接口,服务端在收到请求后会做大量校验和分类处理,这些才是源码分析的重头戏。
3.2 客户端 API 到 Binder 的桥接
在 Java 层,SystemProperties类是对外提供的工具类:
// frameworks/base/core/java/android/os/SystemProperties.java public static String get(String key) { ... return PropertyServiceHelper.get(key); }它在 native 层会调用property_get,而property_get的实现在 bionic 库中:
// bionic/libc/system_properties/system_properties.cpp int __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); } return 0; }注意这个实现:__system_property_find是直接在当前进程映射好的共享内存里找属性名。如果找到了,就直接读内存返回;只有在找不到对应属性的情况下,才会考虑走 Binder 去服务端查。
而写操作就不一样了:
int __system_property_set(const char* key, const char* value) { // 先做基本校验 if (key == nullptr || value == nullptr) return -1; // 调用 Binder 到远端 return PropertyServiceBinderSetProperty(key, value); }3.3 Binder 事务的编解码细节
native 侧的PropertyServiceBinderSetProperty会构造一个 Parcel,写入TRANSACTION_SET对应的 transaction code,然后调用transact()。这里有一个容易被忽略的点:Binder 层对属性 key 的长度是有限制的。早期限制是 32 字节,后来放宽到 92 字节(其中 name 最大 92,value 最大 92)。这个限制在property_info的结构体定义里写死了:
#define PROP_NAME_MAX 92 #define PROP_VALUE_MAX 92提示:如果你在做 ROM 定制时发现“属性值超过 91 个字符就自动截断”,不用怀疑代码逻辑,就是这里定义的常量约束的。需要改这个限制的话,要同时改 bionic 和 init 里的宏定义,并且共享内存头部结构也需要重新设计,改动量不小,一般不建议动。
3.4 服务端的 transaction 分发
PropertyService 在 system_server 中收到 Binder 事务后,会走onTransact()分发。核心处理函数有点像这样:
status_t PropertyService::onTransact(uint32_t code, const Parcel& data, Parcel* reply) { switch (code) { case TRANSACTION_GET: { std::string key = data.readString(); std::string value = GetProperty(key); reply->writeString(value); return NO_ERROR; } case TRANSACTION_SET: { std::string key = data.readString(); std::string value = data.readString(); bool result = SetProperty(key, value); reply->writeBool(result); return NO_ERROR; } } }到了这里,往下的逻辑就进入真正的服务端核心了。
4. 核心服务端逻辑:属性存储、权限控制与分类处理
前面提到,PropertyService 只是一个 Binder 壳,但壳里面的GetProperty和SetProperty才是真正的“大脑”。下面重点拆一下SetProperty的完整流程,因为它的逻辑最丰富。
4.1 SetProperty 的完整流程
服务端收到 set 请求后,大致会经过以下几步:
- 参数校验:检查 key 是否为空、是否包含非法字符(
=,$,\0等)。 - 权限校验:这里分为两层。第一层是 check_mac_perms,即检查调用方的 SELinux 权限;第二层是传统权限位校验,只对部分属性生效。
- 属性分类:根据 key 的前缀决定处理方式。常见分类有:
ro.开头的只读属性:一旦设置就不允许再修改persist.开头的持久化属性:不仅写入内存,还要落盘保存ctl.开头的控制属性:用于触发 init 执行 start/stop 动作net.开头的网络属性:有些版本会做特殊处理- 普通属性:直接写入共享内存
伪代码逻辑大致是:
static int property_permission(struct property* prop, const char* name, const char* value) { // 检查 SELinux 权限 if (check_mac_perms(name, source_context, target_context) == 0) { return -1; } // 检查 ro. 属性不可二次写入 if (!strncmp(name, "ro.", 3)) { if (prop != nullptr) return -1; } return 0; }4.2 权限控制的细节与设计考量
SELinux 权限校验在 Android 的属性系统里扮演了关键角色。它的工作方式是基于“属性上下文映射”:init 进程在启动时会加载一份property_contexts文件,把属性名前缀映射到对应的 SELinux context。比如:
net. u:object_r:net_prop:s0 persist. u:object_r:persist_prop:s0 debug. u:object_r:debug_prop:s0当进程调用setprop时,PropertyService 会根据属性的 context 与调用进程的 context,通过selinux_check_access进行判定。如果调用方没有对该 context 的写权限,就直接拒绝。
这就解释了一个很常见的现象:普通应用设置persist.属性通常会失败,因为应用进程的 SELinux domain 是untrusted_app,在property_contexts的规则里没有写persist_prop的权限。即使应用有系统签名也没用,因为属性权限走的是 SELinux 规则,和签名权限是两套体系。
我第一次调这个的时候踩过一个坑:在应用里调用SystemProperties.set("persist.my.test", "1"),明明代码编译通过、签名也没问题,就是设置不生效,而且系统日志里看不到明显的错误。后来通过adb shell dmesg | grep avc才看到 SELinux denied 的日志。所以如果你遇到“属性设置无效”的问题,第一步先查 SELinux,比看代码效率高得多。
4.3 持久化属性与落盘机制
persist.属性的处理比较特殊。它不仅要写入共享内存,还要通过 init 持久化到磁盘。这里就涉及 init 进程的参与,链路是:
PropertyService -> init 通过 property_set 回调 -> 将 "persist." 前缀的属性写入 /data/property/persistent_properties在 Android 较新的版本中,持久化属性存储在/data/property/目录下,是一个二进制文件。init 每隔一段时间或者在属性每次变化时,会把所有persist.属性重新序列化后写入磁盘。这样重启后,init 再从这个文件恢复属性值。
这个持久化机制有一个实际应用场景:如果你想让某个配置在设备重启后依然生效,必须使用persist.前缀。例如:
setprop persist.sys.language zh-CN重启之后这个值会被恢复,很多系统设置就是依赖这一机制实现的。
但这里要特别注意:persist 属性的写入频率不能太高。因为每次写入都可能触发磁盘落盘,频繁的setprop persist.xxx在低端设备上会导致 I/O 压力,严重时甚至造成 io wait 升高。我在实际项目里遇到过某厂商预装应用每隔几百毫秒就写一次 persist 属性,导致系统卡顿的情况。定位方法很简单:用adb shell cat /proc/mounts | grep property查看属性文件挂载情况,再用iostat观察对应分区的写入负载,很快就能确认。
4.4 ctl. 控制属性:与 init 的联动
ctl.属性是另一个特殊分类。它的 key 格式一般是ctl.start和ctl.stop,value 指定要启动/停止的服务名,例如:
setprop ctl.start zygotePropertyService 在处理这类属性时不会写入共享内存,而是直接转发给 init 进程,由 init 执行对应的服务动作。这个机制是 Android 系统动态启停系统服务的核心通道。
源码里对应的处理逻辑类似于:
if (strncmp(name, "ctl.", 4) == 0) { // 通知 init 执行 start/stop/restart property_changed(name, value); return 0; }property_changed最终会通过 socket 或 binder 调用到 init 进程,触发它从属性变更队列中取出这些控制指令并执行。理解了ctl.属性的含义,再回头看adb shell stop/adb shell start命令,就能明白它们本质上就是调用了setprop ctl.stop/setprop ctl.start,只不过把动作和服务名都封装好了。
4.5 ro. 属性不可变性带来的“坑”
ro.前缀的属性只能设置一次,而且通常是在启动阶段通过system.prop文件里的规则写入。一旦写入,运行期任何进程尝试修改都会失败,哪怕是有 root 权限也不行。
这里有一个容易踩的坑:在自定义 ROM 开发时,很多人想在 init.rc 里动态修改ro.build.display.id,却发现根本不生效。原因就在于 ro. 属性在 init 加载完system.prop之后就已经被锁定,后续的 setprop 都会返回失败。如果你想在编译阶段注入版本信息,应该修改的是编译期间生成system.prop的脚本,而不是在运行时试图去 setprop。这个坑我见过多次,每次帮别人排查时都能省下不少时间。
5. 属性变化的通知机制:property_trigger 和 init 的联动
属性服务从来不是“写完就结束”的。它有一个非常重要的能力:当某个属性发生变化时,系统里其他组件能够感知到并做出响应。这就要聊到属性变化通知机制。
5.1 触发器的原理与实现
在 init 中,触发器(trigger)的典型使用方式是在 init.rc 里:
on property:sys.boot_completed=1 start some_service当sys.boot_completed被设置为 1 时,init 会执行对应 action 中的命令。这个机制的背后,是 init 进程维护了一个“等待属性变化”的列表,每个表项包含属性名和期望值。每次属性区域更新后,init 都会去扫描这个列表,看哪些条件已经满足,然后执行对应动作。
PropertyService 与 init 之间的通知通道,本质上是 init 在自己启动时注册了一个回调,当属性变化事件发生时,init 进程内部会对变化做匹配。在现代 Android 版本中,这个过程已经演进为 init 内部的PropertyMonitor组件,逻辑更体系化,但核心思想不变:属性写入后通知订阅者,订阅者根据属性名和值决定是否触发动作。
5.2 属性 wait 机制的作用
另一个相关机制是 native 层的属性等待,常用代码路径:
int property_wait(const char* name, const char* value, int timeout);这个 API 会阻塞调用方,直到指定属性的值变为期望值,或者超时。它经常被用在“条件同步”场景中:比如某个服务需要等待系统属性sys.boot_completed=1之后再开始初始化。
在实际的源码阅读中,你会发现property_wait的实现并不是简单地死循环轮询属性值,而是通过 poll 共享内存区域中的序列号(serial)来实现高效等待。每次属性区域有更新,序列号会递增,等待者阻塞在 poll 上,被唤醒后再重新检查目标属性的值。这样避免 CPU 空转,效率比直接 sleep 轮询高得多。
5.3 属性触发器与开机时序
开机过程中,属性触发器的时序逻辑非常值得关注。整个 Android 启动链路上,有几个关键的里程碑属性:
sys.powerctl:关机/重启指令通道sys.boot_completed:标志开机完成dev.bootcomplete:早期 boot 完成标志vold.decrypt:加密状态指示
以sys.boot_completed为例,它的置位时机是在 SystemServer 相关服务启动完成之后。一旦置位,大量依赖它的组件才真正开始工作。如果某个 ROM 或者其他模块在开机过程中卡住,很多时候就是在等待某个里程碑属性未被正确设置。排查这类问题时,可以先看属性区域的序列号是否持续增长,再看具体属性值是否符合预期。
6. 常见问题与排查技巧实录
聊了这么多源码细节,最后分享一些我在实际定位问题过程中总结的经验。这些问题在社区和内部项目里反复出现,排查思路和顺序基本固定。
6.1 setprop 不生效的排查路径
当遇到“属性设置无效”的情况,我建议按以下顺序排查:
- 确认 key 前缀是否符合预期:如果
ro.属性已经设置过,后面再 set 一律失败,这是设计行为,不是 bug。 - 确认 SELinux 权限:
adb shell dmesg | grep avc | grep property,看有没有 denied 日志。如果有,说明调用方的 SELinux context 缺少写目标属性 context 的权限。不要试图暴力关闭 SELinux,正确做法是补充property_contexts和 sepolicy 规则。 - 确认是否走到服务端:有些版本在读路径上做了缓存,setprop 没有报错但值是旧的,这通常是因为共享内存映射没有刷新,重启进程即可。注意,不要把这个和我们前面说的“读共享内存”混淆,这里特指某些进程中缓存了 prop_info 指针,属性区域 realloc 后指针失效。
6.2 getprop 读到的值不一致
这个问题在不同进程中表现不同,典型的场景是:shell 里getprop debug.my.test返回 1,应用里调用读取却返回空。原因一般是:
- 应用进程的 SELinux 域对读取某些属性有限制,
getprop时返回默认值而非真实值。 - 应用进程启动时属性区域尚未映射完整(极端情况,发生在早期 phase),导致初始化失败。
这种情况我会先对比同一进程内 native 和 Java 的读取结果。如果 native 层正常但 Java 层异常,检查SystemProperties的 JNI 绑定和ProcessState初始化时机。如果 native 层本身就失败,基本可以确定是 SELinux 过滤规则的问题。
6.3 persist 属性重启后丢失
如果你发现某个persist.属性重启后没有恢复,优先检查下面几点:
- 确认属性值是真正写入了共享内存,且 init 的持久化落盘流程已执行。
- 确认持久化属性文件所在的
/data分区没有异常,比如剩余空间不足、I/O 错误。 - 确认该属性的 context 被正确映射,没有被 SELinux 拦截。
在模拟器或 CTS 测试里,我遇到过因为磁盘空间写满导致持久化属性丢失的情况。当时用df -h /data一眼就看出问题,但很多人不会把“属性丢失”和“磁盘满”关联起来,这里提个醒。
6.4 实用调试命令与工具
最后整理一份我在分析属性问题时会用到的命令清单:
| 命令/工具 | 用途 |
|---|---|
adb shell getprop | 列出所有属性及其值 |
adb shell setprop key value | 设置单个属性 |
adb shell dumpsys property | 查看 PropertyService 服务端信息、调用次数等 |
adb shell service list | grep property | 确认服务是否正常注册 |
adb shell dmesg | grep avc | 查看 SELinux 拒绝日志 |
adb shell cat /dev/__properties__/property_info | 查看属性区域头部信息(部分设备需要 root) |
strace -f -e trace=setsockopt,ioctl setprop test 1 | 跟踪 setprop 的系统调用链路 |
在代码调试层面,我常用的方法是直接在property_service.cpp的SetProperty入口加日志,编译 userdebug 版后刷机验证,比单纯读代码更直观。不过现在很多设备不让刷 userdebug,那就只能靠 dumpsys 和日志输出做近距离观察了。
7. 源码阅读中可以扩展的方向
如果你已经把 PropertyService 的主链路读通了,我建议继续向这几个方向延伸,因为它们和属性服务强相关,而且能帮你建立更完整的系统观。
第一个方向是 init 进程完整的启动流程。PropertyService 的属性区域是在 init 早期建立的,但 init 本身做的事情远不止于此:解析 init.rc、加载 sepolicy、启动 ueventd、管理服务进程。理解了 init 的主循环,你会发现属性服务只是其中一个模块,但它与 init 其他模块的交互方式很有代表性。
第二个方向是 Binder 本身的工作原理。从ServiceManager.getService("property")出发,你可以一路追踪到 binder 驱动的 binder_thread_read、binder_transaction 等核心函数。属性服务的代码能让你从业务层面理解 Binder 的同步调用、Parcel 编解码、服务注册查找这些概念,比直接啃 binder 内核文档要容易上手。
第三个方向是 SELinux access vector 机制。属性服务给了你一个非常鲜活的案例:同一个property_set系统调用,为什么对不同的属性返回不同结果?答案在 security class 和 policy 规则。你可以围绕property_service这个 class 仔细看一遍 sepolicy 目录下的property_contexts、property.te文件,再配合adb shell sesearch命令验证,一台设备就是一个完整的 SELinux 实验室。
以上就是我阅读 PropertyService 源码的主要思路。这份源码不算复杂,但因为它横跨了 Binder、SELinux、共享内存、init 进程、持久化存储等多个系统模块,读一遍下来收获会非常大。我的个人经验是:从setprop命令出发,按“客户端 → Binder → 服务端 → 存储 → 通知 → 持久化”这条链走一遍,比按文件顺序从头读到尾效率高得多。你在过程中遇到的每一个“为什么”,都会成为你理解整个 Android 系统的一块拼图。