9、数据存储(上):SharedPreferences存储、内部存储与外部存储、文件读写操作、SQLite数据库与Room框架入门
数据存储,说白了就是让App记住东西。用户登录状态、设置偏好、聊天记录、下载的图片……这些都得有个地方放。Android提供了好几种存储方案,各有各的脾气。今天咱们先把上半场讲透——从轻量级的SharedPreferences,到文件读写,再到数据库和Room框架的入门。
核心脉络:轻量配置用SP → 文件存二进制 → 结构化数据上数据库 → Room帮你省掉样板代码。
9.1 SharedPreferences —— 轻量级“小本本”
SharedPreferences,我习惯叫它SP。它就是个键值对存储,适合放一些简单的配置项。比如用户是否第一次启动、夜间模式开关、上次登录的用户名等等。
基本用法:
// 获取SP实例
SharedPreferences sp = getSharedPreferences("my_config", MODE_PRIVATE);
// 写入
SharedPreferences.Editor editor = sp.edit();
editor.putString("username", "张三");
editor.putBoolean("is_first_launch", false);
editor.apply(); // 或者 commit()
// 读取
String name = sp.getString("username", "默认值");
boolean first = sp.getBoolean("is_first_launch", true);
我的经验:apply()和commit()有什么区别?apply()是异步写入,速度快,不阻塞UI线程。commit()是同步写入,会返回boolean告诉你是否成功。我一般都用apply(),除非你需要在写入后立刻知道结果。
避坑指南:我曾经在一个项目里用SP存了一个JSON字符串,结果数据越来越大,读写越来越慢。SP不适合存大量数据,它本质上是XML文件,全量读写的。超过10KB的数据,建议用数据库或文件存储。
9.2 内部存储与外部存储
这两个概念容易搞混。我简单说:内部存储是App的私有空间,其他App碰不到。外部存储是公共区域,比如相册、下载目录。
9.2.1 内部存储
每个App安装后,系统会在/data/data/包名/下给你一个目录。你在这里读写文件,不需要任何权限。
// 写入内部存储
String content = "这是内部存储的内容";
FileOutputStream fos = openFileOutput("my_file.txt", MODE_PRIVATE);
fos.write(content.getBytes());
fos.close();
// 读取内部存储
FileInputStream fis = openFileInput("my_file.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
String line = reader.readLine();
reader.close();
你想想看,内部存储的文件,卸载App时会被自动删除。所以适合放一些缓存数据或者用户私密文件。
9.2.2 外部存储
外部存储分两种:公有目录和私有目录。公有目录比如DCIM、Downloads、Music,这些需要读写权限。私有目录是Android/data/包名/下的目录,Android 10以后不需要权限就能读写。
重点:Android 10(API 29)开始,推荐使用Context.getExternalFilesDir()来获取外部私有目录。这样既不用申请权限,卸载时文件也会被清理。
// 获取外部私有目录
File externalDir = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
File file = new File(externalDir, "note.txt");
// 读写操作同上
避坑指南:我曾经在Android 11上踩过坑——直接往/sdcard/下写文件,结果报错。因为Android 11强制推行分区存储,你不能再随意访问公共目录了。要么用MediaStore API,要么就用外部私有目录。
9.3 文件读写操作进阶
基本的FileInputStream/FileOutputStream大家都会。但实际项目中,我建议用一些更优雅的方式。
使用缓存目录:
// 获取缓存目录
File cacheDir = getCacheDir();
File cacheFile = new File(cacheDir, "temp_data");
// 适合放临时文件,系统空间不足时会自动清理
使用Gson读写对象:
// 假设有一个User对象
User user = new User("张三", 25);
Gson gson = new Gson();
String json = gson.toJson(user);
// 写入文件
FileOutputStream fos = openFileOutput("user.json", MODE_PRIVATE);
fos.write(json.getBytes());
fos.close();
// 读取并反序列化
FileInputStream fis = openFileInput("user.json");
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
fis.close();
String jsonStr = new String(buffer);
User savedUser = gson.fromJson(jsonStr, User.class);
我的习惯:文件操作一定要放在子线程里做。别在主线程读写大文件,否则ANR警告等着你。用AsyncTask、Handler或者协程都行。
9.4 SQLite数据库入门
SQLite是Android自带的轻量级关系型数据库。说白了就是一个小型SQL引擎,不需要安装任何服务端。
创建数据库和表:
public class MyDatabaseHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "my_app.db";
private static final int DB_VERSION = 1;
public MyDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
String sql = "CREATE TABLE user (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT," +
"name TEXT NOT NULL," +
"age INTEGER)";
db.execSQL(sql);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS user");
onCreate(db);
}
}
增删改查操作:
MyDatabaseHelper helper = new MyDatabaseHelper(context);
SQLiteDatabase db = helper.getWritableDatabase();
// 插入
ContentValues values = new ContentValues();
values.put("name", "李四");
values.put("age", 30);
long id = db.insert("user", null, values);
// 查询
Cursor cursor = db.query("user", null, null, null, null, null, null);
while (cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndex("name"));
int age = cursor.getInt(cursor.getColumnIndex("age"));
}
cursor.close();
// 更新
ContentValues updateValues = new ContentValues();
updateValues.put("age", 31);
db.update("user", updateValues, "name=?", new String[]{"李四"});
// 删除
db.delete("user", "id=?", new String[]{"1"});
db.close();
避坑指南:Cursor用完后一定要close()。我曾经因为忘记关闭Cursor,导致内存泄漏,数据库操作越来越慢。另外,数据库操作也必须在子线程执行,尤其是大批量数据。
9.5 Room框架入门
Room是Google推出的ORM框架,说白了就是对SQLite的封装。它帮你省掉了写SQLiteOpenHelper、ContentValues、Cursor这些样板代码的麻烦。
添加依赖:
// build.gradle (app)
implementation "androidx.room:room-runtime:2.5.2"
annotationProcessor "androidx.room:room-compiler:2.5.2"
定义实体类:
@Entity(tableName = "user")
public class User {
@PrimaryKey(autoGenerate = true)
public int id;
@ColumnInfo(name = "user_name")
public String name;
public int age;
}
定义DAO接口:
@Dao
public interface UserDao {
@Insert
void insert(User user);
@Query("SELECT * FROM user")
List<User> getAllUsers();
@Query("SELECT * FROM user WHERE id = :userId")
User getUserById(int userId);
@Update
void update(User user);
@Delete
void delete(User user);
}
定义数据库类:
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
private static AppDatabase INSTANCE;
public static AppDatabase getInstance(Context context) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.getApplicationContext(),
AppDatabase.class,
"my_app_room.db"
).build();
}
return INSTANCE;
}
}
使用Room:
AppDatabase db = AppDatabase.getInstance(context);
UserDao userDao = db.userDao();
// 插入
User user = new User();
user.name = "王五";
user.age = 28;
userDao.insert(user);
// 查询
List<User> users = userDao.getAllUsers();
for (User u : users) {
Log.d("Room", u.name + " - " + u.age);
}
为什么推荐Room?因为它在编译时就会检查SQL语句是否正确。我当年用原生SQLite时,经常在运行时才发现SQL写错了,然后崩溃。Room帮你把问题提前暴露,省心不少。
我的建议:新项目直接用Room,别再用原生SQLite了。Room配合LiveData或Flow,还能实现数据变化自动通知UI更新,非常方便。这部分我们后面会详细讲。
好了,数据存储的上半场就到这里。SharedPreferences搞定小配置,文件存储搞定二进制和JSON,SQLite和Room搞定结构化数据。下半场我们会聊到DataStore、网络存储和更高级的持久化方案。嗯,先把这些基础打牢,后面才能飞得起来。
公众号:蓝海资料掘金营,微信deep3321