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(),除非你需要在写入后立即知道结果。
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目录下。
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 私有目录
外部存储分两种:
- 公共目录:比如
DCIM、Download、Music等。所有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);
10.4.3 推荐做法
我建议你这样做:
- 如果只是存自己App的数据,用内部存储或外部私有目录,不需要任何权限。
- 如果需要访问图片、视频、音频,用MediaStore。
- 如果需要访问任意文件,用SAF(存储访问框架),让用户自己选择文件。
// 使用SAF让用户选择文件
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.setType("*/*");
startActivityForResult(intent, REQUEST_CODE);
10.5 知识体系总览
下面这张图,我把本章的知识点串起来了。你看一眼就能明白各种存储方式的关系和适用场景。
10.6 避坑指南
最后,分享几个我踩过的坑:
- SP存大对象:我曾经把整个用户信息对象转成JSON存SP,结果数据量大了之后,读取速度明显变慢。后来改用数据库了。
- 忘记关闭流:文件操作一定要在finally里关闭流,或者用try-with-resources。我早期有个bug就是流没关,导致文件一直占用。
- 权限申请时机:不要在App启动时就申请存储权限,用户会很反感。等到真正需要读写文件时再申请,成功率更高。
- Android 11兼容:如果你的App targetSdkVersion是30,记得测试所有文件操作。我有个项目上线后才发现,旧代码在Android 11上直接崩溃了。
数据存储这块儿,说白了就是选对工具、用对方法。简单配置用SP,私有数据用内部存储,共享文件用外部存储。权限适配这块,紧跟Android版本变化就行。