Android 存储体系与分区存储
前言
Android 存储最容易被误解成"有没有 SD 卡权限"。这个说法在今天已经不够准确:现代 Android 把存储拆成应用私有数据、应用专属外部目录、共享媒体、用户选择的文档 URI、数据库、偏好设置以及少数受严格政策约束的全文件访问能力。开发者真正要回答的问题不是"文件放哪里都能读写吗",而是:这份数据是谁的、是否要被其他应用看到、卸载后是否保留、用户是否明确选择、系统是否允许按路径访问。
分区存储(scoped storage)不是单个 API,而是一套把"共享外部存储"重新划边界的访问模型。它的目标是减少应用随意扫描、修改和泄露用户文件的机会,同时保留相机、相册、下载、文档编辑、文件管理器等真实场景的能力。本文从工程决策开始,串起私有目录、MediaStore、SAF、权限变化、源码路径和迁移策略,帮助你把"能跑的文件读写"升级成"符合当前 Android 隐私模型的存储设计"。
截至 2026-07-07,本文按 Android 16 / API 36 文档、本地 Android SDK API 36 framework sources 和 AOSP main 分支路径复查。存储、相册权限和 Google Play 全文件访问政策都属于高频变化区域,生产代码要以目标 SDK、设备版本和官方文档为准。
前置知识
建议先理解:
- A023:Intent 与 PendingIntent,因为 SAF、照片选择器和系统设置页都通过 Intent 边界授予访问能力。
- A027:Context 区别,因为
filesDir、cacheDir、getExternalFilesDir()都从Context暴露。 - A029:权限模型,因为 Android 13+ 媒体权限、Android 14 部分照片访问和
MANAGE_EXTERNAL_STORAGE都建立在权限模型之上。
先记住一个实用判断表:
| 数据类型 | 首选方案 | 是否随卸载删除 | 是否需要存储权限 | 典型 API |
|---|---|---|---|---|
| 登录态、配置、小型结构化数据 | DataStore / Room / 内部文件 | 是 | 否 | DataStore、Room、filesDir |
| 临时缓存 | 内部或外部 cache | 是,且可能提前清理 | 否 | cacheDir、externalCacheDir |
| 应用独占的大文件 | 外部应用专属目录 | 是 | Android 4.4+ 否 | getExternalFilesDir() |
| 用户希望在相册看到的图片/视频/音频 | 共享媒体集合 | 否 | 写入通常不需要,读取他人媒体按版本申请 | MediaStore |
| PDF、Office、压缩包等用户选择文件 | SAF | 否 | 否,用户选择 URI | ACTION_OPEN_DOCUMENT、ACTION_CREATE_DOCUMENT |
| 文件管理器/备份/杀毒等核心全盘场景 | 全文件访问 | 否 | 需要特殊授权和政策理由 | MANAGE_EXTERNAL_STORAGE |
这张表比背"内部/外部存储"更有用。Android 官方文档也把应用专属文件、共享媒体、文档、偏好和数据库分别放在不同访问模型下,而不是鼓励所有数据都落到 /sdcard。
核心概念
1. 内部存储不是"手机内置存储"的同义词
在 Android API 语义里,内部存储通常指应用私有目录,例如 context.filesDir 和 context.cacheDir。这些目录由系统按应用 UID 隔离,其他普通应用不能直接访问,适合 token、业务数据库、草稿、加密前的敏感数据和不应共享的业务文件。缺点是空间相对有限,且卸载应用时会随应用删除。
工程上要避免把"用户想导出的文件"长期塞进内部私有目录。如果文件要被用户带走或被其他应用打开,应通过 FileProvider 临时授权、MediaStore 写入共享媒体,或通过 SAF 让用户选择保存位置。
2. 外部存储不是"可插拔 SD 卡"的同义词
Android 设备上常见的 /sdcard 往往是 emulated external storage。它可能位于内部闪存上,但 API 语义仍属于外部存储。外部存储还包括可移除 SD 卡、USB OTG 等卷。外部应用专属目录通常形如:
text
/storage/emulated/0/Android/data/<package>/files/
/storage/emulated/0/Android/data/<package>/cache/
这些目录适合应用独占的大文件、离线包、日志缓存、模型文件缓存等。Android 4.4 及以上访问自己的外部应用专属目录不需要存储权限,但文件不保证永远可用:可移除介质可能被拔出,用户或系统也可能清理空间。
3. 共享媒体应该走 MediaStore,而不是手写公共路径
图片、视频和音频属于共享媒体。现代 Android 推荐通过 MediaStore.Images、MediaStore.Video、MediaStore.Audio 插入和查询,而不是拼接 /sdcard/DCIM/xxx.jpg。写入新媒体时要使用 DISPLAY_NAME、MIME_TYPE、RELATIVE_PATH、IS_PENDING 等列;官方文档明确提醒,创建或更新媒体文件时不要依赖 DATA 列,而应使用 DISPLAY_NAME 和 RELATIVE_PATH。
分区存储之后,MediaStore 实际扮演了"共享媒体索引 + 访问仲裁"的角色。应用能直接管理自己创建的媒体;要修改、删除不属于自己的媒体,则通常需要用户确认,或使用 MediaStore.createWriteRequest()、createDeleteRequest() 这样的请求流。
4. 文档和任意文件优先 SAF
PDF、ZIP、日志导出、Office 文档这类"不属于相册媒体集合"的用户文件,首选 Storage Access Framework。它通过系统文件选择器让用户选择具体文件或目录,应用拿到的是 content:// URI,而不是永久全盘路径。用户参与选择后,应用无需申请传统存储权限;如果需要跨重启长期访问,要配合 takePersistableUriPermission() 保存 URI 授权。
SAF 的关键不是"文件选择器 UI",而是用户把某个具体资源授权给某个应用。这个模型比全盘扫描更符合隐私,也更适合云盘、文档提供者和跨设备文件系统。
5. 分区存储改变的是共享外部存储边界
Android 10 引入分区存储,Android 11 开始对 target 30+ 忽略 requestLegacyExternalStorage。这意味着历史上"申请 READ/WRITE_EXTERNAL_STORAGE 后遍历 /sdcard"的模式不能作为新应用的默认设计。今天的正确模型是:
- 应用私有数据放内部目录或应用专属目录。
- 用户可见媒体通过
MediaStore写入或读取。 - 非媒体文档通过 SAF 由用户选择。
- 全文件访问只给文件管理器、备份、杀毒等核心场景,并受 Google Play 政策约束。
整体架构
可以把 Android 存储看成三道门:
- 应用沙箱门:私有目录和数据库默认只服务当前 UID。
- Provider 门:共享媒体和文档通过
ContentResolver、MediaProvider、DocumentsProvider仲裁。 - 权限/用户确认门:读取他人媒体、编辑他人媒体、选择文件、全文件访问,都需要用户或系统策略参与。
工作流程
写入一张用户可见图片
这里有两个容易忽略的点:
- 插入新媒体不等于立即对外可见。
IS_PENDING=1期间适合写入未完成文件,写完后再改为 0。 - 对媒体使用
content://URI 是正常路径,不要为了"拿真实路径"而绕回DATA列。
读取用户选择的文档
SAF 流程更像"拿授权票据":
- 应用发起
ACTION_OPEN_DOCUMENT或ACTION_CREATE_DOCUMENT。 - 用户在系统 UI 中选择文件或保存位置。
- 应用收到
content://URI。 - 应用通过
ContentResolver.openInputStream()或openOutputStream()读写。 - 如果需要长期访问,保存 URI 并调用
takePersistableUriPermission()。
这个流程的权限粒度是 URI,不是目录路径。即使底层文件来自本地、云盘或企业文档系统,应用也只应该通过 URI 协议访问。
迁移 legacy 外部存储
老项目升级 target SDK 时,迁移不要从"把权限补齐"开始,而要先做资产盘点:
| 旧路径/旧行为 | 迁移方向 | 说明 |
|---|---|---|
/sdcard/<app>/cache |
externalCacheDir |
可清理缓存,不承诺长期存在 |
/sdcard/<app>/files |
getExternalFilesDir() |
应用独占、随卸载删除 |
/sdcard/DCIM 手写图片 |
MediaStore.Images |
用户可见媒体,使用 RELATIVE_PATH |
/sdcard/Download/*.pdf |
SAF 或 MediaStore.Downloads |
根据是否由用户选择和平台版本决策 |
| 扫描全盘找文件 | SAF 目录授权或业务侧导入 | 不要默认全盘扫描 |
| 文件管理核心能力 | MANAGE_EXTERNAL_STORAGE |
只在确为核心功能且符合政策时申请 |
API 使用
1. 写入私有文件
kotlin
fun savePrivateDraft(context: Context, articleId: String, body: String) {
val file = File(context.filesDir, "draft-$articleId.md")
file.parentFile?.mkdirs()
file.writeText(body, Charsets.UTF_8)
}
fun readPrivateDraft(context: Context, articleId: String): String? {
val file = File(context.filesDir, "draft-$articleId.md")
return file.takeIf { it.exists() }?.readText(Charsets.UTF_8)
}
私有文件适合业务内部状态,但不适合直接把 File 暴露给其他应用。要分享内部文件,应通过 FileProvider 生成临时 content:// URI,并加上 FLAG_GRANT_READ_URI_PERMISSION。
2. 写入共享图片到相册
kotlin
suspend fun savePngToPictures(
context: Context,
displayName: String,
bytes: ByteArray,
): Uri = withContext(Dispatchers.IO) {
val resolver = context.contentResolver
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, displayName)
put(MediaStore.Images.Media.MIME_TYPE, "image/png")
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/StorageDemo")
put(MediaStore.Images.Media.IS_PENDING, 1)
}
val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val uri = resolver.insert(collection, values)
?: error("Create MediaStore row failed")
try {
resolver.openOutputStream(uri, "w")?.use { output ->
output.write(bytes)
} ?: error("Open output stream failed")
values.clear()
values.put(MediaStore.Images.Media.IS_PENDING, 0)
resolver.update(uri, values, null, null)
uri
} catch (t: Throwable) {
resolver.delete(uri, null, null)
throw t
}
}
这个示例有三个生产要点:
- I/O 放到
Dispatchers.IO,避免阻塞主线程。 - 先设置
IS_PENDING=1,写入成功后再公开。 - 写入失败要删除半成品记录,避免相册里出现损坏条目。
Android 10 及以上,如果应用只是向共享存储添加文件,通常不需要申请存储权限。只有读取其他应用已有媒体、管理他人媒体或访问更宽范围时,才进入权限和用户确认流程。
3. 使用 Photo Picker 获取图片
kotlin
class AvatarActivity : ComponentActivity() {
private val pickImage = registerForActivityResult(
ActivityResultContracts.PickVisualMedia()
) { uri: Uri? ->
if (uri != null) {
renderAvatar(uri)
}
}
fun onChangeAvatarClicked() {
pickImage.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}
}
如果需求只是让用户选一张或几张照片,Photo Picker 往往比申请媒体读取权限更好。它让用户明确选择资源,应用无需持有整个相册的读取权限。
4. 使用 SAF 打开文档
kotlin
class ImportActivity : ComponentActivity() {
private val openDocument = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri: Uri? ->
if (uri != null) {
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION,
)
importDocument(uri)
}
}
fun onImportClicked() {
openDocument.launch(arrayOf("application/pdf", "text/plain"))
}
private fun importDocument(uri: Uri) {
contentResolver.openInputStream(uri)?.use { input ->
// 解析业务文件,注意不要假设它来自本地路径。
}
}
}
SAF 代码里不要把 Uri.path 当作文件系统路径。正确做法是通过 ContentResolver 读写流,并把 URI 当成长期资源标识。
5. Android 13/14 媒体读取权限
kotlin
fun mediaReadPermissions(): Array<String> = when {
Build.VERSION.SDK_INT >= 34 -> arrayOf(
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED,
)
Build.VERSION.SDK_INT >= 33 -> arrayOf(
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO,
)
else -> arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}
Android 13 把图片、视频、音频读取拆成更细的 READ_MEDIA_* 权限;Android 14 引入照片/视频部分访问。如果应用自己维护相册选择器,就要处理"全量、部分、拒绝"三种状态;如果只是选图,优先用 Photo Picker,减少权限面。
源码分析
Framework API 层
本地 Android SDK API 36 sources 中可以看到几类入口:
| 源码路径 | 关注点 | 代表 API/字段 |
|---|---|---|
frameworks/base/core/java/android/content/Context.java |
暴露应用目录入口 | getFilesDir()、getCacheDir()、getExternalFilesDir()、getExternalFilesDirs() |
frameworks/base/core/java/android/os/Environment.java |
定义公共目录、外部存储状态和全文件访问检查 | DIRECTORY_DOCUMENTS、getExternalStoragePublicDirectory()、isExternalStorageManager() |
frameworks/base/core/java/android/provider/MediaStore.java |
共享媒体集合和写入请求 | RELATIVE_PATH、IS_PENDING、createWriteRequest()、createDeleteRequest() |
Context 是应用拿目录的门面;Environment 保留了很多历史公共目录 API,但它的注释和官方文档都倾向引导应用使用应用专属目录、MediaStore 或 SAF;MediaStore 则把共享媒体从"路径集合"提升为"带元数据和权限仲裁的 Provider 表"。
MediaStore 与 MediaProvider
MediaStore 位于 framework API 层,开发者通过它构建 URI、插入元数据、打开流、发起用户确认请求。真正维护媒体数据库和文件映射的实现位于 AOSP packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java。
典型写入链路可以概括为:
text
App -> ContentResolver.insert() -> MediaStore collection URI
-> MediaProvider 插入媒体行 -> 返回 content URI
-> ContentResolver.openOutputStream() 写入文件
-> update(IS_PENDING=0) 公开媒体
这个链路解释了为什么 IS_PENDING 是媒体写入的关键列:它让应用在文件尚未完整写入时先占位,避免其他应用索引到半成品。RELATIVE_PATH 则表达媒体在共享集合中的相对位置,替代旧式绝对路径拼接。
SAF 与 DocumentsProvider
SAF 的 framework 入口主要是 Intent action、DocumentsContract 和 ContentResolver。系统文件选择器背后接入多个 DocumentsProvider:本机外部存储、Downloads、云盘、企业文档提供者都可以成为 provider。应用拿到的 URI 是 provider 授权的能力,而不是底层文件路径。
这也解释了两个常见现象:
- 同一个 URI 可能没有真实本地路径,或者真实路径对应用不可见。
- 读写性能取决于 provider,可能是本地文件,也可能是网络或加密存储。
StorageManagerService 与底层存储栈
frameworks/base/services/core/java/com/android/server/StorageManagerService.java 负责更底层的卷、挂载、用户存储和系统服务协调。普通应用不直接调用这个服务,而是通过 Context、StorageManager、ContentResolver、MediaStore、SAF 等稳定 API 间接使用存储能力。
在 Android 10 之后,外部存储访问还涉及 FUSE/sdcardfs 演进、UID 隔离、AppOps、Provider 检查等多个层面。对应用开发者来说,重要的是不要把底层挂载细节当成业务契约;路径能在一台设备上访问,不代表在目标 SDK、厂商 ROM 或未来 Android 版本上仍可访问。
线程模型
存储 I/O 不属于主线程工作。无论是 File、ContentResolver.openInputStream()、MediaStore 写入,还是 SAF provider 读写,都可能触发磁盘、数据库、跨进程甚至网络访问。推荐原则:
- ViewModel 或 Repository 中用
Dispatchers.IO包裹阻塞 I/O。 - UI 只持有 URI、加载状态和业务状态,不直接在 Composable 中读大文件。
- 大文件复制要支持取消、进度和失败清理。
- 对
content://流不要假设可重复读取,必要时复制到应用缓存再处理。
实战案例
场景:图片编辑器保存作品
需求:用户从相册选一张图,应用编辑后保存结果,并允许在系统相册看到。
推荐设计:
- 用 Photo Picker 让用户选择源图,避免申请全量相册读取权限。
- 将中间编辑状态保存到内部 cache 或
externalCacheDir。 - 用户点击保存时,通过
MediaStore.Images写入Pictures/<AppName>。 - 写入时使用
IS_PENDING,失败时删除半成品。 - 如果要覆盖原图,不直接写原 URI;使用
MediaStore.createWriteRequest()让用户确认,或另存为新文件。
这个设计的优点是权限面小、失败边界清晰、作品能被相册看到,也不会把中间缓存暴露给其他应用。
场景:企业应用导入 PDF
需求:用户从本机或云盘导入合同 PDF,应用解析并保存业务记录。
推荐设计:
- 用
ACTION_OPEN_DOCUMENT或ActivityResultContracts.OpenDocument()选择 PDF。 - 调用
takePersistableUriPermission()保存读取授权。 - 解析时通过
ContentResolver.openInputStream()读取。 - 若业务需要离线长期处理,复制一份到内部私有目录,并在数据库记录原始 URI 和导入时间。
- 不申请
MANAGE_EXTERNAL_STORAGE,不扫描/sdcard/Download。
这个设计尊重用户选择,也兼容云盘和企业文档 provider。
场景:旧应用升级 target 30+
迁移顺序建议:
- 扫描代码里的硬编码
/sdcard、Environment.getExternalStorageDirectory()、DATA列依赖。 - 给每类文件标注归属:私有、缓存、共享媒体、用户文档、全文件核心能力。
- 先迁移写入路径,再迁移读取路径。
- 对历史文件做一次性搬迁,搬迁完成后记录版本标记,避免每次启动扫描。
- 目标 SDK 升级前用真实设备覆盖 Android 10、11、13、14+ 权限和 URI 行为。
性能优化
1. 少做全盘扫描
全盘扫描在分区存储下既不稳定,也很慢。即便拿到了宽权限,遍历大量文件也会消耗 I/O、触发 provider 查询和电量成本。更好的做法是:
- 媒体用
MediaStore条件查询,只取需要列。 - 文档让用户通过 SAF 选择。
- 业务文件维护自己的索引表,而不是每次启动扫目录。
- 大目录分页加载,避免一次性把所有 URI 和缩略图塞进内存。
2. 对 MediaStore 查询做投影和排序
查询共享媒体时,不要 projection = null。只取 _ID、DISPLAY_NAME、MIME_TYPE、DATE_ADDED、SIZE 等需要字段。缩略图交给图片加载库通过 URI 懒加载,不要在查询循环中同步解码大图。
kotlin
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED,
)
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
sortOrder,
)?.use { cursor ->
// 分批映射到轻量模型,UI 层再按需加载缩略图。
}
3. 写入大文件要可恢复
大文件写入共享存储时,至少处理:
- 写入失败删除 pending 项。
- 低空间错误提示用户清理空间。
- 复制过程支持取消。
- 进程死亡后能识别未完成任务并清理或重试。
4. 缓存有上限
cacheDir 和 externalCacheDir 都不是永久存储。系统可能在空间不足时清理缓存,应用也应该主动维护 LRU 或总大小限制。把关键业务数据放 cache 是线上丢数据的常见根因。
常见问题
Q1:Android 10 之后还需要 WRITE_EXTERNAL_STORAGE 吗?
大多数新写入共享媒体的场景不需要。Android 10 及以上向共享存储添加文件通常可以通过 MediaStore 完成,不应为了保存图片或 PDF 默认申请传统写权限。读取其他应用已有媒体则要按版本处理读取权限或使用 Photo Picker。
Q2:为什么 File(uri.path) 读不到 SAF 选中的文件?
因为 SAF 返回的是 content:// URI,path 不是你的应用可访问的本地文件路径。正确方式是用 ContentResolver.openInputStream(uri) 或 openFileDescriptor(uri, mode)。
Q3:getExternalFilesDir() 里的文件会被相册看到吗?
通常不应把它当作用户共享媒体目录。外部应用专属目录服务当前应用,卸载后会删除。用户希望长期保留并在相册看到的内容,应写入 MediaStore 的图片、视频或音频集合。
Q4:什么情况下才考虑 MANAGE_EXTERNAL_STORAGE?
只有当应用核心功能必须管理大量共享文件,且 SAF、MediaStore 等隐私友好 API 无法满足时才考虑,例如文件管理器、备份恢复、杀毒、安全审计等。即使系统允许申请,Google Play 也会按政策审核使用场景。
Q5:能不能继续用 requestLegacyExternalStorage?
不能把它作为长期方案。Android 11 起,当应用 target Android 11/API 30 及以上时,系统会忽略该标记。它只适合作为 Android 10 兼容迁移阶段的临时缓冲。
Q6:MediaStore 的 DATA 列还能用吗?
读取已有媒体时某些设备上仍可能返回路径,但不应把它作为创建、更新或跨版本兼容的基础。官方文档明确建议创建或更新媒体时使用 DISPLAY_NAME 和 RELATIVE_PATH。
Q7:部分照片访问会影响相册列表吗?
会。Android 14 的 Selected Photos Access 允许用户只授权部分图片/视频。应用不能把"查不到"直接解释为相册为空,应该展示当前授权范围,并提供重新选择入口。
面试考点
- Android 内部存储和外部存储在 API 语义上分别指什么?它们和物理内置存储、SD 卡是什么关系?
filesDir、cacheDir、getExternalFilesDir()、MediaStore、SAF 分别适合什么数据?- 分区存储解决了什么问题?为什么不能再依赖
/sdcard全路径扫描? - Android 11 对
requestLegacyExternalStorage的处理是什么?迁移 legacy 存储时怎么做资产盘点? - 向相册保存图片时为什么建议使用
IS_PENDING?失败时如何清理? MediaStore的RELATIVE_PATH和旧式DATA列有什么区别?- Photo Picker、媒体读取权限和 SAF 的使用边界是什么?
- Android 13/14 对图片视频读取权限有哪些变化?部分照片访问要如何设计 UI?
MANAGE_EXTERNAL_STORAGE能做什么?为什么仍不应该作为普通业务功能的默认方案?- 为什么所有文件 I/O 都不应该放在主线程?
content://URI 还可能带来哪些性能不确定性?
总结
Android 存储设计的核心不是"找一个还能写的路径",而是给每类数据选择正确边界:
- 私有业务数据放内部目录、Room 或 DataStore。
- 应用独占的大文件放外部应用专属目录。
- 用户可见媒体通过
MediaStore。 - 用户选择的文档通过 SAF。
- 选图优先 Photo Picker。
- 全文件访问只留给少数核心场景。
分区存储把历史上的公共外部存储从"应用共享硬盘"变成"按数据类型、用户选择和系统策略授权的资源集合"。当你按数据归属建模,权限弹窗会更少,兼容性会更好,迁移 target SDK 时也不会被路径假设拖住。
扩展阅读
- Android Developers: Data and file storage overview, developer.android.com/training/da...
- Android Developers: Access app-specific files, developer.android.com/training/da...
- Android Developers: Access media files from shared storage, developer.android.com/training/da...
- Android Developers: Access documents and other files from shared storage, developer.android.com/training/da...
- Android Developers: Storage updates in Android 11, developer.android.com/about/versi...
- Android Developers: Photo picker, developer.android.com/training/da...
- Android Developers: Grant partial access to photos and videos, developer.android.com/about/versi...
- Android Developers: Manage all files on a storage device, developer.android.com/training/da...
- AOSP:
frameworks/base/core/java/android/provider/MediaStore.java - AOSP:
frameworks/base/core/java/android/os/Environment.java - AOSP:
frameworks/base/core/java/android/content/Context.java - AOSP:
frameworks/base/services/core/java/com/android/server/StorageManagerService.java - AOSP:
packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java - 系列前文:A029 权限模型与运行时权限
- 下一篇:A031 SharedPreferences 原理与迁移