16、配置持久化:WifiConfigStore、XML 序列化、加密存储
Wi-Fi 配置的持久化,说白了就是让手机记住你连过的 Wi-Fi。你想想看,如果每次解锁手机都要重新输入密码,那体验得多糟糕。Android 系统通过一套完整的机制,把 Wi-Fi 配置安全地存到磁盘上,下次开机或重启 Wi-Fi 时自动加载回来。
这套机制的核心就是 WifiConfigStore。我当年刚接触这部分代码时,第一反应是「这不就是个读写文件的操作吗?」后来踩了几个坑才明白,事情远没那么简单。
WifiConfigStore 的职责
WifiConfigStore 是 Wi-Fi 配置持久化的总管。它负责:
- 管理配置数据的读、写、同步
- 处理配置的版本升级与迁移
- 协调多个数据源(用户配置、系统配置、热点推荐等)
- 保证写入的原子性和一致性
它的内部维护了一个 StoreData 列表,每个 StoreData 代表一类配置数据。比如用户保存的 Wi-Fi 网络是一个 StoreData,系统管理的 Passpoint 配置是另一个 StoreData。这样做的好处是职责清晰,互不干扰。
核心设计思路:WifiConfigStore 不直接操作文件,而是通过 StoreData 接口与具体的序列化逻辑解耦。每个 StoreData 只关心自己怎么把数据转成 XML,至于文件怎么存、存哪里,那是 Store 层的事。
XML 序列化:从对象到文件
Android 选择 XML 作为配置的存储格式,原因很简单:可读性强、结构清晰、便于调试。我曾在项目中遇到过一个问题——某个用户反馈 Wi-Fi 配置丢失,我直接拉取设备上的 WifiConfigStore.xml 文件,一眼就看出某个字段被写成了空值。如果是二进制格式,排查起来就麻烦多了。
序列化的核心类是 XmlUtil 和 XmlSerializer。每个 StoreData 需要实现两个方法:
// 序列化:将配置对象写入 XML
public void serializeData(XmlSerializer out, boolean shared)
throws XmlPullParserException, IOException {
// 写入根标签
out.startTag(null, "NetworkList");
// 遍历所有配置
for (WifiConfiguration config : mConfigs) {
out.startTag(null, "Network");
out.attribute(null, "SSID", config.SSID);
out.attribute(null, "configKey", config.configKey());
// ... 写入更多字段
out.endTag(null, "Network");
}
out.endTag(null, "NetworkList");
}
// 反序列化:从 XML 恢复配置对象
public void deserializeData(XmlPullParser in, boolean shared)
throws XmlPullParserException, IOException {
// 解析 XML 树
int eventType = in.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG
&& "Network".equals(in.getName())) {
String ssid = in.getAttributeValue(null, "SSID");
// ... 构造 WifiConfiguration 对象
}
eventType = in.next();
}
}
嗯,这里要注意:序列化时不要遗漏关键字段。我见过一个 bug,开发者忘了序列化 allowedKeyManagement 字段,结果恢复配置后,原本 WPA2 的网络变成了开放网络,用户连上去才发现不安全。
加密存储:保护敏感信息
Wi-Fi 配置里最敏感的就是密码(preSharedKey)。如果明文存储,任何有 root 权限的应用都能读到。Android 从早期版本就开始对密码进行加密,但不同版本的加密方式差异很大。
| Android 版本 | 加密方式 | 密钥来源 |
|---|---|---|
| 4.3 及之前 | AES-128-CBC | 设备特定密钥(硬编码风险) |
| 4.4 - 6.0 | AES-256-GCM | KeyStore + 用户锁屏密码 |
| 7.0 及之后 | AES-256-GCM + 文件级加密 | KeyStore + 用户凭据 |
我个人习惯把加密分为两层:
- 传输层加密:在写入文件前,对密码字段单独加密
- 存储层加密:利用 Android 的文件级加密(FBE),整个文件自动加密
两层叠加,安全性才够。我曾经在分析一个安全漏洞报告时发现,某厂商只做了文件级加密,但没对密码字段单独加密。结果攻击者通过备份恢复机制绕过了 FBE,直接读到了明文密码。从那以后,我坚持两层都要做。
小技巧:加密时记得带上 关联数据(AAD)。比如把 SSID 作为 AAD 的一部分,这样即使两个网络密码相同,加密后的密文也不一样,防止攻击者通过密文比对推断信息。
写入流程与原子性保证
配置写入最怕什么?写到一半断电,文件损坏,配置全丢。WifiConfigStore 通过三步策略保证原子性:
- 写临时文件:先写入
.tmp后缀的临时文件 - 同步到磁盘:调用
FileDescriptor.sync()确保数据落盘 - 重命名:原子性地将临时文件重命名为正式文件
代码实现大致如下:
private void writeToFile(File file, byte[] data) throws IOException {
File tmpFile = new File(file.getParent(), file.getName() + ".tmp");
FileOutputStream fos = new FileOutputStream(tmpFile);
try {
fos.write(data);
fos.getFD().sync(); // 强制刷盘
} finally {
fos.close();
}
// 原子重命名
if (!tmpFile.renameTo(file)) {
throw new IOException("原子重命名失败");
}
}
这里有个坑:renameTo() 在跨文件系统时可能失败。所以临时文件和目标文件必须在同一个目录下。我见过有人把临时文件写到了缓存目录,然后重命名到数据目录,结果在某些设备上总是失败,排查了半天才发现是文件系统不同。
警告:不要依赖 FileOutputStream 的 close() 来保证数据落盘。close() 只保证数据从应用缓冲区刷到内核缓冲区,但不保证内核缓冲区刷到磁盘。必须显式调用 sync() 或 flush() + sync()。
配置版本升级与迁移
Android 版本迭代时,Wi-Fi 配置的格式也会变。比如 Android 12 引入了新的安全类型,Android 13 增加了对 WPA3 增强开放网络的支持。如果直接读取旧格式的配置,就会解析失败。
WifiConfigStore 通过 版本号 来管理兼容性。每个 StoreData 都有一个版本字段,反序列化时检查版本号:
- 如果版本号匹配,直接解析
- 如果版本号较低,执行升级逻辑(填充默认值、转换字段格式)
- 如果版本号过高,说明是未来版本写入的,当前系统可能无法完全解析
我建议你在自定义 StoreData 时,从一开始就设计好版本号机制。哪怕当前只有一个版本,也要预留这个字段。否则后续升级时,你会发现旧设备上的配置全部失效,用户只能重新连接所有 Wi-Fi。
SVG 流程图:配置持久化核心流程
这个流程图展示了配置从内存到磁盘的完整路径。每一步都有对应的保护措施:序列化保证结构正确,加密保证数据安全,原子写入保证不丢数据。三者缺一不可。
调试与排查技巧
配置持久化出问题时,最常见的现象就是「Wi-Fi 配置丢失」或「配置恢复后无法连接」。我的排查思路是这样的:
- 检查文件是否存在:
adb shell ls /data/misc/wifi/WifiConfigStore.xml - 检查文件权限:确保只有 Wi-Fi 进程有读写权限
- 解析 XML 内容:
adb shell cat /data/misc/wifi/WifiConfigStore.xml,看字段是否完整 - 检查加密状态:看密码字段是否被正确加密(密文应该是 Base64 编码的字符串)
- 查看日志:过滤
WifiConfigStore或WifiConfigManager的日志,看是否有写入失败或解析异常
我曾经遇到过一个诡异的问题:某款设备在重启后,Wi-Fi 配置全部丢失。查了三天,最后发现是 sync() 调用在特定文件系统上返回了错误,但代码没有检查返回值,导致数据根本没落盘。从那以后,我写文件操作时都会检查 sync() 的返回值,虽然它通常不会失败,但一旦失败,后果很严重。
调试小工具:你可以用 adb shell dumpsys wifi 查看当前内存中的配置状态,对比 XML 文件中的内容,快速定位是写入问题还是读取问题。
配置持久化看起来是个小功能,但它是 Wi-Fi 服务稳定性的基石。用户不会关心你的序列化逻辑写得有多优雅,他们只关心「为什么我连过的 Wi-Fi 不见了」。把这块做扎实了,用户投诉就能少一大半。
公众号:蓝海资料掘金营,微信deep3321