<Android>3 数据存储
本文最后更新于:2024年10月26日 早上
3 数据存储
Android 提供了 5 种方式来让用户保存持久化应用程序数据:
- 共享参数 SharedPreferences
- 数据库 SQLite
- 文件存储
- 内容提供者 ContentProvider
- 网络存储数据
3.1 共享参数 SharedPreferences
SharedPreferences 是一个轻量级储存工具,其存储结构是 k-v 键值对模式,可用来保存一些基础数据类型。
其存储介质是 XML 文件,保存路径是:/data/data/包名/shared_prefs
下面是一个 XML 实例
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="str">这是一个字符串</string>
<int name="int">10</string>
<boolean name="bool">true</string>
<float name="float">35.5</string>
</map>
要操作上述的共享参数 XML 文件,可通过 Java 进行如下操作:
SharedPreferences sp = getSharedPreferences("config", Context.MODE_PRIVATE);
/* 下面进行文件写入事务 */
SharedPreferences.Editor edit = sp.edit();
edit.putString("str", "这是一个字符串");
edit.putInt("int", 10);
edit.putBoolean("bool", true);
edit.putFloat("float", 35.5f);
edit.commit(); // 提交事务
/* 下面读取之前写入的数据 */
String str = sp.getString("str", "");
int anInt = sp.getInt("int", 0);
boolean bool = sp.getBoolean("bool", false);
float aFloat = sp.getFloat("float", 0.0f);
上面出现的一些方法如下
SharedPreferences getSharedPreferences(String name, int mode) |
获取指定共享参数实例。 在 Context 类中有如下模式: MODE_PRIVATE:(默认)仅caller uid的进程可访问 MODE_WORLD_READABLE:所有人可写 MODE_WORLD_READABLE:所有人可读 MODE_MULTI_PROCESS:始终重新加载数据 |
class SharedPreferences | |
---|---|
String getString(String var1, String var2) Set int getInt(String var1, int var2) long getLong(String var1, long var2) float getFloat(String var1, float var2) boolean getBoolean(String var1, boolean var2) |
获取数据,其名称为 var1,默认值为 var2 |
Editor edit() | 开启一个事务 |
class SharedPreferences.Edit | |
Editor putString(String var1, String var2) Editor putInt(String var1, int var2) Editor putLong(String var1, long var2) Editor putFloat(String var1, float var2) Editor putBoolean(String var1, boolean var2) |
写入数据,其名称为 var1,值为 var2 |
boolean commit() |
提交一个事务 将数据同步写入,并将阻塞当前线程直到完成 若写入失败则返回 false |
void apply() |
提交一个事务 将数据异步写入,写入过程在后台进行 |
共享参数的使用场合:
- 简单且孤立的数据。复杂且相关的数据应保存在数据库中
- 文本形式数据。二进制数据应保存在文件中
- 需要持久化存储的数据。
实际开发中,App 的个性化配置信息、用户使用 App 行为信息、临时保存的片段信息等,都可以使用共享参数保存
3.2 数据库 SQLite
SQLite 是 Android 内置的一个小型、关系型、属于文本型的数据库,是一种轻量级的嵌入式关系型数据库管理系统。
SQLite 不支持布尔类型,将以整形的 1(true)或 0(false)代替。其支持的数据类型有:
- 整形 INTEGER
- 长整形 LONG
- 字符串 VARCHAR
- 浮点数 FLOAT
3.2.1 SQLite 语法
数据定义语言 DDL
-
创建表格
CREATE TABLE IF NOT EXISTS table_name ( _id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR NOT NULL, height LONG NOT NULL, gender INTEGER NOT NULL);
SQL 语句不分大小写,除非是被单引号包裹的字符串值。
-
删除表格
DROP TABLE IF EXISTS table_name;
-
修改标结构
ALTER TABLE table_name ADD COLUMN new_column VARCHAR;
SQLite 只支持增加字段,不支持修改、删除字段。
SQLite 的 ALTER 语句每次只能添加单一字段。添加多列时必须分多次添加。
数据操作语言 DML
-
添加记录
INSERT INTO table_name (name,height,weight,gender) VALUES ('成步堂龙一',176,1);
-
删除记录
DELETE FROM table_name WHERE name='绫里千寻';
-
修改记录
UPDATE table_name SET height=168 WHERE name='绫里真宵'
-
查询记录
SELECT * FROM table_name WHERE name='裁判长' ORDER BY height DESC;
3.2.2 数据库管理器 SQLiteDatabase
SQLiteDatabase 是 SQLite 的数据库管理类,其提供了若干操作数据表的 API。
通过下列操作,即可创建/打开、删除数据库
String dir = getFilesDir() + "/test.db";
/* 打开、创建数据库 */
SQLiteDatabase db = openOrCreateDatabase(dir, Context.MODE_PRIVATE, null);
/* 删除数据库 */
deleteDatabase(dir);
上例中用到的方法
static SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory) |
打开或创建指定目录的数据库 |
static boolean deleteDatabase(String name) | 删除指定目录的数据库 |
通过以下方法进行数据库的增删改查
class SQLiteDatabase | |
---|---|
void execSQL(String sql) void execSQL(String sql, Object[] bindArgs) |
执行 SQL 语句 |
long insert(String table, String nch, ContentValues values) |
向表中添加指定数据 若 values 非空则 nch 可以是 null 返回值为行号,若失败返回 -1 |
int delete(String table, String whereClause, String[] whereArgs) |
删除表中的指定数据 whereClause 为条件语句,以 ? 为占位符 whereArgs 的值用于依次取代 whereClause 中的占位符 返回值为被删除的行数 |
int update(String table, ContentValues values, String whereClause, String[] whereArgs) |
更新表中的指定数据 返回值为被影响的行数 |
Cursor query(bool distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) |
查询表中的指定数据。参数如下 distinct:是否去重 columns:查找的列名。null 则返回全部列 selection:where 条件。null 则无条件 groupBy:groupBy 条件 having:having 条件 orderBy:orderBy 条件 limit:limit 条件 |
void beginTransaction() | 开启事务 |
void setTransactionSuccessful() | 设置事务成功标记 |
void endTransaction() |
提交事务 之后根据是否设置成功标记,而回滚或执行事务 |
-
ContentValues 类
以键值对形式保存一系列数据。用于表示表中的一条记录
ContentValues() 构造函数 void put(String key, Byte value)
void put(String key, Short value)
void put(String key, Integer value)
void put(String key, Long value)
void put(String key, Float value)
void put(String key, Double value)
void put(String key, Boolean value)
void put(String key, byte[] value)
void putNull(String key)向其中添加一组数据 -
Cursor 类
Cursor 是一个指向返回结果的指针,其提供了一种对数据库返回结果集进行随机读写的途径
boolean moveToFirst()
boolean moveToLast()
boolean moveToNext()
boolean moveToPrevious()移动到第一行/最后一行/下一行/上一行
不能移动时返回 falseint getCount()
int getPosition()返回总行数/当前行数 int getColumnIndex(String var1)
String getColumnName(int var1)根据名称/索引,返回指定列的索引/名称 String getString(int var1)
short getShort(int var1)
int getInt(int var1)
long getLong(int var1)
float getFloat(int var1)
double getDouble(int var1)获取指定列的数据
3.2.3 数据库管理辅助类 SQLiteOpenHelper
SQLiteOpenHelper 是 SQLiteDatabase 的辅助类,它可以简化打开数据库链接的过程,增加可维护性。
SQLiteOpenHelper 是一个抽象类,使用时必须实现下列抽象方法。
abstract void onCreate(SQLiteDatabase var1) | 创建时调用,应涵盖初始化数据库的操作 |
abstract void onUpgrade(SQLiteDatabase var1, int var2, int var3) | 版本更新时调用,应涵盖更新数据库的操作 |
此外,还有一些重要方法
SQLiteDatabase getReadableDatabase() SQLiteDatabase getWritableDatabase() |
获取一个可读/可写的 SQLiteDatabase 实例 |
void close() | 关闭连接 |
下面是一个例子,其使用 SQLiteOpenHelper 来对数据库的部分操作进行了封装
class MyDBHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "test.db";
private static final String TABLE_NAME = "test_db";
private static final int DB_VERSION = 1;
private static MyDBHelper MY_DB_HELPER = null;
private SQLiteDatabase mRDB = null;
private SQLiteDatabase mWDB = null;
private MyDBHelper(@Nullable Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
public static MyDBHelper getInstance(Context context) {
if (MY_DB_HELPER == null) MY_DB_HELPER = new MyDBHelper(context);
return MY_DB_HELPER;
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
String sql = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME +
" (_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
" name VARCHAR NOT NULL," +
" height LONG NOT NULL," +
" gender INTEGER NOT NULL);";
sqLiteDatabase.execSQL(sql);
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
}
public SQLiteDatabase openReadLink() {
if (mRDB == null || !mRDB.isOpen()) mRDB = getReadableDatabase();
return mRDB;
}
public SQLiteDatabase openWriteLink() {
if (mWDB == null || !mWDB.isOpen()) mWDB = getWritableDatabase();
return mWDB;
}
public void closeLink() {
if (mRDB != null && mRDB.isOpen()) mRDB.close();
if (mWDB != null && mWDB.isOpen()) mWDB.close();
mRDB = null;
mWDB = null;
}
public long insert(String name, long height, int gender) {
ContentValues contentValues = new ContentValues();
contentValues.put("name", name);
contentValues.put("height", height);
contentValues.put("gender", gender);
return mWDB.insert(TABLE_NAME, null, contentValues);
}
public long deleteByName(String name) {
return mWDB.delete(TABLE_NAME, "name=?", new String[]{name});
}
public List<String> queryAll() {
List<String> res = new ArrayList<>();
Cursor query = mRDB.query(TABLE_NAME, null, null, null, null, null, null);
while (query.moveToNext()) {
int id = query.getInt(0);
String name = query.getString(1);
long height = query.getLong(2);
int gender = query.getInt(3);
res.add(String.format("ID:%s 姓名:%s 身高:%s 性别:%s", id, name, height, gender == 1 ? '男' : '女'));
}
query.close();
return res;
}
}
3.2.4 Room 框架
Room 是谷歌推出的数据库处理框架。其通过注解技术简化了数据库操作,使得使用 SQLite 数据库变得更加方便。
试用前,必先在 build.gradle 如下位置添加如下配置
...
dependencies {
...
implementation 'androidx.room:room-runtime:2.6.1'
annotationProcessor 'androidx.room:room-compiler:2.6.1'
}
在 AndroidStudio 中修改了 build.gradle 后,记得点击上方的同步按钮才能生效
Room 框架提供了以下注释:
类注释 | |
---|---|
@Entity | 代表一个实体,对应数据库的一张表 |
@Dao | 代表一个持久化类,其中蕴含数据访问操作 |
@Database(entities = {Xxx.class},version = 0) |
代表一个派生自 RoomDatabase 数据库抽象类 entities:指定了数据库所含的表 该抽象类必须包含一个返回自定义的 Dao 的抽象方法 |
属性注释 | |
@PrimaryKey(autoGenerate = true) | 代表主键,autoGenerate 设置自增长 |
@ColumnInfo(name = "xxx", typeAffinity = ColumnInfo.INTEGER) | 设置一列的某些信息 |
方法注释 | |
@Insert | 该方法对应一条插入语句 |
@Delete | 该方法对应一条删除语句 |
@Update | 该方法对应一条更新语句 |
@Query("SELECT * FROM xxx") | 该方法对应一条查询语句 |
下面是一个简单的示例:
-
People.java
import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; @Entity public class People { @PrimaryKey(autoGenerate = true) private int id; @ColumnInfo(name = "age", typeAffinity = ColumnInfo.INTEGER) private int age; }
-
PeopleDao.java
import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.Query; import androidx.room.Update; @Dao public class PeopleDao { @Insert public void insert() { ... } @Delete public void delete() { ... } @Update public int update() { ... } @Query("SELECT * FROM People") public List<People> queryAll() { ... } }
-
PeopleDb.java
import androidx.room.Database; import androidx.room.RoomDatabase; @Database(entities = {People.class},version = 0, exportSchema = false) public abstract class PeopleDb extends RoomDatabase { public abstract PeopleDao get(); }
使用时,通过 Room 类的如下方法构建数据库实例
3.3 存储卡文件操作
Android 访问存储卡(即外部存储)通常涉及三种主要的目录:应用程序私有空间、公共空间、缓存空间
graph LR
subgraph s[外部公共存储空间]
subgraph ssa[外部私有存储空间]
a1[PICTURE]
a2[MUSIC]
a3[……]
end
subgraph ssb[外部私有存储空间]
b1[PICTURE]
b2[MUSIC]
b3[……]
end
end
style s fill:#ddddddee, stroke:#000000
style ssa fill:#ffffff, stroke:#000000
style ssb fill:#ffffff, stroke:#000000
通过下列方法,即可获取指定的存储卡文件的路径
File getExternalFilesDir(String type) |
获取指定的外部私有存储空间路径 type 是 Environment 类中规定的常量 |
File getExternalCacheDir() | 获取外部缓存存储空间路径 |
File getFilesDir() | 获取内部存储空间路径 |
class Environment | |
---|---|
static File getExternalStorageDirectory() static File getExternalStorageDirectory(String type) |
获取外部公共存储空间路径 type 是 Environment 类中规定的常量 |
获取到路径后,只需使用 Java 的 IO 工具,就能对存储卡数据进行读写。
3.3.1 位图 Bitmap
Android 提供了 Bitmap 类来处理位图文件。它可以获取图像文件信息,进行图像颜色变换、剪切、旋转、缩放等操作,并可以指定格式保存图像文件。
通过 位图辅助类 BitmapFactory 以获取位图实例。
class BitmapFactory | |
---|---|
static Bitmap decodeResource(Resources res, int id) static Bitmap decodeResource(Resources res, int id, Options opts) |
从资源文件中获取图片信息 opts:BitmapFactory.Options 的一个实例 |
static Bitmap decodeFile(String pathName) static Bitmap decodeFile(String pathName, Options opts) |
从指定路径文件中获取图片信息 |
static Bitmap decodeStream(InputStream is) | 从指定输入流中获取图片信息 |
static Bitmap decodeByteArray(byte[] data, int offset, int length) static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts) |
从二进制数据中获取图片信息 |
class BitmapFactory.Options | |
Options() | 构造函数 |
boolean inJustDecodeBounds |
只扫描轮廓以获取图片宽高 返回的 Bitmap 实例将为 null |
int inSampleSize |
采样率,默认为 1 设为 x 时,每 x 个像素中保留 1 个像素 最佳采样率可以通过原图尺寸与 ImageView 尺寸计算得到 |
Bitmap.Config inPreferredConfig |
色深值 该属性为 Bitmap.Config 枚举类的一个实例 |
int outHeight int outWidth String outMimeType |
获取图像的高度/宽度/MINE 类型 |
Bitmap.Config inPreferredConfig | 设置色彩配置。该实例属于 Bitmap.Config 枚举类 |
Bitmap inBitmap | 复用一个已存在的 Bitmap 实例,以节约内存开销 |
boolean inScaled | 是否可缩放,默认为 true |
int inDensity | 像素密度 |
enum Bitmap.Config | |
ALPHA_8 | 每个像素占 1 字节,且仅储存透明度 |
RGB_565 | 每个像素占 2 字节,RGB(5/6/5bit) |
ARGB_8888 | 默认项。每个像素占 4 字节,ARGB(8/8/8/8bit) |
获取到 Bitmap 实例后,通过 ImageView 的 setImageBitmap(birmap)
方法即可显示图片。
图片文件通常非常占用内存,因此,必须通过合适的压缩算法,以兼顾显示效果与内存优化。
class Bitmap | |
---|---|
static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height, Matrix m, boolean filter) |
获取 Bitmap 实例。该方法有许多不同的重载 在本重载中: x/y:裁剪起始坐标 width/height:新图的宽高 m:一个矩阵,指出了要对新图进行的一系列变换 |
class Bitmap | |
static Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight, boolean filter) | 获取一个缩放的 Bitmap 实例 |
boolean compress(CompressFormat format, int quality, OutputStream stream) |
将图片以指定压缩方式转为输出流 format:Bitmap.CompressFormat 枚举类的一个实例 quality:压缩质量,取值从 0-100 |
Bitmap copy(Config config, boolean isMutable) | 以指定色彩模式创建一个副本 |
Bitmap extractAlpha() | 创建一个仅有透明度的副本,其色彩配置为 ALPHA_8 |
void recycle() boolean isRecycled() |
回收该 Bitmap 占用的内存空间 |
int getByteCount() | 获取该图像的像素数量 |
enum Bitmap.CompressFormat | |
JPEG |
一种有损压缩算法,quality 越小体积越小 不支持透明度,并将以黑色背景填充不透明度。 |
PNG | 一种无损压缩算法,会忽略 quality 压缩质量设置,且支持透明度。 |
WEBP_LOSSY | 以 WEBP 算法进行有损压缩,quality 越小体积越小 |
WEBP_LOSSLESS | 以 WEBP 算法进行无损压缩,quality 越大体积越小 |
3.4 内容提供者 ContentProvider
ContentProvider 是 Android 四大组件之一,其可实现不同应用程序之间的数据共享。其也能为应用存取内部数据提供接口。
需事先在 AndroidManifest.xml 内注册以使用 ContentProvider
<application...>
<activity.../>
<provider
android:authorities="com.xxx.xxx.Xxxxxx"
android:name=".Xxxxxx"
android:enabled="true"
android:exported="true"/>
</application>
之后,创建一个 ContentProvider 类的派生类,并实现其抽象方法
boolean onCreate() | 初始化时被调用 返回 true 则加载成功,否则失败 |
Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1) |
查询数据 |
String getType(Uri uri) | 11 |
Uri insert(Uri uri, ContentValues contentValues) | Uri:需要操作哪张表 ContentValues:插入的值 |
int delete(Uri uri, String s, String[] strings) | 删除数据 |
int update(Uri uri, ContentValues contentValues, String s, String[] strings) |
更新数据 |
3.4.1 通用资源标识符 URI
URI 通用资源标识符(Universal Resource Identifier,也称统一资源定位符),是描述物理或虚拟资源的唯一标识符。互联网领域的 URL 就是 URI 的子集。
下面是 URI 的一些示例:
https://search.bilibili.com/all?keyword=M.League
file:///D:/test.txt
URI 的基本结构如下:
<scheme>:<authority><path>?<query>#<fragment>
-
scheme:前缀
是以何种方式访问何种资源的概念。
常见有:https、file 等
-
authority:授权者(可选)
-
path:路径(可选)
通过单个斜杠连接的一个路径
-
query:查询(可选)
GET 请求时的参数部分
-
fragment:片段(可选)
在 ContentProvider 中使用 URI 的语法如下
content://authority/path/id
其中 authority 是 AndroidManifest.xml 规定的内容,用于指出 ContentProvider 的类名;path 为指向表的名称;id 为行号。
3.4.2 ContentResolver
借助 ContentResolver,以跨应用获取其他应用 ContentProvider 提供的内容。
ContentResolver getContentResolver() | 获取 ContentResolver |
class ContentResolver | |
---|---|
Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal) |
通过 URI 进行查询 |
Uri insert(Uri url, ContentValues values) | 插入 |
int delete(Uri url, Bundle extras) | 删除 |
int update(Uri uri, ContentValues values, String where, String[] selectionArgs) | 更新 |
在 Android 11 后添加了安全机制,要访问其他应用 ContentProvider,必须在 AndroidManifest.xml 中提前声明以获取权限
<manifest...>
<queries>
<package android:name="com.xxxx.xxxx"/>
</queries>
<application...>
</manifest>
3.4.3 运行时申请权限
Android 从 6.0 起引入了运行时权限管理机制,在程序运行时动态检查是否拥有权限。若缺少权限,则系统将弹出小窗提示用户开启权限
Android 常用权限主要有两类:普通权限、危险权限。其中,普通权限只需在 AndroidManifest.xml 中添加声明即可,而危险权限必须由用户手动授权。
每个危险权限属于某一权限组,申请权限时使用权限名,而用户一旦同一申请,则改组所有权限都将被授权。
SMS 短信权限 | SEND_SMS | 发送短信 |
RECEIVE_SMS | 接收短信 | |
READ_SMS | 读取短信 | |
RECEIVE_WAP_PUSH | 接收 WAP Push消息 | |
RECEIVE_MMS | 接收彩信 | |
SMS 存储权限 | READ_EXTERNAL_STORAGE | 读取存储卡内容 |
WRITE_EXTERNAL_STORAGE | 向存储卡写入内容 | |
联系人权限 | READ_CONTACTS | 读取联系人 |
WRITE_CONTACTS | 写入联系人 | |
GET_ACCOUNTS | 访问帐户列表 | |
手机权限 | READ_PHONE_STATE | 读取手机状态 |
CALL_PHONE | 拨打电话 | |
READ_CALL_LOG | 读取通话记录 | |
WRITE_CALL_LOG | 写入通话记录 | |
ADD_VOICEMAIL | 添加语音信箱 | |
USE_SIP | 使用 SIP 协议进行网络电话 | |
PROCESS_OUTGOING_CALLS | 处理呼出电话 | |
日历权限 | READ_CALENDAR | 读取日历 |
WRITE_CALENDAR | 写入日历 | |
相机权限 | CAMERA | 访问摄像头 |
位置权限 | ACCESS_FINE_LOCATION | 访问精确位置 |
ACCESS_COARSE_LOCATION | 访问模糊位置 | |
传感器权限 | BODY_SENSORS | 访问传感器 |
麦克风权限 | RECORD_AUDIO | 访问麦克风 |
动态申请权限的步骤如下:
-
检查应用是否拥有指定权限
int i = ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE); if (i == PackageManager.PERMISSION_GRANTED) ... else ...
用到的方法如下
class ContentCompat static int checkSelfPermission(Context context, String permission) 检查是否已有权限 -
(若未授权)请求系统弹窗,请求开启权限
String[] permissions = {Manifest.permission.CAMERA}; int requestCode = 1; requestPermissions(permissions, requestCode);
用到的方法如下
class ActivityCompat static void requestPermissions(Activity activity,
String[] permissions, int requestCode)动态申请权限
permissions:所有申请的权限集合
requestCode:请求码,是唯一值即可 -
判断用户是否选择开启权限
用户选择完毕后,Activity 的回调方法将被调用。重写该方法以处理选择结果
@Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode) { ... } }
3.4.4 内容监听器 ContentObserver
ContentObserver 用于监听 ContentProvider 中数据的变化,当数据发生改变时会收到通知。这样可以实现实时数据同步或更新 UI。
重写其 onChange 方法后,即可对数据变化做出响应。
使用时,在要监听的地方注册 ContentObserver
Handler handler = new Handler();
MyContentObserver observer = new MyContentObserver(handler);
getContentResolver().registerContentObserver(ExampleProvider.CONTENT_URI, true, observer);
使用完毕后,亦须注销以免内存泄漏
getContentResolver().unregisterContentObserver(observer);