Android 的缓存目录选择:cacheDir vs filesDir vs externalCacheDir
Android 的缓存目录选择:cacheDir vs filesDir vs externalCacheDir
一个被系统清理搞崩的线上事故
去年维护的一个工具类 App 在 Android 14 上出现了批量崩溃。日志指向同一个原因:下载的临时文件在操作中途被删除,导致 FileInputStream 抛出 FileNotFoundException。复现路径很诡异——用户把 App 切后台一段时间,再回来继续操作就崩了。
排查下来,问题出在 getCacheDir() 的使用上。这个目录在 Android 14 的新后台行为下,系统清理频率比旧版本激进得多。Google 在 Android 14 的变更文档里确实提到了这一点:当应用处于缓存状态(cached state)时,系统可能更积极地释放 cache 目录空间。但文档没说的是,这个"更积极"在实际设备上表现为:某些 OEM 厂商在后台策略里把 cache 清理阈值调得很低,甚至应用进入后台几分钟就会触发。
这次事故让我意识到,Android 的缓存目录选择不是"用 cacheDir 存临时文件"这么简单。三个主要目录——getCacheDir() 返回的 /data/data/<package>/cache、getFilesDir() 返回的 /data/data/<package>/files,以及 getExternalCacheDir() 返回的 /Android/data/<package>/cache——各自的行为边界在不同 API 级别、不同厂商定制、不同存储管理策略下都有微妙差异。
三个目录的底层路径与权限模型
先厘清基础事实。在 Android 13(API 33)的模拟器上跑一段简单的路径探测:
Context.getCacheDir() → /data/user/0/com.example.app/cache
Context.getFilesDir() → /data/user/0/com.example.app/files
Context.getExternalCacheDir() → /storage/emulated/0/Android/data/com.example.app/cachegetCacheDir() 和 getFilesDir() 都位于应用私有目录下,受 Linux 文件权限保护(drwx------),其他应用无法访问。getExternalCacheDir() 虽然也在应用专属路径下(/Android/data/<package>),但位于共享存储分区,权限模型不同。
关键差异在系统对待它们的态度。getCacheDir() 从 Android 设计之初就被标记为"可由系统任意清理",官方文档的原话是 "The system will automatically delete files in this directory as space is needed elsewhere on the device"。getFilesDir() 没有这种标记,属于持久存储。getExternalCacheDir() 的语义和 getCacheDir() 类似,但位于外部存储,受 READ_EXTERNAL_STORAGE / MANAGE_EXTERNAL_STORAGE 等权限演进的影响更深。
Android 10(API 29)引入的分区存储(Scoped Storage)是个重要分水岭。在此之前,getExternalCacheDir() 创建的文件可以通过路径直接访问,其他有存储权限的应用也能读到。Android 10 之后,应用只能通过 MediaStore 或 Storage Access Framework 访问共享存储,但 /Android/data/ 路径是个特殊区域——应用自己包名下的目录仍然可以直接访问,只是其他应用无法通过路径遍历进来了。这个变化导致很多旧代码在 Android 10 上行为异常,尤其是那些依赖 Environment.getExternalStorageDirectory() 做文件中转的模块。
cacheDir 的清理机制:比文档写的更复杂
回到那次 Android 14 的事故。我专门写了一个测试应用,在不同版本上监控 cache 目录的存活情况。
测试逻辑很简单:在 onStop() 里往 cacheDir 写一个标记文件,记录时间戳;在 onResume() 检查文件是否存在。配合 adb 命令模拟系统存储压力:
adb shell pm trim-caches 100MAndroid 12(API 31)的 Pixel 设备上,执行 trim-caches 后标记文件大概率还在。Android 14(API 34)的 Pixel 上,同样的命令几乎必定触发清理。更麻烦的是,某些国产 ROM 在系统设置里加了"一键清理"或"垃圾清理"功能,这些功能通常会把 cacheDir 作为重点扫描目标,而且不会区分应用是否正在运行。
我抓过小米 HyperOS 的清理日志,发现其清理策略会读取 /data/system/package_cache 下的应用状态,对"后台超过 30 分钟且 cache 大小超过阈值"的应用直接删除整个 cache 目录。这个行为不违反 Android 兼容性定义文档(CDD),因为 CDD 对 cache 清理的时机没有强制规定,只要求"系统可以在需要时清理"。
这就带来一个实际编码问题:如果你用 cacheDir 存放下载中的临时文件,必须假设文件可能在任何时候消失。Android 的 DownloadManager 实际上把临时文件放在 getCacheDir()/downloads 下,但它在下载过程中会持有文件描述符(fd),即使路径被删除,fd 仍然有效——这是 Linux 文件系统的特性。很多开发者不知道这个技巧,自己实现的下载模块在路径被清理后直接崩溃。
我现在的做法是:需要跨生命周期保留的临时文件,要么放 filesDir 并自己管理清理策略,要么用 cacheDir 但配合 FileDescriptor 或 ParcelFileDescriptor 做防护。对于大文件临时缓存,更倾向用 getExternalCacheDir(),因为外部存储的清理优先级通常低于内部 cache,且空间更大。
filesDir 的隐性成本:卸载重装与备份
getFilesDir() 作为持久存储,问题不在系统清理,在备份和卸载行为。
Android 6.0(API 23)引入的 Auto Backup 机制,默认会把 filesDir 下的内容备份到 Google 云端。很多开发者没意识到这一点,把敏感数据或大型缓存文件放在 filesDir,结果用户卸载重装后数据莫名其妙回来了,或者备份配额超限导致备份失败。
控制备份范围需要在 AndroidManifest.xml 里配置 android:fullBackupContent:
<application
android:fullBackupContent="@xml/backup_rules">
</application>然后在 res/xml/backup_rules.xml 里排除特定目录:
<full-backup-content>
<exclude domain="file" path="large_cache/"/>
</full-backup-content>这个配置在 Android 12(API 31)之前是 fullBackupContent,Android 12 之后 Google 推 D2D(device-to-device)备份,引入了 dataExtractionRules,需要同时维护两套规则。实际项目中我见过因为只配了旧规则,导致新设备迁移时敏感数据泄露的案例。
另一个坑是 filesDir 的空间管理。内部存储的配额由 getCacheQuotaBytes() 和 getCacheDir() 相关 API 控制,但 filesDir 没有直接的配额限制 API。系统只在存储极度紧张时才会提示用户清理,不会像 cache 那样自动删除。这意味着放在 filesDir 的文件如果不主动清理,会一直累积。我维护的一个应用曾经因为把日志文件放在 filesDir 且未设轮转策略,导致某用户设备上积累了 8GB 日志,触发系统存储告警。
externalCacheDir 的分区存储陷阱
getExternalCacheDir() 在 Android 10 前后的行为变化最大。Android 9(API 28)及以下,这段代码可以正常工作:
File extCache = getExternalCacheDir();
File sharedFile = new File(extCache, "shared.jpg");
// 其他应用可以通过路径直接读取
Uri uri = Uri.fromFile(sharedFile);Android 10 上,Uri.fromFile() 在跨应用分享时会抛出 FileUriExposedException,这是 Google 强制推行 FileProvider 的一部分。但更深层的改变是:即使在自己应用内部,getExternalCacheDir() 返回的路径在分区存储下也受到更多限制。
我在 Android 13 的设备上测试过,getExternalCacheDir() 返回的目录应用可以正常读写,不需要任何运行时权限。但如果用户从系统设置里撤销了"文件和媒体"权限(Android 13 把存储权限细化为 READ_MEDIA_IMAGES/VIDEO/AUDIO),getExternalCacheDir() 仍然可访问——这是因为它属于应用专属目录,不在权限管控范围内。这个行为在 Android 11(API 30)之前不同,Android 11 之前撤销存储权限会影响对外部存储所有区域的访问。
一个容易忽略的细节:getExternalCacheDir() 返回的目录可能在 SD 卡未挂载时不可用。虽然现在的 Android 设备很少支持物理 SD 卡,但模拟的外部存储(emulated)在某些情况下也会"不可用"——比如设备通过 USB 以 MTP 模式连接电脑时,部分厂商的实现会锁定外部存储,导致 getExternalCacheDir() 返回的目录无法写入。这个行为没有统一标准,Samsung 和 Pixel 的处理就不一致。
具体场景下的选择策略
经过这些踩坑,我对三个目录的使用形成了相对明确的判断标准。
网络下载的临时文件:如果下载过程需要跨 Activity 生命周期甚至应用进程存活,不要用 cacheDir。Android 14 的后台清理会让下载中途失败。我的方案是:小文件(< 10MB)用 filesDir 的子目录,下载完成后移动到最终位置;大文件用 getExternalCacheDir(),配合 DownloadManager 或自己维护 fd 引用。
图片/视频编辑的中间产物:这类文件通常体积大、生命周期和编辑会话绑定。我用 getExternalCacheDir(),在编辑 Activity 的 onCreate 里清理该会话的遗留文件。关键是在 onSaveInstanceState 里保存当前会话的目录名,防止进程重建后找不到临时文件。
离线缓存的索引数据库:Room 数据库默认放在 filesDir 的 databases 子目录,这是合理的。但如果是纯缓存性质、可以重建的数据库(比如新闻列表的离线缓存),我倾向于用 cacheDir,同时实现重建逻辑。这样系统清理后应用能自动恢复,而不是崩溃。
日志文件:绝对不要放 cacheDir,系统清理会导致日志丢失,不利于问题排查。也不要直接放 filesDir 根目录,用 filesDir/logs 子目录,并配置备份排除规则。轮转策略必须自己实现,Android 没有内置的日志轮转 API。
跨应用分享的中转文件:Android 7.0(API 24)之后必须用 FileProvider,文件可以放在 cacheDir 或 filesDir,通过 FileProvider.getUriForFile() 生成 content:// URI。这里有个性能细节:cacheDir 位于内部存储,FileProvider 读取时不需要跨存储分区;externalCacheDir 在外部存储,某些设备上可能有额外的 IO 开销。对于频繁分享的场景,内部 cache 更高效。
版本差异与厂商定制的实际影响
Android 14 的变更不止影响了 cacheDir 的清理频率。Google 在 Android 14 还引入了 RECEIVE_EXPLICIT_USER_INTERACTION 权限和更严格的后台启动限制,间接改变了应用对文件操作的时机假设。
一个具体的例子:推送到达后,应用从后台启动下载,文件还没写完就被系统放入"受限待机"(restricted standby)状态。在 Android 13 及以下,进入受限待机后网络访问被切断,但已打开的 fd 通常还能继续写入。Android 14 上,某些设备会同时冻结文件系统缓存,导致写入操作挂起直到应用下次前台化,或者超时失败。这个行为在 AOSP 代码里没有直接体现,但我在 Pixel 8 的 Android 14 上通过 strace 观察到了 futex 等待模式的变化。
国产厂商的定制更加不可预测。OPPO 的 ColorOS 在 Android 13 上引入了"应用速冻"机制,后台应用超过 72 小时未使用会被完全冻结,解冻时 cacheDir 可能被清空。vivo 的 OriginOS 有类似的"省电策略",但阈值是 48 小时。这些行为不会出现在官方文档里,只能通过实际测试或用户反馈发现。
我现在的项目中,对关键临时文件会做多重保护:主副本在 filesDir,定期同步到 externalCacheDir 作为灾备,同时记录文件存在性到 SharedPreferences(虽然 SharedPreferences 也有性能问题,但至少不会被系统清理)。这种冗余在低端机上增加了 IO 开销,但稳定性提升明显。
一个关于存储空间计算的细节
Android 的存储空间 API 在不同目录上的行为也值得注意。StatFs 可以用来查询文件系统的可用空间,但针对的路径不同,结果可能不同。
内部存储和外部存储通常是不同的文件系统分区。在 Pixel 7 上测试:
StatFs internalStat = new StatFs(getCacheDir().getAbsolutePath());
StatFs externalStat = new StatFs(getExternalCacheDir().getAbsolutePath());internalStat.getAvailableBytes() 和 externalStat.getAvailableBytes() 的结果经常不一致,某些设备上差异能达到数 GB。这是因为内部存储是 /data 分区,外部存储是 /sdcard 或 fuse 挂载点,空间分配策略不同。
StorageManager 在 Android 8.0(API 26)引入了 getAllocatableBytes(),比 StatFs 更准确,因为它考虑了系统的保留空间和缓存配额。但这个 API 的行为在 cacheDir 和 filesDir 上也有差异——对 cache 目录,返回的是"系统在清理后可能释放的空间",而不是当前实际可用空间。
我在文件下载前做空间检查时,会同时用 getAllocatableBytes(FileDescriptor) 和 StatFs,取较小值,并预留 10% 缓冲。这个策略在 Android 10 以下的设备上尤其重要,因为旧版本的 getAllocatableBytes 实现有 bug,在 cache 目录上可能返回负数。
代码层面的防御性实践
最后分享几个经过验证的代码片段,都是实际项目中沉淀下来的。
判断目录可用性的可靠方式不是 file.exists(),而是尝试写入并立即删除:
boolean isDirWritable(File dir) {
File test = new File(dir, ".probe" + System.currentTimeMillis());
try {
if (test.createNewFile()) {
test.delete();
return true;
}
} catch (IOException e) {
// 目录可能被挂载为只读,或权限异常
}
return false;
}这个检查在 getExternalCacheDir() 上尤其必要,因为外部存储的挂载状态可能变化。
跨进程或跨组件传递文件路径时,不要用 String 路径,用 Uri 或 ParcelFileDescriptor。Android 14 的 Service 后台限制让路径在传递过程中失效的情况更常见:
// 不可靠:路径可能在接收方使用时已失效
intent.putExtra("path", file.getAbsolutePath());
// 更可靠:传递 fd,即使路径被清理,已打开的 fd 仍然有效
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
intent.putExtra("fd", pfd);清理策略上,不要依赖系统对 cacheDir 的自动清理。自己实现 LRU 清理,按最后修改时间排序:
void trimCache(File dir, long maxBytes) {
File[] files = dir.listFiles();
if (files == null) return;
Arrays.sort(files, Comparator.comparingLong(File::lastModified));
long total = 0;
for (File f : files) total += f.length();
for (File f : files) {
if (total <= maxBytes) break;
total -= f.length();
f.delete();
}
}这个清理逻辑在 Application.onTrimMemory() 里调用,配合 ComponentCallbacks2 的内存级别判断,比被动等待系统清理更可控。
对 Jetpack 存储库的看法
Google 推出的 Jetpack 存储库试图简化这些混乱,比如 Room 封装了数据库路径,DataStore 替代 SharedPreferences,WorkManager 处理后台文件操作。但存储目录的选择本身没有被抽象掉——Room 默认用 filesDir,WorkManager 的临时文件用 cacheDir,这些选择是库内部的,开发者如果不知道底层路径,遇到系统清理或备份问题时仍然束手无策。
我个人不太认同把目录选择完全交给框架的做法。Android 的存储生态碎片化太严重,框架的默认行为在 AOSP 上合理,在 OEM 定制系统上可能完全错误。至少要知道文件最终落在哪个目录,才能针对性地做防御。
MediaStore API 是另一个例子。Google 推荐用 MediaStore 保存图片视频到共享存储,但 MediaStore 的 IS_PENDING 标志在 Android 10 和 11 上的行为不一致,Android 12 又引入了 setRequireOriginal() 的权限变化。这些 API 的演进速度比目录路径快得多,也更容易出兼容性问题。对于不需要跨应用分享的文件,直接用 filesDir 或 externalCacheDir 反而更简单可靠。
写在最后
Android 的存储目录选择是个"看起来简单,做起来全是细节"的话题。三个主要目录的语义差异在 AOSP 层面还算清晰,但叠加版本变迁、厂商定制、后台策略演进之后,实际行为变得难以预测。那次 Android 14 的线上事故教会我最核心的一点:不要对任何目录的持久性做绝对假设,即使是 filesDir,也要考虑备份、卸载、权限撤销等场景。
现在的项目中,我会在代码审查时专门检查存储路径的使用,把 cacheDir 的使用限制在真正可丢失的纯缓存数据上,所有需要跨会话保留的文件都有明确的清理策略和灾备方案。这些防御性代码增加了复杂度,但在 Android 这个生态里,这是必要的代价。