Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

存储体系与 Scoped Storage

★ 数据持久化是客户端的根本。面试不仅考“怎么存”,更考“存哪里最安全”、“大文件怎么管”以及“多进程怎么防丢数据”。

一、轻量级 KV 存储:SharedPreferences 与 DataStore

特性SharedPreferences (SP)Preferences DataStore
线程安全是(但容易造成主线程阻塞)是(基于协程与 Flow)
类型安全弱(可能抛 ClassCastException)弱(但有 Type DataStore 支持强类型)
异步 APIapply() 异步,且可能阻塞生命周期完全原生异步操作
多进程支持MODE_MULTI_PROCESS 已废弃,极不靠谱支持,配合 MMKV 等替代方案更佳

SP 的核心痛点commit() 同步阻塞;apply() 虽然异步,但在 Activity/Service 销毁时,系统层(QueuedWork)会等待所有 apply 的磁盘写入完成,极易导致 ANR。

二、关系型数据库:SQLite 与 Room

  • Room 的优势:编译时 SQL 语法检查;与协程/Flow/LiveData 无缝结合;基于注解的类型转换与实体映射。
  • WAL 模式 (Write-Ahead Logging):开启后 (setWriteAheadLoggingEnabled),读写可以并发执行,极大提升并发性能。写操作先追加到 -wal 文件,稍后再 Checkpoint 同步到 .db
  • 数据库升级 (Migration)
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
    }
}

三、文件目录与沙盒机制

Android 的私有目录分为内部存储和外部存储的私有部分,应用卸载时都会被删除:

  1. 内部存储 (Internal Storage): Context.getFilesDir(), Context.getCacheDir()。空间非常有限,绝对不能放图片视频等大文件。
  2. 外部私有存储 (External Storage - App-specific): Context.getExternalFilesDir(), Context.getExternalCacheDir()。空间较大,适合放应用的业务文件。不需要存储权限即可访问。

四、分区存储 (Scoped Storage) 与 SAF

在 Android 10+ 引入,为了防止应用在 SD 卡上“拉屎”。

  • 自己产生的业务文件:放沙盒 (getExternalFilesDir),不需要权限,卸载自动删。
  • 需要分享/保存给用户的多媒体:通过 MediaStore 插入(图片、视频、音频)。插入不需要权限,读取别的应用插入的媒体需要 READ_MEDIA_IMAGES 等权限。
  • 让用户选择文件/非媒体文件:使用 SAF (Storage Access Framework),即 ACTION_OPEN_DOCUMENT。由系统选择器 UI 授权,返回一个不可伪造的 Document URI。

五、安全存储:Keystore 与 EncryptedSharedPreferences

  • 敏感信息(如 Token、密码)怎么存? 不能明文写在 SP 或文件里。推荐使用 Jetpack Security 库中的 EncryptedSharedPreferences
  • 底层原理:它利用 Android Keystore 系统生成并保管主密钥(Master Key),主密钥存放在设备的硬件安全模块(TEE/SE)中,极难被提取。然后用主密钥加密真实的数据。

高频面试题

Q1:SharedPreferences 为什么会导致 ANR?怎么解决? 答:因为 apply() 提交的任务会进入系统级的 QueuedWork 队列。在组件(如 Activity)执行 onStop 时,系统为了保证数据不丢,会强制等待队列中的磁盘写入任务完成。如果写入量大或磁盘 IO 慢,就会阻塞主线程导致 ANR。 解决:迁移到 DataStore 或 MMKV;或者通过反射清理 QueuedWork(黑科技,不推荐)。

Q2:如何优雅地处理几十上百 MB 的大文件下载与缓存清理? 答:

  1. 存储位置:放在 Context.getExternalCacheDir() 下,千万别放内部存储。
  2. 清理策略:实现 LRU 算法的磁盘缓存工具,设定最大容量(如 200MB)。在每次写入后检查总大小,淘汰最旧的未访问文件。
  3. 系统干预:可以覆写 Application 的 onTrimMemory,在设备空间不足时主动清理。

Q3:Room 数据库在跨版本升级时,如果用户跨了好几个版本(比如 V1 直接升级到 V4)怎么处理? 答:Room 支持定义多段 Migration,如 1-2, 2-3, 3-4。在构建 Database 时 addMigrations() 传入所有规则。Room 内部会自动寻找最短的迁移路径(按序应用 1-2,然后 2-3,然后 3-4)。如果没有找到路径且没有配置 fallbackToDestructiveMigration(),App 会崩溃。

易错点 / 追问

  • 混淆 MediaStore 与 File API:在 Android 11+,即使你拿到了 MediaStore 中的文件真实绝对路径 (_data 字段),用 File API 去读写也可能被拒绝,必须使用 ContentResolver.openInputStream() 处理 URI。
  • 忽略多进程 SP 的危险:多进程读写 SP 是数据损坏和丢失的重灾区。追问时一定要提到 MMKV 的共享内存 (mmap) 与文件锁机制是目前多进程 KV 存储的最佳实践。
  • 数据库主线程读写:初学者常在主线程做 DB 查询。必须借助协程调度到 Dispatchers.IO