三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Android属性系统深度解析:从property_get/set到权限与调试实战

Android属性系统深度解析:从property_get/set到权限与调试实战

1. 项目概述:深入理解Android属性系统

在Android系统开发与底层调试中,我们经常需要与一个名为“属性”(Property)的系统打交道。无论是查看设备的序列号、获取系统版本,还是动态调整某个系统行为(比如开启调试日志),都离不开对属性的读写操作。property_getproperty_set这两个函数,就是开发者与这套属性系统交互最直接、最核心的C/C++接口。它们看似简单,背后却链接着Android的初始化进程init、共享内存区域以及一套完整的安全管控机制。很多新手开发者,甚至一些有经验的工程师,往往只停留在“会用”的层面,一旦遇到属性设置不生效、权限不足或者跨进程同步等问题,就容易陷入困惑。今天,我就结合自己多年在Android Framework层和系统定制中的实战经验,把这套机制的里里外外彻底拆解清楚,让你不仅知道怎么用,更明白为什么这么用,以及如何避开那些隐藏的“坑”。

2. 属性系统核心架构与设计思路

2.1 属性是什么?为什么需要它?

你可以把Android的属性系统想象成一个全局的、键值对形式的“公告栏”或“共享白板”。这块白板由系统核心进程init负责维护和管理。任何进程都可以来读取(property_get)白板上的信息,但只有被授权的进程才能修改(property_set)上面的内容。

它的设计初衷是为了解决系统运行时的信息共享与动态配置问题。比如:

  • 系统状态标识ro.build.version.sdk(只读的系统API级别)、sys.boot_completed(系统启动完成标志)。
  • 运行时控制开关persist.sys.usb.config(控制USB连接模式,如mtp,adb)、debug.trace.tag(控制日志追踪级别)。
  • 硬件与设备信息ro.serialnoro.product.model

与Linux环境变量不同,属性是Android特有的机制,它通过共享内存实现,访问速度极快,并且天生支持跨进程变更通知(通过property_changed信号)。与配置文件相比,属性更适合存储简单的、需要频繁访问或动态改变的全局状态。

2.2 属性系统的三层架构

理解属性系统,需要建立起一个清晰的三层模型:

  1. 用户空间API层:这就是我们最常接触的property_getproperty_set等函数,它们定义在libcutilslibbase库中。这层API对开发者友好,隐藏了下层的复杂细节。

  2. 属性服务层(Property Service):这是init进程中的一个核心模块。它是属性系统的“大脑”和“仲裁者”。所有对属性的修改请求(property_set)最终都会通过一个Unix Domain Socket发送到init进程的属性服务进行处理。属性服务负责执行权限检查、触发变更动作、以及通知监听者。

  3. 共享内存存储层:这是属性系统的“数据库”。init进程在启动早期会创建一块共享内存区域(/dev/__properties__),所有属性名和值都存储在这里。这块内存被映射到所有进程的地址空间(通常是只读映射),因此property_get操作几乎就是一次本地内存访问,开销极小。而init进程持有这块内存的可写映射。

当调用property_set(“ctl.start”, “service_name”)时,数据流是这样的:你的进程 -> 通过socket发送请求 ->init进程的属性服务接收 -> 校验权限 -> 在共享内存中更新值 -> 向所有监听ctl.*属性的进程发送通知 -> 执行与ctl.start关联的启动服务动作。

注意ro.(read-only)开头的属性是在系统构建时确定的,存储在只读分区(如/system/build.prop),init在启动时加载它们。这些属性在运行时无法property_set修改,尝试修改会失败。这是很多开发者容易混淆的点。

3. 核心API详解与实战要点

3.1 property_get:如何高效读取属性

property_get的函数原型很简单:

int property_get(const char* key, char* value, const char* default_value);
  • key:要读取的属性名。
  • value:用于存储结果的缓冲区。
  • default_value:如果属性不存在,则返回的默认值。
  • 返回值:实际拷贝到value缓冲区的字符串长度(不包括结尾的\0)。

实操心得1:缓冲区大小与PROP_VALUE_MAX属性值的最大长度由系统宏PROP_VALUE_MAX定义(在Android 8.0及以后通常是91,早期是92)。这意味着你提供的value缓冲区至少要有PROP_VALUE_MAX个字节。一个健壮的写法是:

char value[PROP_VALUE_MAX] = {'\0'}; int len = property_get("sys.some.property", value, "unknown"); if (len > 0) { // 成功获取到属性值 } else { // 属性不存在,此时value中已经是default_value “unknown” }

千万不要假设属性值很短而使用固定小数组,这会导致缓冲区溢出,引发难以追踪的内存错误。

实操心得2:理解返回值property_get的返回值是字符串长度,这非常有用。你可以利用它来判断属性是否存在以及值的有效性。例如,有些属性可能被设置为空字符串“”,此时返回值为0。如果你传入的default_valueNULL,当属性不存在时,value缓冲区会被置为空字符串。

3.2 property_set:设置属性的门道与禁忌

property_set的原型更简单:

int property_set(const char* key, const char* value);
  • key:要设置的属性名。
  • value:要设置的值。
  • 返回值:0表示成功,-1表示失败。

失败的原因通常有以下几种:

  1. 权限不足:这是最常见的原因。属性有严格的安全控制(SELinux策略和property_perms)。普通应用或非特权进程尝试设置persist.ctl.等属性会被拒绝。
  2. 属性名非法:属性名不能包含特殊字符,长度也有限制。
  3. 尝试设置只读(ro.)属性:运行时修改ro.属性是徒劳的。

实操心得3:设置persist属性实现重启保留persist.开头的属性是“持久化”属性。当它被设置后,init进程会将其值自动保存到/data/property/目录下的一个对应文件中。设备重启后,init会读取这些文件并重新设置这些属性。这是实现用户配置跨重启保存的轻量级方法。

// 设置一个持久化属性 if (property_set(“persist.debug.my_flag”, “1”) != 0) { // 处理错误,很可能是权限问题 }

设置成功后,你会在/data/property/persist.debug.my_flag文件中看到内容1

实操心得4:使用ctl属性控制服务ctl.开头的属性是特殊的控制属性,用于向init进程发送命令。最常用的两个是:

  • ctl.start <service_name>:启动一个定义在init.rc中的服务。
  • ctl.stop <service_name>:停止一个服务。 这为其他进程动态管理系统服务提供了接口。例如,在ADB Shell中输入的startstop命令,其底层就是通过设置ctl.start/stop属性实现的。
// 在代码中启动一个名为`my_daemon`的服务 property_set(“ctl.start”, “my_daemon”);

重要警告ctl.start/stop是异步操作。设置属性成功只代表命令已送达init,不意味着服务已经成功启动或停止。你需要通过其他方式(如检查服务socket、进程是否存在)来确认结果。

4. 权限、安全与SELinux策略

属性系统的安全是Android安全体系的重要组成部分。权限检查发生在init进程的属性服务端,主要通过以下两层:

  1. 传统Uid/Gid检查(property_perms):在system/core/init/property_service.cpp中,有一个property_perms数组。它定义了哪些属性前缀允许哪些Unix用户/组进行设置。例如,net.开头的属性可能允许netd用户设置。如果你的进程不具备相应的Uid/Gid,设置请求会被拒绝。

  2. SELinux策略:这是更现代、更强大的安全机制。每个property_set操作都会触发SELinux权限检查。策略规则通常形如:allow system_app sysfs_type:file write;(这只是一个类比,属性有专门的property_type)。 对于平台开发者,如果你新增了一个自定义属性vendor.debug.my_prop并希望某个守护进程能设置它,你需要在SELinux策略文件(.te)中添加类似规则:allow my_daemon vendor_debug_prop:property_service set;

排查技巧:当property_set返回-1时

  1. 查看内核日志:使用logcatdmesg。权限拒绝时,通常会留下SELinux avc denied日志,这是最直接的线索。
    avc: denied { set } for property=vendor.debug.my_prop pid=1234 uid=1000 ...
    这条日志明确告诉你,进程(pid=1234)尝试设置vendor.debug.my_prop属性时被SELinux拒绝。
  2. 检查进程身份:确认你的进程是以什么用户/组运行的。在代码中可以用getuid()getgid(),在Shell中可以用ps -Z查看SELinux上下文。
  3. 审查现有策略:在设备上,可以尝试getprop -Z查看属性的安全上下文,或者去源码中搜索相关属性的权限定义。

5. 属性变更监听与异步通知

属性系统的强大之处在于它的通知机制。进程可以注册对某个属性前缀的监听,当该属性被更改时,内核会向监听进程发送一个信号。这是通过int property_listener(const char* key, void (*callback)(const char* name, const char* value, void* cookie))这样的内部机制实现的。

一个更常见的用法是,在init.rc脚本中,可以使用on property:<key>=<value>触发器来定义一系列动作。当属性<key>的值变为<value>时,init会执行对应的命令。

# 在init.rc中 on property:sys.boot_completed=1 start my_post_boot_service

这意味着,当系统启动完成标志被设置为1时,会自动启动一个自定义的后启动服务。

在Native C/C++代码中,监听属性变化相对复杂,需要用到property_listener或通过__system_property_wait_any__system_property_find等底层API进行轮询。而在Java层,可以通过SystemProperties.addChangeCallback来注册回调(需要系统权限)。

实操心得5:避免在监听回调中执行耗时操作属性变更通知可能比较频繁。如果你的回调函数执行时间过长,可能会阻塞其他监听者,甚至影响属性服务本身的响应。回调函数的设计应遵循“快进快出”原则,复杂的逻辑应该抛到另一个线程中去处理。

6. 常见问题排查与调试技巧实录

在实际开发和调试中,与属性相关的问题五花八门。下面我整理了一个典型问题排查表,并附上我的解决思路。

问题现象可能原因排查步骤与解决方案
property_set返回 -1,属性未改变1. SELinux权限拒绝。
2. 进程Uid/Gid无权限。
3. 属性名以ro.开头。
4. 属性服务未就绪(极早期启动阶段)。
1.首要步骤:检查logcatdmesg寻找avc: denied日志。
2. 检查property_perms数组(需查源码),确认你的进程Uid/Gid是否在允许列表中。
3. 确认属性名是否正确,ro.属性不可写。
4. 在系统启动后期或确保init进程运行后再尝试设置。
property_get返回空或默认值1. 属性确实不存在。
2. 缓冲区大小不足导致截断(但返回值会体现)。
3. 属性名拼写错误或大小写问题。
1. 使用getprop命令在ADB Shell中手动验证属性是否存在及值是什么。
2. 确保缓冲区大小为PROP_VALUE_MAX
3. 属性名是大小写敏感的,仔细核对。
设置了persist.属性,但重启后丢失1./data分区未成功挂载或加密导致init无法写入文件。
2. 保存属性的文件权限错误或被意外删除。
3. 在init读取持久化属性之前,你的进程就尝试读取了。
1. 检查/data/property/目录下是否存在对应的文件,内容是否正确。
2. 查看init启动日志,确认是否有读写/data/property的错误。
3. 如果你的服务在early-initinit阶段就需要读取持久化属性,考虑将属性定义在/vendor/default.prop或类似位置。
ctl.start执行后服务没起来1. 服务名错误,在init.rc中未定义。
2. 服务启动条件不满足(如依赖的设备未就绪)。
3. 服务本身启动崩溃。
1. 检查init.rc及相关rc文件,确认服务定义是否正确。
2. 查看logcat中是否有该服务的启动日志或错误信息。
3. 手动在ADB Shell中执行start <service_name>,观察输出和日志。
属性更改后,依赖的模块行为未变1. 监听该属性的模块没有正确注册监听或处理逻辑有bug。
2. 模块缓存了旧的属性值,未及时更新。
1. 确认属性值确实已改变(用getprop)。
2. 检查目标模块的代码,看其是否通过正确的方式监听属性变更(如on property:触发器或注册回调)。
3. 尝试重启依赖的模块或进程。

高级调试技巧:使用/dev/__properties__对于想深入探究的开发者,可以尝试(在eng或userdebug版本上)直接检查共享内存的内容。属性存储区域位于/dev/__properties__。虽然它是二进制格式,但通过一些工具或自定义代码可以窥探其结构。不过,这通常只在分析极其棘手的问题时才需要,日常开发中getprop/setprop和日志分析已经足够。

7. 在应用层与Framework层使用属性

虽然property_get/set是Native API,但在Android应用和Framework开发中,我们也有对应的接口。

在Java Framework层: 使用android.os.SystemProperties类。这个类通过JNI封装了Native的property函数。

import android.os.SystemProperties; // 读取属性 String sdkVersion = SystemProperties.get(“ro.build.version.sdk”, “unknown”); // 设置属性 (需要系统权限,如`android:sharedUserId=“android.uid.system”`) SystemProperties.set(“debug.my_app.flag”, “1”);

注意:SystemProperties.set要求调用进程具有很高的系统权限,普通应用无法调用。

在App层(无权限): 普通应用无法直接设置系统属性,但可以读取一些公开的、非敏感属性。然而,Google并不鼓励这样做,因为属性API对App并不可见。更规范的做法是通过系统提供的公开API来获取信息,例如用Build类获取设备信息。

在Native Service或HAL层: 这是property_get/set最常使用的场景。例如,一个音频HAL实现可能需要读取persist.vendor.audio.some_feature来决定是否启用某个功能。你需要在对应的Android.bpAndroid.mk中添加对libcutilslibbase的链接。

8. 自定义属性与系统集成规范

当你为设备定制功能,需要新增自定义属性时,请遵循以下规范,这能避免未来维护的混乱和潜在的冲突:

  1. 命名空间前缀:使用清晰的前缀来划分归属。

    • ro.vendor.vendor.:用于芯片厂商或ODM的只读/可读写属性。
    • ro.product.ro.hardware.:用于设备相关的只读属性。
    • persist.vendor.:用于需要持久化的厂商自定义属性。
    • debug.test.:用于调试和测试,注意这些属性在user版本可能被禁用。绝对不要使用sys.ctl.hw.等系统保留前缀。
  2. 在SEPolicy中添加规则:如果你新增的属性需要被某个守护进程或服务设置,必须在相应的SELinux策略文件(.te)中授予set权限。同时,也要考虑哪些进程需要read权限。

  3. 在init.rc中定义默认值或触发器:可以在/vendor/etc/init/hw/下的rc文件中,使用setprop命令在启动早期设置默认值,或者使用on property:触发器来响应属性变化。

  4. 文档化:在项目的内部文档中记录新增属性的名称、用途、可接受的值以及读写权限。这对于团队协作和后续调试至关重要。

我个人在多个大型设备定制项目中,见过因为属性命名混乱(比如不同团队都用了persist.debug.flag)导致功能互相覆盖的案例,也花过大量时间排查因SELinux策略缺失导致的属性设置失败。严格遵守上述规范,是从一开始就避免这些麻烦的最佳实践。属性系统是Android的毛细血管,虽然细小,但贯通全身,理解它、用好它,是深入Android系统开发的必经之路。

← 返回列表