1. 项目概述:为什么需要添加系统属性?
在Android开发,特别是系统定制和底层调试中,我们经常会遇到一个需求:需要让一个配置项或状态信息,能够被系统内几乎所有的组件(从Java应用、Native服务到Shell脚本)方便地读取和监控。比如,你想标记设备当前处于“工程测试模式”,或者动态控制某个底层驱动的行为开关,再或者传递一个跨进程的版本标识。这时候,系统属性(System Property)就成为了一个近乎完美的解决方案。
简单来说,系统属性是Android(以及其底层的Linux)提供的一个全局键值对存储机制。它像一块系统级的“公告板”,任何进程都可以在上面“张贴”信息(写属性)或“查看”信息(读属性)。最著名的例子就是ro.build.version.sdk这个只读属性,它定义了设备的API级别,无数应用都依赖它来做版本兼容判断。当你需要创造一个类似的、属于自己的全局状态标识时,添加自定义系统属性就成了必备技能。
这个操作看似只是调用setprop和getprop命令,但其背后涉及到Android属性系统的架构、权限控制、属性域划分以及如何让属性在系统启动的早期就生效。如果只是临时设置,重启就丢,那意义不大。我们真正要做的,是添加一个持久化、有明确权限、在合适时机被初始化的系统属性。这需要深入到AOSP(Android Open Source Project)源码层面进行修改。接下来,我将以一个实际的场景为例,带你完整走一遍从原理到实现的流程:为设备添加一个名为persist.sys.custom_feature_enabled的属性,用于控制一个自定义功能的开关。
2. 系统属性机制深度解析
在动手修改代码之前,必须理解系统属性是如何工作的。这能帮你避开很多坑,比如属性不生效、权限拒绝或者重启丢失。
2.1 属性服务的架构与流程
Android的属性系统主要由一个名为init的守护进程(也是Android的第一个用户空间进程)来管理。它运行着一个property_service。你可以把它想象成一个中央注册中心。
- 属性存储:所有属性都存储在一块共享内存区域中。这块内存由
init进程创建并维护,其他进程通过Unix域套接字(/dev/socket/property_service)向init发起请求来读写。 - 读写分离:
- 读操作 (
getprop):进程可以直接从共享内存中读取属性值,效率很高。 - 写操作 (
setprop):进程必须通过上述套接字向property_service发送请求。init会校验权限和属性名规则,通过后才更新共享内存。
- 读操作 (
- 权限控制:这是关键。在
property_contexts文件中,定义了哪个安全上下文(Security Context,通常与SELinux相关)可以读写哪个属性。例如,只有system_server或root才有权修改某些系统关键属性。
2.2 属性的类型与命名规范
属性不是随便命名的,其前缀决定了它的行为和生命周期:
ro.:只读属性。通常在系统构建时确定(如ro.build.*),或在init进程的启动阶段早期设置,之后任何进程都无法修改。用于描述系统静态信息。persist.:持久化属性。这是我们需要重点关注的类型。当这类属性被设置后,其值会被自动保存到/data/property/目录下的对应文件中。系统下次启动时,init会读取这些文件并重新设置这些属性,从而实现持久化。ctl.:控制属性。用于控制服务(start/stop)。例如setprop ctl.start bootanim可以启动开机动画服务。sys.、hw.、dev.等:这些是普通读写属性,但没有自动持久化功能。重启后值会丢失。通常用于运行时状态记录。
在我们的例子中,选择persist.sys.custom_feature_enabled是合理的:persist.保证开关状态重启后不丢失;sys.表明这是一个系统级别的功能开关。
2.3 属性变更监听与触发
属性系统还有一个强大功能:属性变更监听。进程可以调用property_listener相关的API,注册对某个属性变化的回调。当属性值改变时,监听者会收到通知。这使得基于属性的动态配置成为可能。例如,系统可以根据persist.sys.debug.trace属性的变化,动态开启或关闭调试日志。
注意:修改系统属性源码属于AOSP层开发,需要完整的Android源码编译环境。本文假设你已有源码目录(如
~/aosp)并熟悉source build/envsetup.sh和lunch等基本操作。
3. 实现步骤:添加持久化系统属性
我们的目标是添加persist.sys.custom_feature_enabled,并为其设置默认值0(关闭)。这需要修改两个核心文件。
3.1 第一步:在build.prop中定义默认值
系统属性最初的默认值是在构建过程中生成的。我们需要在设备相关的配置文件中定义它。
- 定位设备Makefile:找到你的设备对应的产品定义文件。通常路径在
device/<制造商>/<设备名>/或vendor/<制造商>/<设备名>/下。例如,device/google/coral/aosp_coral.mk。 - 添加属性定义:在该
.mk文件中,你会看到类似PRODUCT_SYSTEM_DEFAULT_PROPERTIES的变量。这是添加系统默认属性的地方。我们添加一行:PRODUCT_SYSTEM_DEFAULT_PROPERTIES += \ ... \ persist.sys.custom_feature_enabled=0- 为什么是
PRODUCT_SYSTEM_DEFAULT_PROPERTIES?这个变量中的属性会被编译到/system/build.prop文件中。init进程在启动早期会加载这个文件,从而初始化这些属性。对于persist.属性,如果/data分区没有存储过值,就会使用这里的默认值。
- 为什么是
实操心得:
- 如果你找不到确切的
.mk文件,可以在源码根目录执行find . -name “*.mk” | xargs grep -l “你的设备代号”来搜索。 - 确保语法正确,每行结尾的
\表示续行,最后一行不要加。添加后最好在附近找找规律,保持代码风格一致。
3.2 第二步:配置SELinux权限(关键且易错)
这是最重要也最容易出错的一步。如果没有正确配置SELinux规则,即使属性编译进了系统,普通进程(甚至system_server)也可能无法写入它,导致setprop失败并提示权限错误。
- 定位SELinux策略文件:设备相关的SELinux策略通常位于
device/<制造商>/<设备名>/sepolicy/或vendor/<制造商>/<设备名>/sepolicy/目录。 - 修改
property_contexts文件:找到property_contexts文件。这个文件将属性名模式映射到安全上下文。我们需要为我们新增的属性添加一条规则。在文件末尾添加:persist.sys.custom_feature_enabled u:object_r:system_prop:s0- 解释:这表示名为
persist.sys.custom_feature_enabled的属性,其安全上下文是system_prop。这是一个常用的、允许系统服务读写的前缀。
- 解释:这表示名为
- (可选但推荐)添加Te规则:如果你希望进一步控制哪些域(Domain,即进程类型)可以读写这个属性,需要在
.te文件中声明。例如,在system_server.te(如果你想允许系统服务修改它)或你自己的服务域.te文件中添加:# 允许域对 system_prop 类型的属性进行读写 allow myservice system_prop:property_service { set };- 更精细的控制:你可以创建新的属性类型,而不是使用通用的
system_prop。例如,在property.te中声明type custom_feature_prop, property_type;,然后在property_contexts中指定persist.sys.custom_feature_enabled u:object_r:custom_feature_prop:s0,最后只允许特定的域访问custom_feature_prop。这安全性更高,但复杂度也增加。
- 更精细的控制:你可以创建新的属性类型,而不是使用通用的
避坑指南:
- SELinux拒绝(avc: denied):这是添加属性后最常见的问题。当你执行
setprop时,如果遇到权限错误,一定要先adb shell然后执行su提权到root,再执行setprop测试。如果root下成功,但非root或特定服务下失败,基本就是SELinux问题。 - 排查方法:使用
adb logcat | grep avc或adb shell dmesg | grep avc查看SELinux拒绝日志。日志会明确告诉你哪个进程(scontext)、试图对哪个属性(tcontext)进行什么操作(perm denied)。根据日志来补充对应的allow规则。 - 规则生效:修改SELinux策略后,必须重新编译并刷写
bootimage或vendorimage(具体取决于策略文件所在分区),因为策略文件是在内核启动早期加载的。仅编译systemimage可能不生效。
3.3 第三步:在代码中访问属性
属性定义好并配置好权限后,就可以在代码中使用了。
在Java/Kotlin中:
import android.os.SystemProperties; // 读取属性,第二个参数是默认值 String value = SystemProperties.get("persist.sys.custom_feature_enabled", "0"); boolean isEnabled = "1".equals(value); // 设置属性(需要相应权限) SystemProperties.set("persist.sys.custom_feature_enabled", "1");SystemProperties类是一个隐藏API,但系统应用和特权应用可以直接使用。第三方应用通常无法调用。
在C/C++ Native代码中:
#include <cutils/properties.h> char value[PROPERTY_VALUE_MAX] = {'\0'}; property_get("persist.sys.custom_feature_enabled", value, "0"); // 第三个参数是默认值 if (strcmp(value, "1") == 0) { // 功能开启 } property_set("persist.sys.custom_feature_enabled", "1"); // 设置属性在Shell脚本或ADB中:
# 读取 adb shell getprop persist.sys.custom_feature_enabled # 设置 adb shell setprop persist.sys.custom_feature_enabled 1 # 或者在设备Shell内直接操作 setprop persist.sys.custom_feature_enabled 13.4 第四步:编译与验证
编译:在源码根目录,根据你修改的文件所在分区进行编译。
# 如果主要修改了 device 或 vendor 下的文件 source build/envsetup.sh lunch your_target-eng # 选择你的目标 make -j$(nproc) bootimage vendorimage systemimage如果修改范围小,也可以只编译
bootimage。刷机:将编译出的镜像刷入设备。例如,使用
fastboot flash boot boot.img等命令。验证:
- 验证默认值:设备首次启动后,在adb shell中执行
getprop persist.sys.custom_feature_enabled,应该输出0。 - 验证持久化:执行
setprop persist.sys.custom_feature_enabled 1,然后重启设备。重启后再次执行getprop,应该输出1,证明持久化成功。 - 验证权限:尝试从一个非特权进程(比如一个普通App的JNI代码)中调用
property_set,观察是否会失败,并检查SELinux日志。
- 验证默认值:设备首次启动后,在adb shell中执行
4. 高级应用与问题排查
4.1 属性触发动作:让属性变化执行命令
init进程的另一个强大功能是,可以根据属性变化来触发执行命令或服务。这需要在init.rc或其引入的.rc文件中配置。
例如,我们想在自定义功能开关打开时,自动启动一个后台服务,关闭时停止它。
- 找到正确的
.rc文件:通常设备特定的init.rc在device/<制造商>/<设备名>/下。不建议直接修改顶级init.rc,而是在设备目录下创建或修改一个如init.${device}.rc的文件。 - 添加触发器:
# 当 persist.sys.custom_feature_enabled 属性被设置为 1 时 on property:persist.sys.custom_feature_enabled=1 # 启动一个自定义服务,或者执行一个命令 start my_custom_service # 或者写日志到 kernel message write /dev/kmsg "Custom feature enabled" # 当该属性被设置为非 1 (通常是 0) 时 on property:persist.sys.custom_feature_enabled=0 stop my_custom_service write /dev/kmsg "Custom feature disabled" - 定义服务:在同一个
.rc文件中定义my_custom_service。service my_custom_service /system/bin/my_custom_daemon class main user root group root disabled # 默认不启动,由属性触发器控制 oneshot # 如果只执行一次,可以用 oneshot
这样,当你通过setprop或代码改变属性值时,对应的服务就会自动启动或停止,实现了配置与行为的联动。
4.2 常见问题排查实录
问题1:属性设置成功,但重启后恢复默认值。
- 可能原因:属性名没有以
persist.开头。或者/data分区无法挂载(例如在recovery模式下),导致无法写入持久化文件。 - 排查:检查
/data/property/目录下是否有名为persist.sys.custom_feature_enabled的文件。用cat命令查看其内容。如果文件不存在或内容不对,说明持久化机制未生效。
问题2:setprop命令返回,但属性值实际没变,或者getprop看不到变化。
- 可能原因1:属性名拼写错误,或者存在空格等不可见字符。
- 可能原因2(更常见):有另一个进程(可能是
init自身的触发器,或者某个系统服务)在你设置后立即将其改了回去。这被称为“属性竞争”。 - 排查:使用
watch -n 0.5 getprop persist.sys.custom_feature_enabled命令持续观察属性值变化。如果看到值在闪烁变化,就说明存在竞争。需要检查所有.rc文件和相关代码,看是否有其他地方也在设置这个属性。
问题3:SELinux权限配置都改了,但还是被拒绝。
- 可能原因:SELinux策略文件修改后,没有编译进正确的镜像,或者刷机后没有生效(可能需要清除
/data/下的旧属性文件?不,通常不需要)。 - 排查:
- 确认修改的
property_contexts文件确实被编译系统包含。检查BoardConfig.mk中BOARD_SEPOLICY_*变量的配置。 - 刷机后,检查设备上的
/vendor/etc/selinux/或/system/etc/selinux/下的property_contexts文件,确认你的修改已存在。 - 确保设备运行在
enforcing模式(getenforce返回Enforcing),这样才能触发SELinux拒绝日志。
- 确认修改的
问题4:在应用中使用SystemProperties.set抛出异常或静默失败。
- 可能原因:应用没有足够的权限。
SystemProperties.set需要android.permission.ACCESS_SURFACE_FLINGER或signature|privileged级别的权限,普通应用无法获取。 - 解决方案:
- 如果这是系统应用,在
AndroidManifest.xml中添加android:sharedUserId=“android.uid.system”并使用平台签名。 - 或者,通过
adb shell在root下执行setprop命令。 - 或者,创建一个具有权限的系统服务(
SystemService)来代理属性的设置,应用通过Binder调用该服务。
- 如果这是系统应用,在
添加系统属性是一个从构建系统、权限安全到运行时逻辑都需要通盘考虑的工作。它不仅仅是调用一个API,更是对Android系统架构理解的一次实践。当你成功添加并稳定使用一个自定义属性后,你会发现它为系统级的灵活配置和组件通信打开了一扇新的大门。