09. 数据存储

2025/2/15 Android 开发基础

一个应用除了展示界面和处理交互,往往还需要持久化一些数据:用户的偏好设置、缓存的图片文本、离线的业务数据等。Android 提供了多种本地存储方式,先看一张选型决策图,对方案全貌有个概念,再逐个展开:

本地存储方案选型决策示意图

按使用场景归类:

  • SharedPreferences:轻量的键值对存储,适合保存配置、开关、状态等少量数据
  • 文件存储:内部存储 / 外部存储的文件读写,适合日志、缓存、下载文件等
  • SQLite 数据库:结构化的关系型数据存储,适合列表数据、业务数据、需要查询过滤的场景
  • 网络存储:数据保存在服务端,本地仅做缓存(后续网络编程章节介绍)

# 一、SharedPreferences 存储

SharedPreferences 使用键值对(key-value)的形式存储数据,底层是一个 xml 文件(保存在应用的内部存储目录 /data/data/<包名>/shared_prefs/ 下),是最简单的本地持久化方案。

# 1.1 获取 SharedPreferences 对象

// Activity的getSharedPreferences方法:第一个参数为文件名,第二个参数为操作模式
SharedPreferences sp = getSharedPreferences("config", Context.MODE_PRIVATE);

操作模式说明:

  • Context.MODE_PRIVATE:默认模式,只有本应用可以读写(推荐使用,其他模式已废弃)
  • Context.MODE_WORLD_READABLE / MODE_WORLD_WRITEABLE:允许其他应用读写,安全风险大,Android 7.0 起已废弃

# 1.2 写入数据(通过 Editor)

写入必须通过 Editor 对象完成,所有写入操作最终以 apply() 或 commit() 提交:

SharedPreferences sp = getSharedPreferences("config", Context.MODE_PRIVATE);
// 获取编辑器
SharedPreferences.Editor editor = sp.edit();
editor.putString("username", "JiangMing");
editor.putInt("age", 26);
editor.putBoolean("is_login", true);
editor.putLong("last_login_time", System.currentTimeMillis());
editor.putFloat("score", 98.5f);
editor.putStringSet("tags", new HashSet<>(Arrays.asList("Android", "Java")));
// 提交保存
editor.apply();

# 1.3 读取数据

读取方法的第二个参数是默认值,即 key 不存在时返回的兜底值:

SharedPreferences sp = getSharedPreferences("config", Context.MODE_PRIVATE);
String username = sp.getString("username", "");
int age = sp.getInt("age", 0);
boolean isLogin = sp.getBoolean("is_login", false);
long lastLoginTime = sp.getLong("last_login_time", 0);
float score = sp.getFloat("score", 0f);
Set<String> tags = sp.getStringSet("tags", new HashSet<String>());

Log.i(TAG, "username = " + username + ", age = " + age + ", isLogin = " + isLogin);

# 1.4 删除数据

SharedPreferences sp = getSharedPreferences("config", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
// 删除指定的key
editor.remove("score");
// 清空全部数据
editor.clear();
// remove和clear可以和写入组合在一次提交中,按调用顺序执行
editor.apply();

# 1.5 apply() 与 commit() 的区别(面试高频)

方法 返回值 执行方式 说明
apply() 无 先同步写内存,再异步写文件 无返回值,不阻塞当前线程,推荐使用
commit() boolean 同步写内存 + 同步写文件 返回是否写入成功,会阻塞调用线程

两者的最终写入结果一致,日常使用 apply() 即可。深入理解两点:

  1. apply 的异步性带来的风险:apply() 把待写数据放入队列后立即返回,如果进程在写入完成前被杀(如用户立刻退出应用),极端情况下这批修改可能丢失;对可靠性要求极高的少量数据可以用 commit() 确认落盘。
  2. 内存常驻:SharedPreferences 一旦获取,整个 xml 文件内容会加载到内存 Map 中并常驻,且同一个文件全应用共享一份实例。不要用它存大量数据(如把整个列表 JSON 塞进去),否则启动加载慢、内存压力大。

# 1.6 监听数据变化

SharedPreferences sp = getSharedPreferences("config", Context.MODE_PRIVATE);
SharedPreferences.OnSharedPreferenceChangeListener listener =
        new SharedPreferences.OnSharedPreferenceChangeListener() {
            @Override
            public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
                Log.i(TAG, "配置发生变化,key = " + key);
            }
        };
// 注册监听
sp.registerOnSharedPreferenceChangeListener(listener);
// 在合适时机(如onDestroy)取消监听,否则会内存泄漏(SP持有listener引用)
sp.unregisterOnSharedPreferenceChangeListener(listener);

# 1.7 完整示例:记住登录状态

public class LoginActivity extends AppCompatActivity {
    private static final String SP_NAME = "config";
    private EditText mEtUsername;
    private EditText mEtPassword;
    private CheckBox mCbRemember;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        mEtUsername = findViewById(R.id.et_username);
        mEtPassword = findViewById(R.id.et_password);
        mCbRemember = findViewById(R.id.cb_remember);

        SharedPreferences sp = getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
        // 读取上次的登录信息(回显)
        mEtUsername.setText(sp.getString("username", ""));
        mCbRemember.setChecked(sp.getBoolean("is_remember", false));

        findViewById(R.id.btn_login).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String username = mEtUsername.getText().toString().trim();
                if (username.isEmpty()) {
                    Toast.makeText(LoginActivity.this, "请输入用户名", Toast.LENGTH_SHORT).show();
                    return;
                }
                if (mCbRemember.isChecked()) {
                    // 勾选了记住:写入配置
                    sp.edit()
                            .putString("username", username)
                            .putBoolean("is_remember", true)
                            .apply();
                } else {
                    // 未勾选:清除记录
                    sp.edit()
                            .remove("username")
                            .putBoolean("is_remember", false)
                            .apply();
                }
                Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

# 二、文件存储

Android 的文件存储分为内部存储和外部存储两类,各自的目录、权限和生命周期都不同,是文件操作最容易混淆的地方。

# 2.1 存储目录全景

目录 路径 权限 随应用卸载删除
内部 files /data/data/<包名>/files/ 无需权限 ✔
内部 cache /data/data/<包名>/cache/ 无需权限 ✔
专属外部 files 外部存储/Android/data/<包名>/files/ 无需权限 ✔
专属外部 cache 外部存储/Android/data/<包名>/cache/ 无需权限 ✔
公共目录(Downloads 等) /storage/emulated/0/Download/ 等 需权限 / 受分区存储限制 ✘

选型经验:应用私有的普通文件和缓存 → 内部存储;较大的私有文件(视频缓存、离线包)→ 应用专属外部目录(内部存储空间通常较小);公开的媒体文件 → MediaStore(见分区存储)。

# 2.2 内部存储读写

内部存储是应用私有目录,无需任何权限,应用卸载时数据一并删除:

// 写文件:openFileOutput打开files目录下的文件输出流,文件不存在会自动创建
String data = "Hello, Android File Storage!";
try (FileOutputStream fos = openFileOutput("data.txt", Context.MODE_PRIVATE)) {
    fos.write(data.getBytes());
} catch (IOException e) {
    e.printStackTrace();
}

// 读文件
try (FileInputStream fis = openFileInput("data.txt");
     BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {
    StringBuilder sb = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        sb.append(line);
    }
    Log.i(TAG, "文件内容:" + sb.toString());
} catch (IOException e) {
    e.printStackTrace();
}

openFileOutput 的 mode 参数:

  • MODE_PRIVATE:默认,覆盖写入
  • MODE_APPEND:追加写入

相关目录 API:

File filesDir = getFilesDir();   // /data/data/<包名>/files/
File cacheDir = getCacheDir();   // /data/data/<包名>/cache/(空间不足时系统可能清理)
File dir = getDir("mydir", Context.MODE_PRIVATE);  // 自定义子目录

# 2.3 外部存储与运行时权限

外部存储指设备内置的共享存储空间(如 /storage/emulated/0/)和 SD 卡。访问公共目录涉及运行时权限(Android 6.0+ 动态申请):

// AndroidManifest.xml中声明权限
// <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
// <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

// 代码中动态申请
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
        != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this,
            new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);
}

// 处理授权结果
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                       @NonNull int[] grantResults) {
    if (requestCode == 100 && grantResults.length > 0
            && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        // 已授权,执行文件操作
    } else {
        Toast.makeText(this, "未授予存储权限,无法保存文件", Toast.LENGTH_SHORT).show();
    }
}

分区存储(Scoped Storage):Android 10(API 29)起,应用不能再自由访问共享存储的任意路径。公共媒体文件(图片、视频、音频)推荐通过 MediaStore API 写入相册类集合;需要用户选择任意文件时用 SAF(ACTION_OPEN_DOCUMENT);应用专属外部目录不受影响。Android 11 起 WRITE_EXTERNAL_STORAGE 对分区存储下的公共目录写入已基本失效。

应用专属外部目录(无需权限,推荐):

// /storage/emulated/0/Android/data/<包名>/files/Download
File externalFilesDir = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
// /storage/emulated/0/Android/data/<包名>/cache/
File externalCacheDir = getExternalCacheDir();

// 使用前建议检查外部存储可用性
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
    // 外部存储可用
}

# 2.4 FileProvider:跨应用安全分享文件

应用私有目录的文件(如 files/ 下的图片)不能直接把文件路径给其他应用(Android 7.0 起,传递 file:// URI 会抛 FileUriExposedException),必须通过 FileProvider 把文件包装成 content:// URI 安全分享:

  1. 在 AndroidManifest.xml 中注册 Provider:
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>
  1. 定义可分享的路径规则:res/xml/file_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- files目录下可直接分享 -->
    <files-path name="share_files" path="share/" />
    <!-- 应用专属外部目录 -->
    <external-files-path name="share_external" path="Download/" />
</paths>
  1. 生成 content URI 并分享:
File file = new File(getFilesDir(), "share/photo.jpg");
Uri uri = FileProvider.getUriForFile(this,
        getPackageName() + ".fileprovider", file);

Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("image/jpeg");
intent.putExtra(Intent.EXTRA_STREAM, uri);
// 临时授权目标应用读取该文件的权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(intent, "分享文件"));

# 三、SQLite 数据库存储

SQLite 是一款轻型的关系型数据库,Android 内置了 SQLite 数据库引擎。通过 SQLiteOpenHelper 管理数据库的创建和版本升级,通过 SQLiteDatabase 完成增删改查。

# 3.1 定义数据库帮助类

/**
* 数据库帮助类,负责数据库的创建和版本管理.
*/
public class MyDatabaseHelper extends SQLiteOpenHelper {
    private static final String TAG = "MyDatabaseHelper";
    // 数据库名称
    public static final String DATABASE_NAME = "my_app.db";
    // 数据库版本,升级表结构时需要递增并处理onUpgrade
    public static final int DATABASE_VERSION = 1;
    // 用户表创建语句
    public static final String CREATE_USER_TABLE =
            "CREATE TABLE IF NOT EXISTS user (" +
                    "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                    "name TEXT NOT NULL, " +
                    "age INTEGER, " +
                    "create_time INTEGER)";

    public MyDatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    // 数据库第一次创建时调用(整个生命周期只调用一次)
    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_USER_TABLE);
        Log.i(TAG, "onCreate: 数据库创建完成");
    }

    // 数据库版本升级时调用(oldVersion < newVersion)
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.i(TAG, "onUpgrade: " + oldVersion + " -> " + newVersion);
        // 这里按版本逐级迁移(见3.6节)
        migrate(db, oldVersion);
    }

    private void migrate(SQLiteDatabase db, int oldVersion) {
        // 示例:版本2新增email列,版本3新增score列
        if (oldVersion < 2) {
            db.execSQL("ALTER TABLE user ADD COLUMN email TEXT");
        }
        if (oldVersion < 3) {
            db.execSQL("ALTER TABLE user ADD COLUMN score REAL DEFAULT 0");
        }
    }
}

版本升级的正确姿势:onUpgrade 收到的是"从旧版本到新版本"的区间,应逐级迁移(if (oldVersion < N) 层层叠加),保证无论用户从哪个旧版本升级,表结构都能走到最新,而不是简单 DROP 重建导致用户数据丢失。

# 3.2 打开数据库

MyDatabaseHelper dbHelper = new MyDatabaseHelper(this);
// 获取可写数据库(第一次调用触发onCreate建库建表)
SQLiteDatabase db = dbHelper.getWritableDatabase();
// 只读数据库(仅查询场景,内存占用更小)
// SQLiteDatabase db = dbHelper.getReadableDatabase();

# 3.3 增删改查(CRUD)

public class DemoActivity extends AppCompatActivity {
    private MyDatabaseHelper mDbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo);
        mDbHelper = new MyDatabaseHelper(this);
    }

    // 新增:使用insert方法
    public void insertUser(View view) {
        SQLiteDatabase db = mDbHelper.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put("name", "JiangMing");
        values.put("age", 26);
        values.put("create_time", System.currentTimeMillis());
        // 返回新增行的id,失败返回-1
        long newId = db.insert("user", null, values);
        Log.i(TAG, "insertUser: newId = " + newId);
    }

    // 删除:使用delete方法
    public void deleteUser(View view) {
        SQLiteDatabase db = mDbHelper.getWritableDatabase();
        // whereClause与whereArgs分开传递,防止SQL注入
        int rows = db.delete("user", "name = ?", new String[]{"JiangMing"});
        Log.i(TAG, "deleteUser: rows = " + rows);
    }

    // 修改:使用update方法
    public void updateUser(View view) {
        SQLiteDatabase db = mDbHelper.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put("age", 27);
        int rows = db.update("user", values, "name = ?", new String[]{"JiangMing"});
        Log.i(TAG, "updateUser: rows = " + rows);
    }

    // 查询:使用query方法
    public void queryUser(View view) {
        SQLiteDatabase db = mDbHelper.getWritableDatabase();
        Cursor cursor = db.query(
                "user",                          // 表名
                null,                            // 查询的列(null表示全部列,实际建议显式指定)
                "age > ?",                       // where条件
                new String[]{"18"},              // 条件参数
                null,                            // groupBy
                null,                            // having
                "create_time DESC",              // orderBy
                null);                           // limit
        try {
            while (cursor.moveToNext()) {
                long id = cursor.getLong(cursor.getColumnIndexOrThrow("id"));
                String name = cursor.getString(cursor.getColumnIndexOrThrow("name"));
                int age = cursor.getInt(cursor.getColumnIndexOrThrow("age"));
                Log.i(TAG, String.format("queryUser: id = %d, name = %s, age = %d", id, name, age));
            }
        } finally {
            // 用完必须关闭Cursor,否则内存泄漏
            cursor.close();
        }
    }

    // 事务:批量操作时使用事务,保证效率和原子性
    public void insertUsersInTransaction(View view) {
        SQLiteDatabase db = mDbHelper.getWritableDatabase();
        db.beginTransaction();
        try {
            for (int i = 0; i < 100; i++) {
                ContentValues values = new ContentValues();
                values.put("name", "user_" + i);
                values.put("age", 20 + i % 10);
                values.put("create_time", System.currentTimeMillis());
                db.insert("user", null, values);
            }
            // 标记事务成功
            db.setTransactionSuccessful();
        } finally {
            // 结束事务(成功则提交,失败则回滚)
            db.endTransaction();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mDbHelper != null) {
            mDbHelper.close();
        }
    }
}

# 3.4 SQL 原生执行

参数化的 API 之外,也可以直接执行 SQL(复杂查询、DDL 时使用):

// 执行写操作SQL(INSERT/UPDATE/DELETE/DDL)
db.execSQL("INSERT INTO user(name, age) VALUES(?, ?)", new Object[]{"JiangMing", 26});

// 执行查询SQL,返回Cursor(与query结果一致)
Cursor cursor = db.rawQuery("SELECT * FROM user WHERE age > ?", new String[]{"18"});

# 3.5 注意事项

  1. 防 SQL 注入:查询条件使用占位符 ? 配合 whereArgs 传递,避免手动拼接字符串
  2. Cursor 用完关闭:Cursor 持有数据库资源,用完必须 close()(推荐 try-with-resources 或 finally)
  3. 批量操作用事务:beginTransaction / setTransactionSuccessful / endTransaction 三段式,效率比逐条执行高一个数量级
  4. 数据库操作放子线程:读写是耗时操作,主线程执行可能引起卡顿甚至 ANR
  5. 单例数据库帮助类:getWritableDatabase 每次返回同一个连接,但建议帮助类全局单例,避免多实例并发写冲突

# 四、ORM 框架对比:从裸 SQLite 到 Room

直接使用 SQLiteDatabase 需要手写 SQL 和解析 Cursor,工程中通常使用 ORM(对象关系映射)框架把"数据库表"映射成"Java 对象"。主流方案对比:

方案 语言/生态 特点 现状
Room Android 官方 Jetpack 注解 + 编译期 SQL 校验、与 Coroutines/Flow/LiveData 集成 官方推荐,新项目首选
GreenDAO 第三方 性能好、API 成熟 维护放缓,新项目不建议
LitePal 第三方 零 SQL、极简 API 适合快速小项目
Realm 第三方(非 SQLite) 自研引擎、对象存储 社区活跃度下降

以 Room 为例感受一下对象化操作(Kotlin,将在 12 Kotlin 基础 与 13 Jetpack 入门 章节展开):

// 定义表
@Entity(tableName = "user")
data class User(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val name: String,
    val age: Int
)

// 定义操作(DAO)
@Dao
interface UserDao {
    @Insert
    suspend fun insert(user: User)

    @Update
    suspend fun update(user: User)

    @Delete
    suspend fun delete(user: User)

    @Query("SELECT * FROM user WHERE age > :age ORDER BY create_time DESC")
    fun loadUsersOlderThan(age: Int): Flow<List<User>>
}

// 使用:面向对象操作,SQL有编译期校验,查询结果可自动刷新UI
userDao.insert(User(name = "JiangMing", age = 26))

Room 的核心优势:

  1. 编译期校验 SQL:@Query 中的 SQL 写错(表名、列名)直接编译报错,而不是运行时崩溃
  2. 对象映射自动化:无需手写 Cursor.getColumnIndex 的样板代码
  3. 响应式集成:查询返回 Flow/LiveData,数据变化时 UI 自动刷新,替代手写"查询→通知"模式
  4. 协程友好:suspend 函数自动切线程,规避主线程操作数据库的问题

# 五、现代衔接:DataStore

传统的 SharedPreferences 有两个长期被诟病的问题:同步 IO 可能阻塞主线程、apply() 异步写盘极端情况丢数据。Google 官方推出了 Jetpack DataStore 作为替代品:

  • 完全基于 Kotlin 协程和 Flow 实现,全异步,不会阻塞主线程
  • 数据事务性更新,保证一致性
  • 两种形态:
    • Preferences DataStore:键值对存储,对应 SharedPreferences 的场景
    • Proto DataStore:基于 Protocol Buffers 定义数据结构,类型安全
// Preferences DataStore 简单示例(Kotlin,后续章节详细展开)
val Context.dataStore by preferencesDataStore(name = "settings")

// 写入
context.dataStore.edit { settings ->
    settings[stringPreferencesKey("username")] = "JiangMing"
}

// 读取(Flow,异步流式,数据变化自动通知)
val usernameFlow: Flow<String> = context.dataStore.data
    .map { settings -> settings[stringPreferencesKey("username")] ?: "" }

官方明确推荐:新项目使用 DataStore 替代 SharedPreferences,存量代码可在重构时逐步迁移(Room 官方也提供了 SP → DataStore 的迁移工具 SharedPreferencesMigration)。

# 结束

本节系统介绍了 Android 本地数据存储:SharedPreferences 键值存储(apply/commit 区别、内存常驻特性、监听机制与完整示例)、文件存储(内部/外部目录全景、运行时权限与分区存储、FileProvider 跨应用分享)、SQLite 数据库(SQLiteOpenHelper、版本迁移实战、CRUD 与事务、ORM 框架对比),并衔接了现代替代方案 DataStore 与 Room。选择方案的经验法则(对应开头的选型图):少量配置用 SP/DataStore,文件用内部或应用专属外部目录,结构化数据用 Room。更多内容请参考官方文档:保存数据 (opens new window)、Save data on disk (opens new window) 与 Room (opens new window)。