引言
在现代应用程序开发中,数据的存储与管理是不可或缺的一部分。持久化(Persistence)指的是将程序运行时的数据保存到某种存储介质中,以便在程序关闭、重启甚至设备断电后仍能保留这些数据。
持久化方式
安卓有以下很多种持久化方式。但持久化终归就是文件存储或者数据库存储。
内部文件存储
最基本的存储方式就是文件存储了,不过在Android的文件存储中我们可以不填写存储路径。只需指定文件名即可,Android 有自己的默认存储位置。 写入文件
button.setOnClickListener(v -> {
try (
FileOutputStream stream = openFileOutput("example.txt", MODE_PRIVATE);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream))
) {
writer.write("Hello, World!");
} catch (IOException e) {
throw new RuntimeException(e);
}
});
文件默认存储在/data/data/packageName/files/
下,属于内部目录。
同理,可以使用openFileInput
读取文件。
读取文件
findViewById(R.id.btn_get).setOnClickListener(v -> {
try (
FileInputStream stream = openFileInput("example.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(stream))
){
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
EditText text = findViewById(R.id.edit_text);
text.setText(content);
} catch (IOException e) {
e.printStackTrace();
}
});
文件存储是最原始的一种存储方式,操作灵活。但缺点是难以存储具备复杂结构的数据。
安卓内部目录与外部目录有关信息可看附录
SharedPreferences 存储
SharedPreferences
使用键值对的方式来存储数据,同时SharedPreferences
也会保存值的数据类型,存储的数据类型是整型,那么读取出来的数据也是整型的;如果存储的数据是一个字符串,那 么读取出来的数据仍然是字符串。该存储方式保存了数据类型信息。
button.setOnClickListener(v -> {
// 第一个参数为filename,第二个为操作模式,当前为默认模式
SharedPreferences.Editor edit = getSharedPreferences("app_data", Context.MODE_PRIVATE).edit();
edit.putInt("example_key", 42);
edit.putString("example_string", "Hello, World!");
edit.apply();
});
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="example_string">Hello, World!</string>
<int name="example_key" value="42" />
</map>
可以看到,数据在该存储方式下,以XML
的形式进行存储。存储在/data/data/packageName/shared_prefs
读取数据
findViewById(R.id.btn_get).setOnClickListener(v -> {
SharedPreferences data = getSharedPreferences("data", Context.MODE_PRIVATE);
String dataString = data.getString("example_string", "Default Value");
Toast.makeText(this, dataString, Toast.LENGTH_SHORT).show();
});
读取与写入操作类似,不过读取操作不需要.edit()
。默认即可进行读取。可以看出,操作SharedPreferences
就像是操作HashMap
外部文件存储
与内部文件存储相对应,都是用于存储普通文件,但是存储位置不同。
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
}
File publicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
if (!publicDir.exists()) {
publicDir.mkdirs();
}
File file = new File(publicDir, "my_image.jpg");
try {
file.createNewFile();
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);
FileOutputStream stream = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
stream.close();
} catch (IOException e) {
e.printStackTrace();
}
SQLite 数据库
SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务,所以只要你以前使用过其他的关系型数据库,就可以很快地上手SQLite。而SQLite又比一般的数据库要简单得多,它甚至不用设置用户名和密码就可以使用。SQLite 数据库广泛适用于嵌入式设备,Android 作为一种特殊的嵌入式设备,内部同样使用了该数据库。
熟悉MySQL的都知道,对数据库的操作往往比操作普通文件复杂。Android为了让我们能够更加方便地管理数据库,专门提供了一个SQLiteOpenHelper
帮助类,借助这个类可以非常简单地对数据库进行创建和升级。
SQLiteOpenHelper
是一个抽象类,所以如果我们想要使用它,就需要创建一个自己的帮助类去继承它。SQLiteOpenHelper
中有两个抽象方法:onCreate()
和 onUpgrade()
。我们必须在自己的类里重写这两个方法,然后分别在这两个方法中实现创建和升级数据库的逻辑。
随后,还有两个方法,getReadableDatabase()
和 getWritableDatabase()
。这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则要创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。
不同的是,当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase()
方法返回的对象将以只读的方式打开数据库,而getWritableDatabase()
方法则将出现异常。
SQLiteOpenHelper
中有两个构造方法可供重写,一般使用参数少一点的那个构造方法即可。 这个构造方法中接收 4个参数:第一个参数是Context
,这个没什么好说的;第二个参数是数据库名,创建数据库时使用的就是这里指定的名称;第三个参数允许在查询数据的时候返回一个自定义的 Cursor,一般传入null即可;第四个参数表示当前数据库的版本号,可用于对数据库进行升级操作。
构建出SQLiteOpenHelper
的实例之 后,再调用它的getReadableDatabase()
或getWritableDatabase()
方法就能够创建数据库了,数据库文件会存放在/data/data/packageName/databases/
目录下。此时,重写的onCreate()
方法也会得到执行,所以通常会在这里处理一些创建表的逻辑
示例
public class MyDatabaseHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "my_db.db";
private static final int DATABASE_VERSION = 1;
private static final String TABLE_NAME = "users";
private static final String COLUMN_ID = "_id";
private static final String COLUMN_NAME = "name";
public MyDatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
// 当不存在该数据库时,会创建该数据库,并执行该方法
@Override
public void onCreate(SQLiteDatabase db) {
String createTable = "CREATE TABLE " + TABLE_NAME + " (" +
COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
COLUMN_NAME + " TEXT)";
db.execSQL(createTable);
}
// 根据 SQLiteOpenHelper 构造时传入的版本参数来选择性执行
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion <= 1) {
db.execSQL(createCategory);
}
if (oldVersion <= 2) {
db.execSQL("alter table Book add column category_id integer");
}
}
}
插入和查询数据
MyDatabaseHelper dbHelper = new MyDatabaseHelper(this);
SQLiteDatabase db = dbHelper.getWritableDatabase();
// 插入数据
ContentValues values = new ContentValues();
values.put(MyDatabaseHelper.COLUMN_NAME, "张三");
db.insert(MyDatabaseHelper.TABLE_NAME, null, values);
// 查询数据
Cursor cursor = db.query(
MyDatabaseHelper.TABLE_NAME,
new String[]{MyDatabaseHelper.COLUMN_ID, MyDatabaseHelper.COLUMN_NAME},
null, null, null, null, null
);
while (cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndexOrThrow(MyDatabaseHelper.COLUMN_NAME));
Log.d("DB", "Name: " + name);
}
cursor.close();
insert(String table, String nullColumnHack, ContentValues values)
- table – 要插入到的表
- nullColumnHack – 可以为 Null,如果提供的 values 为空,此时插入将失败。 如果未设置为 null,则该 nullColumnHack 参数要提供可为 null 的列名,以便在values为空时显式插入 NULL。
- values– 此映射包含行的初始列值。键应该是列名,值应该是列值
query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy)
更新和删除也是类似操作,android提供的api,本质上是sql语句的api表示。当我们要写复杂SQL时,比如多表联合,android 也支持直接db.execSQL(sql);
Room 持久化库
Room 是 Android 基于 SQLite出的框架,属于Jetpack内的内容,该部分将移入Jetpack部分。
附录
内部存储路径(Internal Storage)
内部存储是 应用私有 的文件系统路径。应用卸载时,该路径下的所有数据会被自动删除。通常不需要任何权限即可访问。
方法 | 路径示例 |
---|---|
getFilesDir() | /data/data/<package_name>/files/ |
getCacheDir() | /data/data/<package_name>/cache/ |
这些路径是每个应用独有的,其他应用无法访问(除非设备 root)。 容量较小,适合存储小文件,如配置文件、缓存等。随应用卸载清除,当用户卸载应用时,这些目录下的内容会被自动删除。
外部存储路径(External Storage)
外部存储指的是 共享存储空间,通常是设备内置的“公共”存储区域,也可以是 SD 卡。存储在这里的文件可以被其他应用或用户访问。
方法 | 路径示例 |
---|---|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) | /storage/emulated/0/pictures/ |
Environment.getExternalStorageDirectory() | /storage/emulated/0/ |
getExternalFilesDir(String type) | /storage/emulated/0/Android/data/<package_name>/files/ |
Context.getExternalCacheDir() | /storage/emulated/0/Android/data/<package_name>/cache/ |
MediaStore.Downloads | /storage/emulated/0/Download/ |
共享性高,文件可被其他应用访问。需要权限(Android 9 及以下):
<uses-permission android:name="READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="WRITE_EXTERNAL_STORAGE"/>
Android 10 及以上,不再需要这些权限,但访问公共目录仍需 MANAGE_EXTERNAL_STORAGE(特殊权限)。
容量大,适合存储照片、视频、下载文件等大文件。文件可通过文件管理器查看。不随应用卸载删除,除非是 getExternalFilesDir()
,否则文件不会随应用卸载而删除。
内部存储 vs 外部存储
比较项 | 内部存储 | 外部存储 |
---|---|---|
是否私有 | 是,仅本应用可访问 | 否,其他应用或用户可访问 |
是否需要权限 | 否 | 是(Android 9 及以下),Android 10+ 使用 Scoped Storage 不再需要 |
是否需要处理权限请求 | 否 | 是(尤其是 Android 6.0+) |
适合存储类型 | 小型敏感数据、缓存、配置文件 | 大文件、媒体文件、用户可见数据 |
是否受用户卸载影响 | 是(包括 getCacheDir() 和 getFilesDir()) | 部分路径不受影响(如 Environment.getExternalStoragePublicDirectory()) |
是否容易被用户看到 | 否 | 是,通常位于公共目录中 |
API 支持稳定性 | 高,长期稳定 | 在 Android 10+ ,建议使用 MediaStore |
选择建议
使用场景 | 推荐方式 |
---|---|
存储敏感信息(如 token、密码) | 内部存储 |
缓存临时数据 | 内部缓存目录(getCacheDir()) |
存储用户文档、图片、视频 | 外部存储(建议使用 MediaStore 或 Intent 打开 SAF) |
保存应用专属文件(不随卸载删除) | 不推荐,应使用外部存储 |
与其他应用共享文件 | 外部存储 + FileProvider 或 ContentProvider |