1. SystemProperties属性监听机制的前世今生第一次接触SystemProperties属性监听时我也被Java层和底层截然不同的行为搞懵了。明明在init.rc里写个on property:sys.boot_completed1就能自动触发动作为什么在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.javapublic 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类来排查。