深入 Android 用户数据备份恢复全链路:从 Auto Backup 传输机制到 Key/Value Backup 与 DataStore 迁移的数据安全保障

去年接手一个海外社交 App 时,用户反馈换机后登录态丢了——不是 Token 过期,是整个 SharedPreferences 文件没恢复回来。排查发现 Auto Backup 在 Android 12 上静默跳过了超过 25MB 的数据文件,且没有任何回调通知。这让我重新审视了 Android 备份恢复的全链路机制。

Auto Backup:你以为的”自动”没那么自动

Android 6.0 引入的 Auto Backup 默认对所有 targetSdkVersion ≥ 23 的 App 开启。工作方式很粗暴:打包整个 App 私有数据目录上传到 Google Drive,上限 25MB。超过上限直接跳过,不发通知,不留日志——我当时查了三天才定位到这个原因。

核心配置在 Manifest 中:

<application
    android:allowBackup="true"
    android:fullBackupContent="@xml/backup_rules">
</application>

backup_rules.xml 控制哪些文件参与备份:

<full-backup-content>
    <include domain="sharedpref" path="user_prefs.xml" />
    <include domain="database" path="app.db" />
    <exclude domain="file" path="cache/" />
    <exclude domain="external" />
</full-backup-content>

<include> 是白名单模式——一旦定义了任何 include 规则,未列出的文件全部排除。很多开发者只加了 include 却忘记之前默认全量备份的行为变了,文件就这样静默丢失。

BackupManagerService 的调度策略

Auto Backup 不是 App 调 backup() 就立刻执行的。BackupManagerService(BMS)在 SystemServer 进程中统一调度,策略相当保守:

  • 最小间隔 24 小时:两次全量备份之间至少间隔一天,频繁调用 dataChanged() 不会加速。
  • 闲时执行:依赖 JobSchedulersetRequiresDeviceIdle(true),充电且息屏才触发。
  • 超时 30 分钟:一次备份会话最多执行半小时,超时直接 kill 传输进程。

想在测试环境手动验证备份流程,通常要等一天。调试技巧是用 adb shell bmgr 强制触发:

# 启用备份
adb shell bmgr enable true
# 强制运行备份(跳过间隔限制)
adb shell bmgr backupnow <package>
# 查看当前传输状态
adb shell bmgr status

backupnow 在调试阶段是救命命令。但它走的仍是完整传输链路——数据先序列化,经 BMS 路由到 Google Play Services 的传输组件,最后写入 Google Drive。链路中任何一个环节挂掉,整个备份失败且不返回错误详情。

Key/Value Backup:精准但繁琐

Auto Backup 的替代方案是 Key/Value Backup(基于 BackupAgent),适合数据量小且需要精细控制的场景。它把 App 数据拆成独立的 key-value 对,每次只传输变更的部分。

class MyBackupAgent : BackupAgentHelper() {
    override fun onCreate() {
        // SharedPreferences 备份
        addHelper("prefs", SharedPreferencesBackupHelper(this, "user_prefs"))
        // 自定义文件备份
        addHelper("db", FileBackupHelper(this, "../databases/app.db"))
    }
}

和 Auto Backup 的核心差异:

特性Auto BackupKey/Value Backup
备份粒度整个文件单个 key
增量传输不支持支持(仅变更 key)
25MB 限制硬限制无限制(但单 key ≤ 5KB)
恢复控制onRestore() 可拦截

KV Backup 的限制也很明显:单个 key 的 value 不能超过 5KB,且只适用于 SharedPreferences 和少量文件。2018 年之后 Google 不再为它添加新特性,重心完全转向 Auto Backup。

传输加密:数据在链路上到底安不安全

备份数据从设备到 Google Drive 的链路有三层防护:

  1. TLS 传输加密:BMS → Google Play Services 的 IPC 走本地 Socket,Play Services → Google Drive 走 HTTPS。
  2. 设备端加密:备份数据写入 Drive 前,用设备 PIN/密码派生的密钥加密。Google 也无法解密你的备份。
  3. 端到端加密(E2EE):Android 9+ 引入基于设备锁屏密码的端到端加密,云端存储的是加密 blob,只有用户输入正确锁屏密码才能恢复。

一个踩过的坑:开启了 android:fullBackupContent 但没设置 android:backupInForeground 的应用,在 Android 12+ 上可能因后台限制导致备份传输中断。加上这一行:

<application
    android:backupInForeground="true"
    ...>

它在备份期间显示一个不可取消的通知,换取前台进程优先级,显著降低被 kill 的概率。

DataStore 迁移后的备份策略

团队把 SharedPreferences 迁移到 DataStore 之后,备份策略也需要调整。DataStore 的 .preferences_pb 文件默认在 Auto Backup 范围内,但有两个问题:

  1. 文件体积更大:DataStore 用 Protobuf 存储,同数据量下比 XML SharedPreferences 大约 30%-50%。如果你的 SP 之前是 20MB,迁移后可能摸到 25MB 上限。
  2. 跨进程写入:DataStore 的原子写入依赖 File.fsync() + 临时文件重命名。如果在备份读取时恰好发生写入,Auto Backup 拿到的是不完整文件。

解决方案是排除 .preferences_pb,改用自定义 BackupAgent 在备份前将 DataStore 数据导出到临时文件:

override fun onFullBackup(data: FullBackupDataOutput) {
    // 1. 将 DataStore 序列化到临时 JSON
    val tempFile = File(context.cacheDir, "backup_snapshot.json")
    tempFile.writeText(serializeDataStore())

    // 2. 备份临时文件
    fullBackupFile(tempFile, data)

    // 3. 清理
    tempFile.delete()
}

这样控制备份时间节点的是你,而不是文件系统的时机。

恢复流程中的状态同步陷阱

备份恢复更隐蔽的问题在恢复时机:Auto Backup 恢复发生在 App 首次安装后、Application.onCreate() 执行前。这意味着初始化代码运行时,SharedPreferences 或 DataStore 中已经有了恢复的数据——但你的代码可能没准备好。

一个真实事故:我们在 Application.onCreate() 中做了数据迁移(旧版 SP → DataStore),迁移逻辑检测到 SP 中存在旧 key 就覆盖写入 DataStore。换机恢复后,新设备上 SP 被恢复但 DataStore 为空,迁移逻辑用旧 SP 覆盖了本该从备份中恢复的 DataStore 数据。

修复方式是在恢复完成后才触发迁移:

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        // 延迟到首次 Activity 启动后执行迁移
        // 此时 Auto Backup 恢复已完成
        registerActivityLifecycleCallbacks(...)
    }
}

没有官方 API 能准确判断恢复是否完成,但 registerActivityLifecycleCallbacksonActivityCreated 的时间点实测在恢复之后。

几个省时间的实践原则

adb shell dumpsys backup 先看一眼状态再改代码。这个命令输出当前备份队列、上次备份时间、传输组件版本,大部分”备份不生效”的问题在这里就能定位。

25MB 不是存储限制,是传输限制。如果你的 App 数据目录 30MB,不要试图压缩到 24MB——Auto Backup 的 25MB 是针对每个传输会话,不是云存储限额。拆成两个 <include> 域各 15MB,它照样一次传 30MB。

测试恢复流程时清数据不够。adb shell pm clear 不会触发恢复,因为 BMS 认为 App 还被”使用中”。正确方式是完整卸载再重新安装,或者用 adb shell bmgr restore <token> <package> 指定恢复令牌。

DataStore 提供了更好的类型安全和异步读写,但也让备份策略从”文件拷贝”变成了”数据导出”。这个代价在迁移前值得算清楚——如果你的核心需求是容灾恢复而不是性能,SharedPreferences + KV Backup 的组合反而更稳。