1. SystemProperties属性监听机制的前世今生
第一次接触SystemProperties属性监听时,我也被Java层和底层截然不同的行为搞懵了。明明在init.rc里写个on property:sys.boot_completed=1就能自动触发动作,为什么在Java代码里设置了属性后,还要手动调用SystemPropPoker.poke()才能触发回调?这就像你家的门铃,邻居按了会自动响(底层通知),自家人按了反而要手动敲锣(主动触发),这种设计确实让人费解。
SystemProperties本质上是Android系统的全局键值存储,所有进程共享同一套属性空间。它的监听机制演进经历了三个阶段:
- 原始阶段:仅支持init进程通过rc文件监听属性变化
- 扩展阶段:C++层开放PropertyChanged事件接口
- 完善阶段:Java层通过addChangeCallback实现跨进程监听
这种分层演进带来一个关键问题:事件传递路径的不对称性。底层属性服务通过socket通信(/dev/socket/property_service)实现跨进程修改,但事件通知却存在两种完全不同的传播机制。
2. 底层PropertyChanged的主动通知机制
2.1 init.rc的魔法背后
在system/core/init/property_service.cpp中,你会看到这个关键函数:
void PropertyChanged(const std::string& name, const std::string& value) { if (property_triggers_enabled) { ActionManager::GetInstance().QueuePropertyChange(name, value); WakeMainInitThread(); } }这就是rc文件能自动响应属性变化的秘密。当属性通过以下途径被修改时:
- 启动阶段的属性加载(如
ro.build.type) - 通过
__system_property_set的底层设置 - init进程直接修改的属性
PropertyChanged会被自动调用,触发rc文件中定义的action。这种机制效率极高,因为:
- 事件直接从属性服务进程发出
- 仅init进程需要监听,没有跨进程开销
- 采用同步触发模式,延迟可控
2.2 底层通知的局限性
但如果你在C++层尝试监听属性变化,会发现根本没有现成接口!这是因为:
- 安全考量:避免属性变更事件被滥用
- 性能约束:跨进程事件广播可能引发风暴
- 架构设计:init作为系统管家独享此特权
实测一个典型场景:当你通过property_set("debug.sf.vsync", "1")修改VSync配置时,底层确实会触发PropertyChanged,但只有init.rc能收到这个事件,其他native进程完全感知不到。
3. Java层的被动监听设计
3.1 addChangeCallback的工作原理
Java层的监听机制完全另起炉灶,核心代码在frameworks/base/core/java/android/os/SystemProperties.java:
public static void addChangeCallback(@NonNull Runnable callback) { synchronized (sChangeCallbacks) { if (sChangeCallbacks.size() == 0) { native_add_change_callback(); } sChangeCallbacks.add(callback); } }这里有个关键细节:首次注册监听时,会通过JNI调用native_add_change_callback()。这个native调用在frameworks/base/core/jni/android_os_SystemProperties.cpp中注册了一个全局回调。
但坑爹的是,这个回调不会在属性修改时自动触发!它只是建立了回调通道,真正的事件触发需要依赖callChangeCallbacks()的手动调用。
3.2 为什么需要主动触发?
通过分析SystemPropPoker的源码,你会发现它实际上是通过reportSyspropChanged()触发了一个虚拟的属性变更事件。这种设计主要考虑:
- 性能优化:避免Java层频繁处理属性变更
- 线程安全:集中处理回调避免并发问题
- 批量更新:支持多次属性修改后统一通知
举个例子,系统服务启动时可能连续修改十几个属性:
SystemProperties.set("service.boot.start", "1"); SystemProperties.set("service.core.ready", "true"); SystemProperties.set("service.phase", "init"); SystemPropPoker.getInstance().poke(); // 只触发一次回调如果不这样设计,每个set操作都会立即触发回调,可能导致:
- 重复无效的界面刷新
- 线程竞争导致的死锁
- 回调处理顺序不可控
4. 实战中的适配策略
4.1 正确使用Java监听
这里有个我踩过的坑:直接在Activity中注册监听会导致内存泄漏。正确做法应该是:
// 在Application或Service中 SystemProperties.addChangeCallback(mPropertyObserver); // 需要配合 private final Runnable mPropertyObserver = new Runnable() { @Override public void run() { if (SystemProperties.get("debug.trace").equals("1")) { startTracing(); } } }; // 修改属性时 SystemProperties.set("debug.trace", "1"); SystemPropPoker.getInstance().poke();4.2 混合监听方案
对于关键系统属性,我推荐混合监听策略:
- 对于init可控属性,使用rc文件监听
- 对于Java层属性,使用addChangeCallback
- 对于Native关键属性,可以hook property_set实现
比如电池温度监控可以这样实现:
// native层 void my_property_set(const char* key, const char* value) { if (strcmp(key, "battery.temperature") == 0) { notify_battery_temp_change(atoi(value)); } return __system_property_set(key, value); }4.3 调试技巧
当监听不生效时,可以用这个命令检查回调注册:
adb shell dumpsys activity provider android.os.SystemProperties | grep callbacks如果发现回调未触发,检查:
- 是否漏掉poke()调用
- 属性key是否拼写错误
- 监听线程是否被阻塞
记得有次调试时,发现回调死活不执行,最后发现是某个第三方库把sChangeCallbacks列表清空了。这种问题可以通过hook SystemProperties类来排查。