第25章 系统属性(System Properties)
系统属性,说白了就是Android系统里的全局键值对。每个属性都是一个字符串键和一个字符串值,系统各个组件通过它来交换配置信息。我刚开始接触Android Framework时,觉得这东西不就是个配置文件嘛,后来才发现——嗯,它远比我想象的要复杂。
属性系统原理
Android的属性系统,核心是一个共享内存区域。所有进程都能读,但只有少数进程能写。这个设计很有意思——你想想看,如果每个进程都随意改属性,系统早就乱套了。
具体来说,属性系统分三层:
- 属性存储层:一块mmap出来的共享内存,所有进程都能映射到自己的地址空间
- 属性服务层:init进程启动时开启的property_service,监听/dev/socket/property_service这个socket
- 客户端访问层:通过libcutils提供的API来读写属性
我曾在一次性能优化项目中,发现系统启动时大量进程同时读属性,导致init进程的socket队列爆满。后来怎么解决的?把频繁读取的属性缓存到进程本地,减少对属性服务的调用。
核心流程:设置属性时,客户端通过socket发送请求给init进程,init进程检查权限后更新共享内存,然后通过socket通知所有监听该属性的进程。读取属性时,客户端直接读共享内存,不需要经过init进程。
属性读写API
在Android开发中,我们主要通过JNI调用C层的API来读写属性。Java层封装在SystemProperties这个隐藏类里。
读取属性
// C层API
#include <cutils/properties.h>
char value[PROP_VALUE_MAX];
property_get("ro.build.version.sdk", value, "unknown");
// value就是获取到的属性值,如果不存在则返回默认值"unknown"
// Java层(隐藏API)
import android.os.SystemProperties;
String sdkVersion = SystemProperties.get("ro.build.version.sdk", "unknown");
int sdkInt = SystemProperties.getInt("ro.build.version.sdk", 0);
boolean isEng = SystemProperties.getBoolean("ro.debuggable", false);
个人经验:我习惯在读取属性时总是提供默认值。为什么?因为属性可能不存在,或者进程没有权限读取。不给默认值,代码就可能抛出异常。曾经有个同事没给默认值,结果在某个定制ROM上直接崩溃了。
设置属性
// C层
property_set("persist.sys.timezone", "Asia/Shanghai");
// Java层
SystemProperties.set("persist.sys.timezone", "Asia/Shanghai");
设置属性时要注意,只有系统进程或有root权限的进程才能设置属性。普通应用调用set方法会直接失败。
避坑指南:我曾经在系统服务中频繁调用property_set来更新状态,结果发现性能很差。后来定位到原因——每次set都会触发socket通信和权限检查,高频调用会导致init进程CPU飙升。解决方案是批量更新,或者改用其他通信方式。
属性权限控制
属性权限控制,说白了就是「谁能读、谁能写」。Android通过两种机制来管控:
| 机制 | 说明 | 我的看法 |
|---|---|---|
| SELinux策略 | 定义哪些domain可以访问哪些属性 | 这是最底层的防线,绕不过去 |
| UID/GID检查 | 只有system、root等特定UID才能写属性 | 简单粗暴,但有效 |
| 属性前缀控制 | 不同前缀的属性有不同的写权限 | 比如ro.开头的只读,persist.开头的可持久化 |
具体来说,属性前缀决定了它的行为:
- ro.:只读属性,一旦设置就不能再修改。我见过有人试图在运行时改ro.build.fingerprint,结果当然是失败
- persist.:持久化属性,会写入/data/property目录,重启后保留
- ctl.:控制属性,用于控制init服务的启动和停止
- 其他:普通属性,重启后丢失
权限检查流程:当进程调用property_set时,init进程会先检查SELinux策略,确认该进程的domain是否有权限写这个属性。然后检查UID,只有system、root或特定服务才能写。最后检查属性前缀,比如普通进程不能写ro.开头的属性。
常用系统属性分析
Android系统里有上千个属性,但常用的也就那么几十个。我挑几个重点说说:
构建相关属性
ro.build.version.sdk = 33 // Android API Level
ro.build.version.release = 13 // Android版本号
ro.build.fingerprint = ... // 构建指纹,用于OTA升级匹配
ro.product.model = Pixel 7 // 设备型号
ro.product.manufacturer = Google // 制造商
这些属性在系统启动时由init进程从build.prop文件中加载。我曾在做设备适配时,发现某个App通过读取ro.product.model来判断设备类型,结果因为定制ROM改了这个值导致App崩溃。嗯,这就是为什么Google建议用API Level而不是设备型号来做判断。
调试相关属性
ro.debuggable = 1 // 1表示可调试,user版本通常为0
ro.secure = 0 // 0表示非安全模式,允许adb root
persist.sys.usb.config = mtp,adb // USB连接模式
这些属性在开发调试时非常有用。我记得有一次,客户反馈说他们的设备adb连不上,我一看——ro.debuggable被设成了0,而且persist.sys.usb.config里没有adb。这就是典型的工程配置问题。
性能相关属性
dalvik.vm.heapsize = 256m // Dalvik/ART堆大小
dalvik.vm.heapgrowthlimit = 128m // 应用堆增长限制
ro.sf.lcd_density = 480 // 屏幕密度
debug.sf.hw = 1 // 是否启用硬件加速
这些属性直接影响App的运行表现。我曾经优化过一个内存泄漏的应用,发现它频繁触发GC,后来检查发现dalvik.vm.heapsize被设得太小。调整后,应用流畅度明显提升。
网络相关属性
net.dns1 = 8.8.8.8 // DNS服务器
net.dns2 = 8.8.4.4
net.hostname = android-xxx // 主机名
dhcp.eth0.dns1 = ... // DHCP获取的DNS
网络属性是动态变化的,由netd守护进程管理。我遇到过一个问题:设备切换WiFi后,DNS没有及时更新,导致部分域名解析失败。后来发现是net.dns1属性没有正确刷新,重启网络服务后才解决。
实用技巧:如果你想查看当前系统所有属性,可以用adb shell getprop命令。输出结果会列出所有属性和对应的值。我个人习惯把这个输出保存下来,方便对比不同版本之间的差异。
好了,关于系统属性,核心内容就这些。理解属性系统的原理,掌握读写API,注意权限控制,熟悉常用属性——这四步走完,你就能在Android Framework开发中游刃有余地使用系统属性了。
公众号:蓝海资料掘金营,微信deep3321