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_STORAGEWRITE_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存储的整体脉络:

Android File存储 内部存储 外部存储 缓存管理 getFilesDir() / getCacheDir() openFileOutput() / openFileInput() Environment.getExternalStorageDirectory() 需申请读写权限 (Android 10+ 分区存储) 内部缓存 / 外部缓存 设置上限 + LRU淘汰策略 核心原则:敏感数据用内部存储,大文件用外部存储,缓存要设上限

13.5 实战建议

说了这么多,最后给几点实战建议:

  1. 优先使用内部存储 — 除非文件特别大,否则尽量用内部存储。省心,不用处理权限问题。
  2. 缓存一定要设上限 — 我见过太多应用因为缓存无限增长导致崩溃。设置50MB上限,配合LRU策略,基本够用。
  3. 注意Android版本差异 — Android 10+的分区存储是个大坑。如果你的应用要兼容老版本,记得做版本判断。
  4. 文件操作要异步 — 读写文件是IO操作,千万别在主线程执行。用协程或线程池处理。
  5. 及时关闭流 — 这个老生常谈了,但确实容易忘。用try-with-resources或者finally块确保流被关闭。

我的习惯:写文件操作时,我会先写一个工具类,把常用的读写、缓存、清理方法都封装好。这样项目里其他地方直接调用就行,不用重复造轮子。嗯,代码复用是程序员的美德。

好了,File存储的内容就讲到这里。记住一句话:文件操作看似简单,但细节决定成败。多写、多测、多总结,你也能成为文件存储的高手。


公众号:蓝海资料掘金营,微信deep3321