09. 数据存储
一个应用除了展示界面和处理交互,往往还需要持久化一些数据:用户的偏好设置、缓存的图片文本、离线的业务数据等。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() 即可。深入理解两点:
- apply 的异步性带来的风险:
apply()把待写数据放入队列后立即返回,如果进程在写入完成前被杀(如用户立刻退出应用),极端情况下这批修改可能丢失;对可靠性要求极高的少量数据可以用commit()确认落盘。 - 内存常驻:
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)起,应用不能再自由访问共享存储的任意路径。公共媒体文件(图片、视频、音频)推荐通过
MediaStoreAPI 写入相册类集合;需要用户选择任意文件时用 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 安全分享:
- 在 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>
- 定义可分享的路径规则: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>
- 生成 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 注意事项
- 防 SQL 注入:查询条件使用占位符
?配合 whereArgs 传递,避免手动拼接字符串 - Cursor 用完关闭:
Cursor持有数据库资源,用完必须close()(推荐 try-with-resources 或 finally) - 批量操作用事务:
beginTransaction/setTransactionSuccessful/endTransaction三段式,效率比逐条执行高一个数量级 - 数据库操作放子线程:读写是耗时操作,主线程执行可能引起卡顿甚至 ANR
- 单例数据库帮助类:
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 的核心优势:
- 编译期校验 SQL:
@Query中的 SQL 写错(表名、列名)直接编译报错,而不是运行时崩溃 - 对象映射自动化:无需手写
Cursor.getColumnIndex的样板代码 - 响应式集成:查询返回
Flow/LiveData,数据变化时 UI 自动刷新,替代手写"查询→通知"模式 - 协程友好:
suspend函数自动切线程,规避主线程操作数据库的问题
# 五、现代衔接:DataStore
传统的 SharedPreferences 有两个长期被诟病的问题:同步 IO 可能阻塞主线程、apply() 异步写盘极端情况丢数据。Google 官方推出了 Jetpack DataStore 作为替代品:
- 完全基于 Kotlin 协程和 Flow 实现,全异步,不会阻塞主线程
- 数据事务性更新,保证一致性
- 两种形态:
- Preferences DataStore:键值对存储,对应
SharedPreferences的场景 - Proto DataStore:基于 Protocol Buffers 定义数据结构,类型安全
- Preferences DataStore:键值对存储,对应
// 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)。