13、数据存储之File存储:内部存储与外部存储、读写文本文件、缓存图片文件
说到Android的数据存储,File存储绝对是最基础、最直接的方式。我刚开始做Android开发那会儿,数据库还没流行起来,很多配置信息都是直接写文件的。现在虽然有了各种存储方案,但文件操作依然是每个开发者必须掌握的技能。
这一讲,我们就来聊聊Android里的文件存储。说白了,就是搞清楚文件该放哪儿、怎么读写、以及如何管理缓存。
13.1 内部存储 vs 外部存储
Android把文件存储分成了两大类:内部存储和外部存储。很多新手容易搞混,我当年也踩过坑。
13.1.1 内部存储
内部存储是每个应用私有的存储空间。系统会为每个应用分配一个独立的目录,其他应用无法访问。这个目录通常在 /data/data/包名/ 下。
特点:
- 应用卸载时,文件会被自动删除
- 不需要申请权限
- 文件只能被当前应用访问
- 存储空间有限,通常用于保存敏感数据
核心方法:
getFilesDir()— 获取内部存储的文件目录getCacheDir()— 获取内部存储的缓存目录openFileOutput()— 打开文件输出流openFileInput()— 打开文件输入流
13.1.2 外部存储
外部存储就是大家常说的SD卡或内置共享存储。所有应用都可以访问,但需要申请权限。
特点:
- 存储空间大,适合保存大文件
- 应用卸载时,文件可能不会被删除(取决于存储位置)
- 需要申请
READ_EXTERNAL_STORAGE或WRITE_EXTERNAL_STORAGE权限 - Android 10+ 推荐使用分区存储
注意:从Android 10开始,外部存储的访问方式发生了很大变化。如果你还在用老方法直接读写SD卡,大概率会出问题。我曾经在一个项目里就因为这个踩了坑,用户反馈文件保存失败,排查了半天才发现是分区存储的限制。
13.2 读写文本文件
文本文件的读写是最常见的操作。无论是保存用户配置、日志信息,还是缓存网络数据,都离不开它。
13.2.1 写入文本文件
我个人习惯用 FileOutputStream 配合 OutputStreamWriter 来写文件。这样编码可控,不容易出乱码。
// 写入内部存储
public void writeToInternalStorage(Context context, String fileName, String content) {
try {
FileOutputStream fos = context.openFileOutput(fileName, Context.MODE_PRIVATE);
OutputStreamWriter writer = new OutputStreamWriter(fos, "UTF-8");
writer.write(content);
writer.flush();
writer.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 写入外部存储
public void writeToExternalStorage(String fileName, String content) {
// 先检查外部存储是否可用
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
return;
}
File file = new File(Environment.getExternalStorageDirectory(), fileName);
try {
FileOutputStream fos = new FileOutputStream(file);
OutputStreamWriter writer = new OutputStreamWriter(fos, "UTF-8");
writer.write(content);
writer.flush();
writer.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
小技巧:写入文件时,记得用 flush() 方法。我之前遇到过一个问题:数据明明写入了,但文件里就是没有。后来发现是缓冲区没刷新。嗯,这个细节很容易被忽略。
13.2.2 读取文本文件
读取文件相对简单,但要注意处理文件不存在的情况。
// 从内部存储读取
public String readFromInternalStorage(Context context, String fileName) {
StringBuilder content = new StringBuilder();
try {
FileInputStream fis = context.openFileInput(fileName);
InputStreamReader reader = new InputStreamReader(fis, "UTF-8");
BufferedReader bufferedReader = new BufferedReader(reader);
String line;
while ((line = bufferedReader.readLine()) != null) {
content.append(line);
content.append("\n");
}
bufferedReader.close();
reader.close();
fis.close();
} catch (FileNotFoundException e) {
// 文件不存在,返回空字符串
return "";
} catch (IOException e) {
e.printStackTrace();
}
return content.toString();
}
13.3 缓存图片文件
图片缓存是移动应用开发中的常见需求。你想想看,如果每次打开应用都要从网络加载图片,用户体验得多差。所以,合理的缓存策略至关重要。
13.3.1 缓存目录的选择
Android提供了两个缓存目录:
| 目录 | 路径 | 特点 |
|---|---|---|
| 内部缓存 | /data/data/包名/cache/ |
应用卸载时自动删除,系统空间不足时可能被清理 |
| 外部缓存 | /sdcard/Android/data/包名/cache/ |
应用卸载时自动删除,空间更大 |
我个人建议:小图片用内部缓存,大图片用外部缓存。为什么呢?因为内部缓存读写速度快,但空间有限;外部缓存空间大,但读写速度稍慢。
13.3.2 图片缓存实现
下面是一个简单的图片缓存工具类:
public class ImageCacheUtil {
// 缓存图片到内部存储
public static void cacheImageToInternal(Context context, String url, Bitmap bitmap) {
File cacheDir = context.getCacheDir();
String fileName = getFileNameFromUrl(url);
File file = new File(cacheDir, fileName);
try {
FileOutputStream fos = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
fos.flush();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 从缓存读取图片
public static Bitmap getCachedImage(Context context, String url) {
File cacheDir = context.getCacheDir();
String fileName = getFileNameFromUrl(url);
File file = new File(cacheDir, fileName);
if (file.exists()) {
return BitmapFactory.decodeFile(file.getAbsolutePath());
}
return null;
}
// 根据URL生成文件名
private static String getFileNameFromUrl(String url) {
// 简单处理,实际项目中建议用MD5
return String.valueOf(url.hashCode()) + ".jpg";
}
// 清理缓存
public static void clearCache(Context context) {
File cacheDir = context.getCacheDir();
if (cacheDir.exists() && cacheDir.isDirectory()) {
File[] files = cacheDir.listFiles();
if (files != null) {
for (File file : files) {
file.delete();
}
}
}
}
}
避坑指南:我曾经在一个项目中,图片缓存越积越多,最后导致应用崩溃。原因是只缓存不清理。所以,一定要设置缓存上限,比如限制缓存目录不超过50MB。超过时,按最后访问时间删除旧文件。
13.4 知识体系结构图
下面这张图帮你理清File存储的整体脉络:
13.5 实战建议
说了这么多,最后给几点实战建议:
- 优先使用内部存储 — 除非文件特别大,否则尽量用内部存储。省心,不用处理权限问题。
- 缓存一定要设上限 — 我见过太多应用因为缓存无限增长导致崩溃。设置50MB上限,配合LRU策略,基本够用。
- 注意Android版本差异 — Android 10+的分区存储是个大坑。如果你的应用要兼容老版本,记得做版本判断。
- 文件操作要异步 — 读写文件是IO操作,千万别在主线程执行。用协程或线程池处理。
- 及时关闭流 — 这个老生常谈了,但确实容易忘。用try-with-resources或者finally块确保流被关闭。
我的习惯:写文件操作时,我会先写一个工具类,把常用的读写、缓存、清理方法都封装好。这样项目里其他地方直接调用就行,不用重复造轮子。嗯,代码复用是程序员的美德。
好了,File存储的内容就讲到这里。记住一句话:文件操作看似简单,但细节决定成败。多写、多测、多总结,你也能成为文件存储的高手。
公众号:蓝海资料掘金营,微信deep3321