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 文件,一眼就看出某个字段被写成了空值。如果是二进制格式,排查起来就麻烦多了。

序列化的核心类是 XmlUtilXmlSerializer。每个 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 + 用户凭据

我个人习惯把加密分为两层:

  1. 传输层加密:在写入文件前,对密码字段单独加密
  2. 存储层加密:利用 Android 的文件级加密(FBE),整个文件自动加密

两层叠加,安全性才够。我曾经在分析一个安全漏洞报告时发现,某厂商只做了文件级加密,但没对密码字段单独加密。结果攻击者通过备份恢复机制绕过了 FBE,直接读到了明文密码。从那以后,我坚持两层都要做。

小技巧:加密时记得带上 关联数据(AAD)。比如把 SSID 作为 AAD 的一部分,这样即使两个网络密码相同,加密后的密文也不一样,防止攻击者通过密文比对推断信息。

写入流程与原子性保证

配置写入最怕什么?写到一半断电,文件损坏,配置全丢。WifiConfigStore 通过三步策略保证原子性:

  1. 写临时文件:先写入 .tmp 后缀的临时文件
  2. 同步到磁盘:调用 FileDescriptor.sync() 确保数据落盘
  3. 重命名:原子性地将临时文件重命名为正式文件

代码实现大致如下:

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() 在跨文件系统时可能失败。所以临时文件和目标文件必须在同一个目录下。我见过有人把临时文件写到了缓存目录,然后重命名到数据目录,结果在某些设备上总是失败,排查了半天才发现是文件系统不同。

警告:不要依赖 FileOutputStreamclose() 来保证数据落盘。close() 只保证数据从应用缓冲区刷到内核缓冲区,但不保证内核缓冲区刷到磁盘。必须显式调用 sync()flush() + sync()

配置版本升级与迁移

Android 版本迭代时,Wi-Fi 配置的格式也会变。比如 Android 12 引入了新的安全类型,Android 13 增加了对 WPA3 增强开放网络的支持。如果直接读取旧格式的配置,就会解析失败。

WifiConfigStore 通过 版本号 来管理兼容性。每个 StoreData 都有一个版本字段,反序列化时检查版本号:

  • 如果版本号匹配,直接解析
  • 如果版本号较低,执行升级逻辑(填充默认值、转换字段格式)
  • 如果版本号过高,说明是未来版本写入的,当前系统可能无法完全解析

我建议你在自定义 StoreData 时,从一开始就设计好版本号机制。哪怕当前只有一个版本,也要预留这个字段。否则后续升级时,你会发现旧设备上的配置全部失效,用户只能重新连接所有 Wi-Fi。

SVG 流程图:配置持久化核心流程

WifiConfigStore 配置持久化核心流程 用户配置 / 系统配置 StoreData 序列化 XmlSerializer → XML 节点树 加密存储 AES-256-GCM + 文件级加密 原子写入(三步策略) 写临时文件 → sync() 刷盘 → 原子重命名 WifiConfigStore.xml

这个流程图展示了配置从内存到磁盘的完整路径。每一步都有对应的保护措施:序列化保证结构正确,加密保证数据安全,原子写入保证不丢数据。三者缺一不可。

调试与排查技巧

配置持久化出问题时,最常见的现象就是「Wi-Fi 配置丢失」或「配置恢复后无法连接」。我的排查思路是这样的:

  1. 检查文件是否存在adb shell ls /data/misc/wifi/WifiConfigStore.xml
  2. 检查文件权限:确保只有 Wi-Fi 进程有读写权限
  3. 解析 XML 内容adb shell cat /data/misc/wifi/WifiConfigStore.xml,看字段是否完整
  4. 检查加密状态:看密码字段是否被正确加密(密文应该是 Base64 编码的字符串)
  5. 查看日志:过滤 WifiConfigStoreWifiConfigManager 的日志,看是否有写入失败或解析异常

我曾经遇到过一个诡异的问题:某款设备在重启后,Wi-Fi 配置全部丢失。查了三天,最后发现是 sync() 调用在特定文件系统上返回了错误,但代码没有检查返回值,导致数据根本没落盘。从那以后,我写文件操作时都会检查 sync() 的返回值,虽然它通常不会失败,但一旦失败,后果很严重。

调试小工具:你可以用 adb shell dumpsys wifi 查看当前内存中的配置状态,对比 XML 文件中的内容,快速定位是写入问题还是读取问题。

配置持久化看起来是个小功能,但它是 Wi-Fi 服务稳定性的基石。用户不会关心你的序列化逻辑写得有多优雅,他们只关心「为什么我连过的 Wi-Fi 不见了」。把这块做扎实了,用户投诉就能少一大半。


公众号:蓝海资料掘金营,微信deep3321