10. 数据存储(上):SharedPreferences轻量级存储、内部存储(File)、外部存储(File)、存储权限适配(Android 10/11)

数据存储,说白了就是让App能记住东西。用户登录了,下次打开不用再输密码;游戏打到第几关了,退出后进度还在。这些功能都离不开数据存储。

Android提供了好几种存储方式,这一章我们先聊最常用的三种:SharedPreferences、内部存储和外部存储。嗯,权限适配这块儿坑不少,我会重点说说。

10.1 SharedPreferences:轻量级键值对存储

SharedPreferences,我习惯叫它SP。它适合存一些简单的配置信息,比如用户偏好设置、开关状态、登录token等。

说白了,它就是一个键值对(Key-Value)的存储结构。存的时候给个key,取的时候也用这个key。

10.1.1 基本用法

// 获取SharedPreferences对象
SharedPreferences sp = getSharedPreferences("my_config", Context.MODE_PRIVATE);

// 存数据
SharedPreferences.Editor editor = sp.edit();
editor.putString("username", "张三");
editor.putInt("age", 25);
editor.putBoolean("is_login", true);
editor.apply(); // 或者用commit()

// 取数据
String username = sp.getString("username", "默认值");
int age = sp.getInt("age", 0);
boolean isLogin = sp.getBoolean("is_login", false);

这里有个细节:apply()commit()有什么区别?

我刚开始做开发时也搞混过。commit()是同步写入,会阻塞当前线程;apply()是异步写入,不会阻塞。我个人习惯用apply(),除非你需要在写入后立即知道结果。

小技巧:如果存的是复杂对象,可以转成JSON字符串再存。比如用Gson把对象转成String,存进去,取出来再解析。

10.1.2 多文件管理

你可以创建多个SP文件,按功能模块分开。比如:

// 用户相关的配置
SharedPreferences userSp = getSharedPreferences("user_info", MODE_PRIVATE);
// 应用设置
SharedPreferences settingSp = getSharedPreferences("app_settings", MODE_PRIVATE);

这样做的好处是,清除数据时可以更精细。比如用户退出登录,你只需要清除user_info这个文件,不影响其他设置。

10.2 内部存储(File):App的私有空间

内部存储,每个App都有自己的私有目录。路径一般是/data/data/包名/。这个目录只有你的App能访问,其他App看不到。

适合存一些敏感数据,比如数据库文件、用户头像缓存等。

10.2.1 读写文件

// 写文件
String fileName = "my_data.txt";
String content = "这是要保存的内容";
FileOutputStream fos = openFileOutput(fileName, Context.MODE_PRIVATE);
fos.write(content.getBytes());
fos.close();

// 读文件
FileInputStream fis = openFileInput(fileName);
BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
    sb.append(line);
}
reader.close();
String result = sb.toString();

注意,openFileOutput()openFileInput()是Context提供的方法,文件默认存在内部存储的files目录下。

注意:内部存储空间有限,不要存大文件。我见过有人把视频文件存内部存储,结果App体积暴涨,用户直接卸载了。

10.2.2 缓存目录

除了files目录,还有个cache目录。它适合存临时文件,系统空间不足时会自动清理。

// 获取缓存目录
File cacheDir = getCacheDir();
File cacheFile = new File(cacheDir, "temp_data.txt");

嗯,这里要提醒一下:缓存目录的文件随时可能被系统删除,所以别把重要数据放这里。

10.3 外部存储(File):共享空间

外部存储,就是大家常说的SD卡或者内置共享存储。路径一般是/storage/emulated/0/

它适合存一些用户可见的文件,比如下载的图片、导出的报表等。

10.3.1 公共目录 vs 私有目录

外部存储分两种:

  • 公共目录:比如DCIMDownloadMusic等。所有App都能访问,用户也能通过文件管理器看到。
  • 私有目录:路径是/storage/emulated/0/Android/data/包名/。Android 10以后,这个目录不需要额外权限。
// 获取公共目录
File publicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);

// 获取App私有目录
File privateDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);

我个人建议:如果文件需要被其他App共享,放公共目录;如果只是自己App用,放私有目录更安全。

10.3.2 检查存储状态

外部存储可能被移除(比如用户拔了SD卡),所以操作前最好检查一下:

String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
    // 可以读写
} else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
    // 只能读
} else {
    // 不可用
}

10.4 存储权限适配(Android 10/11)

这一节是重点,也是很多开发者踩坑的地方。Android 10和11对存储权限做了大调整,说白了就是越来越严了。

10.4.1 Android 10:分区存储

Android 10引入了分区存储(Scoped Storage)。App不能随意访问外部存储的公共目录了,只能通过MediaStore或者SAF(存储访问框架)来操作。

但是,如果你的App目标版本是29,可以在AndroidManifest.xml里加一个属性来暂时关闭分区存储:

<application
    android:requestLegacyExternalStorage="true"
    ...>

我当时做适配时,加了这个属性后,旧代码基本不用改。但这只是临时方案,Android 11就不管用了。

10.4.2 Android 11:强制分区存储

Android 11开始,requestLegacyExternalStorage失效了。你必须用新的方式访问文件。

如果你确实需要访问所有文件(比如文件管理器App),可以申请MANAGE_EXTERNAL_STORAGE权限:

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

然后引导用户去系统设置里开启:

Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
警告:这个权限审核很严格,Google Play要求你的App必须有正当理由才能使用。普通App别乱申请,否则可能被下架。

10.4.3 推荐做法

我建议你这样做:

  • 如果只是存自己App的数据,用内部存储或外部私有目录,不需要任何权限。
  • 如果需要访问图片、视频、音频,用MediaStore。
  • 如果需要访问任意文件,用SAF(存储访问框架),让用户自己选择文件。
// 使用SAF让用户选择文件
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.setType("*/*");
startActivityForResult(intent, REQUEST_CODE);

10.5 知识体系总览

下面这张图,我把本章的知识点串起来了。你看一眼就能明白各种存储方式的关系和适用场景。

Android数据存储方式 SharedPreferences 内部存储(File) 外部存储(File) 键值对存储 适合配置、偏好、Token apply()异步 / commit()同步 /data/data/包名/ files目录 / cache目录 私有空间,无需权限 公共目录 / 私有目录 Android 10/11权限适配 MediaStore / SAF 核心原则: 私有数据用内部存储,共享数据用外部存储,简单配置用SP

10.6 避坑指南

最后,分享几个我踩过的坑:

  • SP存大对象:我曾经把整个用户信息对象转成JSON存SP,结果数据量大了之后,读取速度明显变慢。后来改用数据库了。
  • 忘记关闭流:文件操作一定要在finally里关闭流,或者用try-with-resources。我早期有个bug就是流没关,导致文件一直占用。
  • 权限申请时机:不要在App启动时就申请存储权限,用户会很反感。等到真正需要读写文件时再申请,成功率更高。
  • Android 11兼容:如果你的App targetSdkVersion是30,记得测试所有文件操作。我有个项目上线后才发现,旧代码在Android 11上直接崩溃了。

数据存储这块儿,说白了就是选对工具、用对方法。简单配置用SP,私有数据用内部存储,共享文件用外部存储。权限适配这块,紧跟Android版本变化就行。


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