尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Android属性服务PropertyService源码解析:从setprop到Binder全链路

发布时间:2026/9/8 3:01:27

资讯中心
01
ARTICLE

Android属性服务PropertyService源码解析:从setprop到Binder全链路

Android属性服务PropertyService源码解析:从setprop到Binder全链路
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 最大 92value 最大 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.stopvalue 指定要启动/停止的服务名例如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_completed1 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_completed1之后再开始初始化。在实际的源码阅读中你会发现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查看属性区域头部信息部分设备需要 rootstrace -f -e tracesetsockopt,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 系统的一块拼图。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。