9、数据存储(上):SharedPreferences存储、内部存储与外部存储、文件读写操作、SQLite数据库与Room框架入门

数据存储,说白了就是让App记住东西。用户登录状态、设置偏好、聊天记录、下载的图片……这些都得有个地方放。Android提供了好几种存储方案,各有各的脾气。今天咱们先把上半场讲透——从轻量级的SharedPreferences,到文件读写,再到数据库和Room框架的入门。

核心脉络:轻量配置用SP → 文件存二进制 → 结构化数据上数据库 → Room帮你省掉样板代码。

Android 数据存储(上)知识体系 数据存储方案 SharedPreferences 键值对存储 适合小配置、用户偏好 内部存储 文件读写,私有目录 其他App不可访问 外部存储 公共目录,需权限 适合大文件、共享文件 SQLite 数据库 关系型数据库 适合结构化数据 Room 框架 SQLite 的 ORM 封装 编译时校验SQL

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