简介
在安卓应用开发中,数据存储是实现应用功能的重要部分。合理选择和实现数据存储方式,可以提升应用的性能和用户体验。本文将介绍安卓开发中常用的数据存储方法、适用场景、实现方式以及一些实践示例,帮助你全面掌握安卓数据存储技术。
常见的安卓数据存储方式
SharedPreferences
SharedPreferences是Android的一个轻量级存储工具,采用的存储结构是Key-Value的键值对方式。
共享参数的存储介质是符合XML规范的配置文件。保存路径是:/data/data/应用包名/shared_prefs/文件名.xml
下面是一个共享参数的XML文件示例:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="name">Mr Lee</string>
<int name="age" value="30" />
<boolean name="married" value="true" />
<float name="weight" value="100.0" />
</map>
共享参数主要适用于如下场合:
(1)简单且孤立的数据。若是复杂且相互间有关的数据,则要保存在数据库中。
(2)文本形式的数据。若是二进制数据,则要保存在文件中。
(3)需要持久化存储的数据。在App退出后再次启动时,之前保存的数据仍然有效。
实际开发中,共享参数经常存储的数据有App的个性化配置信息、用户使用App的行为信息、临时需要保存的片段信息等。
下面是使用共享参数的代码例子:
示例:
// 从share.xml中获取共享参数对象
SharedPreferences shared = getSharedPreferences("share", MODE_PRIVATE);
// 下面是写入共享参数的代码例子
SharedPreferences.Editor editor = shared.edit(); // 获得编辑器的对象
editor.putString("name", "Mr Lee"); // 添加一个名叫name的字符串参数
editor.putInt("age", 30); // 添加一个名叫age的整型参数
editor.putBoolean("married", true); // 添加一个名叫married的布尔型参数
editor.putFloat("weight", 100f); // 添加一个名叫weight的浮点数参数
editor.commit(); // 提交编辑器中的修改
// 下面是读取共享参数的代码例子
String name = shared.getString("name", ""); // 从共享参数中获得名叫name的字符串
int age = shared.getInt("age", 0); // 从共享参数中获得名叫age的整型数
boolean married = shared.getBoolean("married", false); // 从共享参数中获得名叫married的布尔数
float weight = shared.getFloat("weight", 0); // 从共享参数中获得名叫weight的浮点数
实现记住密码功能
// 下面是利用共享参数保存密码的代码
// 如果勾选了“记住密码”,则把手机号码和密码都保存到共享参数中
if (bRemember) {
SharedPreferences.Editor editor = mShared.edit(); // 获得编辑器的对象
editor.putString("phone", et_phone.getText().toString()); // 添加名叫phone的手机号码
editor.putString("password", et_password.getText().toString()); // 添加名叫password的密码
editor.commit(); // 提交编辑器中的修改
}
// 下面是利用共享参数读取密码的代码
// 从share.xml中获取共享参数对象
mShared = getSharedPreferences("share_login", MODE_PRIVATE);
// 获取共享参数中保存的手机号码
String phone = mShared.getString("phone", "");
// 获取共享参数中保存的密码
String password = mShared.getString("password", "");
et_phone.setText(phone); // 给手机号码编辑框填写上次保存的手机号
et_password.setText(password); // 给密码编辑框填写上次保存的密码

SQLite数据库
SQLite是安卓内置的轻量级关系型数据库,适用于复杂的数据操作。
Android官方推出了数据仓库DataStore,并将其作为Jetpack库的基础组件。
DataStore提供了两种实现方式,分别是Preferences DataStore 和Proto DataStore,前者采用键值对存储数据,后者采用自定义类型存储数据。
其中Preferences DataStore可以直接替代SharedPreferences。
SQL的基本语法
标准的SQL语句分为三类:数据定义、数据操纵和数据控制,但不同的数据库往往有自己的实现。
SQLite是一种小巧的嵌入式数据库,由于它属于轻型数据库,不涉及复杂的数据控制操作,因此App开发只用到数据定义和数据操纵两类SQL。
SQLite的SQL语法与通用的SQL语法略有不同。
SQLite的数据定义语言
数据定义语言(DDL)描述了怎样变更数据实体的框架结构。
DDL语言主要包括三种操作:创建表格、删除表格、修改表结构。
(1)创建表格
格式为“CREATE TABLE IF NOT EXISTS 表格名称 (以逗号分隔的各字段定义);”
(2)删除表格
格式为“DROP TABLE IF EXISTS 表格名称;”
(3)修改表结构
格式为“ALTER TABLE 表格名称 修改操作;”
SQLite只支持增加字段,不支持修改字段,也不支持删除字段。
SQLite的数据操纵语言
数据操纵语言(DML)它描述了怎样处理数据实体的内部记录。
表格记录的操作类型包括添加、删除、修改、查询四类:
(1)添加记录
格式为“INSERT INTO 表格名称 (以逗号分隔的字段名列表) VALUES (以逗号分隔的字段值列表);”
(2)删除记录
格式为“DELETE FROM 表格名称 WHERE 查询条件;”
(3)修改记录
格式为“UPDATE 表格名称 SET 字段名=字段值 WHERE 查询条件;”
(4)查询记录
格式为“SELECT 以逗号分隔的字段名列表 FROM 表格名称 WHERE 查询条件;”
适用场景:
- 需要进行复杂查询和数据关系管理的应用。
示例:
// 创建数据库
SQLiteDatabase db = openOrCreateDatabase("mydb.db", Context.MODE_PRIVATE, null);
db.execSQL("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER);");
// 插入数据
db.execSQL("INSERT INTO users (name, age) VALUES ('John', 30);");
// 查询数据
Cursor cursor = db.rawQuery("SELECT * FROM users", null);
while (cursor.moveToNext()) {
int id = cursor.getInt(0);
String name = cursor.getString(1);
int age = cursor.getInt(2);
// 处理数据
}
cursor.close();
db.close();
SQLiteDatabase
SQLiteDatabase是SQLite的数据库管理类,它提供了若干操作数据表的API,常用的方法有3类:
- 管理类,用于数据库层面的操作。
openDatabase:打开指定路径的数据库。
isOpen:判断数据库是否已打开。
close:关闭数据库。
getVersion:获取数据库的版本号。
setVersion:设置数据库的版本号。 - 事务类,用于事务层面的操作。
beginTransaction:开始事务。
setTransactionSuccessful:设置事务的成功标志。
endTransaction:结束事务。 - 数据处理类,用于数据表层面的操作。
execSQL:执行拼接好的SQL控制语句。
delete:删除符合条件的记录。
update:更新符合条件的记录。
insert:插入一条记录。
query:执行查询操作,返回结果集的游标。
rawQuery:执行拼接好的SQL查询语句,返回结果集的游标。
SQLiteOpenHelper是Android提供的数据库辅助工具,用于指导开发者进行SQLite的合理使用。
SQLiteOpenHelper的具体使用步骤如下:
(1)新建一个继承自SQLiteOpenHelper的数据库操作类,提示重写onCreate和onUpgrade两个方法。
(2)封装保证数据库安全的必要方法,包括以下三种。
获取单例对象:确保App运行时数据库只被打开一次,避免重复打开引起错误。
打开数据库连接:读连接可调用SQLiteOpenHelper的getReadableDatabase方法获得,写连接可调用getWritableDatabase获得。
关闭数据库连接:数据库操作完了,调用SQLiteDatabase对象的close方法关闭连接。
(3)提供对表记录进行增加、删除、修改、查询的操作方法。
Cursor
调用SQLiteDatabase的query和rawQuery方法时,返回的都是Cursor对象,因此获取查询结果要根据游标的指示一条一条遍历结果集合。
Cursor的常用方法可分为3类:
(1)游标控制类方法,用于指定游标的状态。
close:关闭游标。
isClosed:判断游标是否关闭。
isFirst:判断游标是否在开头。
isLast:判断游标是否在末尾。
(2)游标移动类方法,把游标移动到指定位置。
moveToFirst:移动游标到开头。
moveToLast:移动游标到末尾。
moveToNext:移动游标到下一条记录。
moveToPrevious:移动游标到上一条记录。
move:往后移动游标若干条记录。
moveToPosition:移动游标到指定位置的记录。
(3)获取记录类方法,可获取记录的数量、类型以及取值。
getCount:获取结果记录的数量。
getInt:获取指定字段的整型值。
getLong:获取指定字段的长整型值。
getFloat:获取指定字段的浮点数值。
getString:获取指定字段的字符串值。
getType:获取指定字段的字段类型。
优化记住密码功能
利用共享参数实现记住密码,只能记住一个用户的登录信息,并且手机号码跟密码不存在从属关系,如果换个手机号码登录,前一个用户的登录信息就被覆盖了。
真正的记住密码功能是先输入手机号码,然后根据手机号匹配保存的密码,一个密码对应一个手机号码,从而实现具体手机号码的密码记忆功能。
运用SQLite技术分条存储不同用户的登录信息,并提供根据手机号码查找登录信息的方法,这样可以同时记住多个手机号码的密码。
利用SQLite记住密码有以下三处改造:
(1)声明一个用户数据库的帮助器对象,然后在活动页面的onResume方法中打开数据库连接,在onPasue方法中关闭数据库连接
(2)登录成功时,如果用户勾选了“记住密码”,就使用数据库保存手机号码与密码在内的登录信息。
(3)再次打开登录页面,用户输入手机号完毕后点击密码输入框时,App到数据库中根据手机号查找登录记录,并将记录结果中的密码填入密码框。

Room持久化库
Room是Google提供的SQLite数据库抽象层,简化了数据库操作。
使用数据库帮助器编码的时候,开发者每次都得手工实现以下代码逻辑:
(1)重写数据库帮助器的onCreate方法,添加该表的建表语句;
(2)在插入记录之时,必须将数据实例的属性值逐一赋给该表的各字段;
(3)在查询记录之时,必须遍历结果集游标,把各字段值逐一赋给数据实例;
(4)每次读写操作之前,都要先开启数据库连接;读写操作之后,又要关闭数据库连接;
Room是谷歌公司推出的数据库处理框架,该框架同样基于SQLite,但它通过注解技术极大简化了数据库操作,减少了原来相当一部分编码工作量。
在使用Room之前,要先修改模块的build.gradle文件,往dependencies节点添加下面两行配置,表示导入指定版本的Room库:
implementation 'androidx.room:room-runtime:2.4.2'
annotationProcessor 'androidx.room:room-compiler:2.4.2'
适用场景:
- 需要使用SQLite数据库但希望简化代码的应用。
示例:
// 定义实体
@Entity
public class User {
@PrimaryKey
public int id;
public String name;
public int age;
}
// 定义DAO
@Dao
public interface UserDao {
@Insert
void insert(User user);
@Query("SELECT * FROM User")
List<User> getAllUsers();
}
// 定义数据库
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
// 使用数据库
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "mydatabase").build();
UserDao userDao = db.userDao();
User user = new User();
user.id = 1;
user.name = "John";
user.age = 30;
userDao.insert(user);
List<User> users = userDao.getAllUsers();
以录入书籍信息为例,使用Room框架的编码过程分为下列五步:
(1)编写书籍信息表对应的实体类,该类添加“@Entity”注解。
(2)编写书籍信息表对应的持久化类,该类添加“@Dao”注解。
(3)编写书籍信息表对应的数据库类,该类从RoomDatabase派生而来,并添加“@Database”注解。
(4)在自定义的Application类中声明书籍数据库的唯一实例。
(5)在操作书籍信息表的地方获取数据表的持久化对象。
// 从App实例中获取唯一的书籍持久化对象
BookDao bookDao = MainApplication.getInstance().getBookDB().bookDao();
存储卡
Android把外部存储分成了两块区域,一块是所有应用均可访问的公共空间,另一块是只有应用自己才可访问的私有空间。
Android在SD卡的“Android/data”目录下给每个应用又单独建了一个文件目录,用于给应用保存自己需要处理的临时文件。这个给每个应用单独建立的文件目录,只有当前应用才能够读写文件,其它应用是不允许进行读写的,故而“Android/data”目录算是外部存储上的私有空间。
Android从7.0开始加强了SD卡的权限管理,App使用SD卡的公共控件前既需要事先声明权限,又需要在设置页面开启权限,使用私有空间无需另外设置权限。
获取公共空间的存储路径,调用的是Environment类的getExternalStoragePublicDirectory方法;获取应用私有空间的存储路径,调用的是getExternalFilesDir方法。代码如下:
// 获取系统的公共存储路径
String publicPath = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS).toString();
// 获取当前App的私有存储路径
String privatePath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString();
TextView tv_file_path = findViewById(R.id.tv_file_path);
String desc = "系统的公共存储路径位于" + publicPath +
"\n\n当前App的私有存储路径位于" + privatePath;
tv_file_path.setText(desc);
文本文件的读写
一般借助于FileOutputStream和FileInputStream。
(1)FileOutputStream用于写文件。
(2)FileInputStream用于读文件。

在存储卡上读写图片文件
Android的位图工具是Bitmap,App读写Bitmap可以使用性能更好的BufferedOutputStream和BufferedInputStream。
Android还提供了BitmapFactory工具用于读取各种来源的图片,相关方法如下:
decodeResource:该方法可从资源文件中读取图片信息。
decodeFile:该方法可将指定路径的图片读取到Bitmap对象。
decodeStream:该方法从输入流中读取位图数据。

App涉及到硬件设备的操作,比如拍照、录音、定位、SD卡等等,都要在AndroidManifest.xml中声明相关的权限。
可是Android系统为了防止某些App滥用权限,又允许用户在系统设置里面对App禁用某些权限。但这又带来另一个问题,用户打开App之后,App可能因为权限不足导致无法正常运行,甚至直接崩溃闪退。
有鉴于此,Android从6.0开始引入了运行时权限管理机制,允许App在运行过程中动态检查是否拥有某项权限,一旦发现缺少某种必需的权限,则系统会自动弹出小窗提示用户去开启该权限。
动态申请权限的步骤
1)检查App是否开启了指定权限
调用ContextCompat的checkSelfPermission方法。
(2)请求系统弹窗,以便用户选择是否开启权限
调用ActivityCompat的requestPermissions方法,即可命令系统自动弹出权限申请窗口。
(3)判断用户的权限选择结果
重写活动页面的权限请求回调方法onRequestPermissionsResult,在该方法内部处理用户的权限选择结果。
内部存储(Internal Storage)
内部存储用于保存应用私有的文件,其他应用无法访问。
适用场景:
- 存储敏感数据或应用私有数据。
示例:
// 保存数据到内部存储
String filename = "myfile.txt";
String fileContents = "Hello, World!";
FileOutputStream fos = openFileOutput(filename, Context.MODE_PRIVATE);
fos.write(fileContents.getBytes());
fos.close();
// 从内部存储读取数据
FileInputStream fis = openFileInput(filename);
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader bufferedReader = new BufferedReader(isr);
StringBuilder sb = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
sb.append(line);
}
String fileContents = sb.toString();
外部存储(External Storage)
外部存储用于保存公共文件,如媒体文件、下载文件等。
适用场景:
- 存储需要与其他应用共享的文件。
示例:
// 检查外部存储是否可读写
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
File externalDir = Environment.getExternalStorageDirectory();
File myFile = new File(externalDir, "myfile.txt");
FileOutputStream fos = new FileOutputStream(myFile);
fos.write("Hello, External Storage!".getBytes());
fos.close();
}
文件存储
文件存储用于保存大型数据或需要以文件形式管理的应用数据。
适用场景:
- 需要保存文本、图片、音频等文件的应用。
public class FileUtil {
// 把字符串保存到指定路径的文本文件
public static void saveText(String path, String txt) {
// 根据指定的文件路径构建文件输出流对象
try (FileOutputStream fos = new FileOutputStream(path)) {
fos.write(txt.getBytes()); // 把字符串写入文件输出流
} catch (Exception e) {
e.printStackTrace();
}
}
// 从指定路径的文本文件中读取内容字符串
public static String openText(String path) {
String readStr = "";
// 根据指定的文件路径构建文件输入流对象
try (FileInputStream fis = new FileInputStream(path)) {
byte[] b = new byte[fis.available()];
fis.read(b); // 从文件输入流读取字节数组
readStr = new String(b); // 把字节数组转换为字符串
} catch (Exception e) {
e.printStackTrace();
}
return readStr; // 返回文本文件中的文本字符串
}
// 把位图数据保存到指定路径的图片文件
public static void saveImage(String path, Bitmap bitmap) {
// 根据指定的文件路径构建文件输出流对象
try (FileOutputStream fos = new FileOutputStream(path)) {
// 把位图数据压缩到文件输出流中
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
} catch (Exception e) {
e.printStackTrace();
}
}
// 从指定路径的图片文件中读取位图数据
public static Bitmap openImage(String path) {
Bitmap bitmap = null; // 声明一个位图对象
// 根据指定的文件路径构建文件输入流对象
try (FileInputStream fis = new FileInputStream(path)) {
bitmap = BitmapFactory.decodeStream(fis); // 从文件输入流中解码位图数据
} catch (Exception e) {
e.printStackTrace();
}
return bitmap; // 返回图片文件中的位图数据
}
public static List<File> getFileList(String path, String[] extendArray) {
List<File> displayedContent = new ArrayList<File>();
File[] files = null;
File directory = new File(path);
if (extendArray != null && extendArray.length > 0) {
FilenameFilter fileFilter = getTypeFilter(extendArray);
files = directory.listFiles(fileFilter);
} else {
files = directory.listFiles();
}
if (files != null) {
for (File f : files) {
if (!f.isDirectory() && !f.isHidden()) {
displayedContent.add(f);
}
}
}
// 按照最后修改时间排序
Collections.sort(displayedContent, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
return (o1.lastModified() > o2.lastModified()) ? -1 : 1;
}
});
return displayedContent;
}
public static FilenameFilter getTypeFilter(String[] extendArray) {
final ArrayList<String> fileExtensions = new ArrayList<String>();
for (int i = 0; i < extendArray.length; i++) {
fileExtensions.add(extendArray[i]);
}
FilenameFilter fileNameFilter = new FilenameFilter() {
@Override
public boolean accept(File directory, String fileName) {
boolean matched = false;
File f = new File(String.format("%s/%s",
directory.getAbsolutePath(), fileName));
matched = f.isDirectory();
if (!matched) {
for (String s : fileExtensions) {
s = String.format(".{0,}\\%s$", s);
s = s.toUpperCase(Locale.getDefault());
fileName = fileName.toUpperCase(Locale.getDefault());
matched = fileName.matches(s);
if (matched) {
break;
}
}
}
return matched;
}
};
return fileNameFilter;
}
示例:
// 保存文件到内部存储
File file = new File(getFilesDir(), "myfile.txt");
FileOutputStream fos = new FileOutputStream(file);
fos.write("Hello, File Storage!".getBytes());
fos.close();
// 从内部存储读取文件
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader reader = new BufferedReader(isr);
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
String fileContents = sb.toString();
云存储
云存储用于在云端保存和同步数据,适用于需要跨设备同步的应用。
适用场景:
- 需要跨设备同步数据或存储大量数据的应用。
示例:
// 使用Firebase Firestore
FirebaseFirestore db = FirebaseFirestore.getInstance();
Map<String, Object> user = new HashMap<>();
user.put("name", "John");
user.put("age", 30);
db.collection("users").add(user);
数据存储方式的选择
选择合适的数据存储方式取决于应用的具体需求:
- 少量简单数据:使用
SharedPreferences。 - 大量复杂数据:使用SQLite或Room。
- 文件数据:使用内部存储或外部存储。
- 跨设备同步:使用云存储。
实战项目:购物车
(1)购物车存放着用户准备购买的商品,一开始是空的,随着商品被加入购物车,购物车中就会显示已添加的商品列表。
(2)除了购物车页面,其它页面(如商场频道页面、商品详情页面),都可能在右上角或者右下角找到购物车图标。购物车图标上会显示已添加的商品数量,且商品数量是实时更新的。
(3)购物车页面、商场频道页面、商品详情页面多处都会显示商品的小图或者大图,如何迅速且高效地加载图片是个需要研究的课题。

线性布局LinearLayout:购物车界面从上往下排列,用到了垂直方向的线性布局。
网格布局GridLayout:商场页面的陈列橱柜,允许分行分列展示商品。
相对布局RelativeLayout:页面右上角的购物车图标,图标右上角又有数字标记,按照指定方位排列控件正是相对布局的拿手好戏。
其他常见控件尚有文本视图TextView、图像视图ImageView,按钮控件Button等。
数据库SQLite:最直观的是数据库,购物车里的商品列表一定放在SQLite中,增删改查都少不了它。
全局内存:购物车图标右上角的数字表示购物车中的商品数量,该数值建议保存在全局内存中,这样不必每次都到数据库中执行count操作。
存储卡文件:App把下载的商品图片保存在存储卡中,这样下次就能直接从存储卡获取商品图片,加快浏览速度。
共享参数SharedPreferences:是否首次访问网络图片,这个标志位推荐放在共享参数中,因为它需要持久化存储,并且只有一个参数信息。
列举几个重要功能的代码片段:
(1)关于页面跳转
购物车页面与商场页面允许互相跳转,此时要指定启动标志FLAG_ACTIVITY_CLEAR_TOP,表示活动栈有且仅有该页面的唯一实例,即可避免多次返回同一页面的情况。
(2)关于商品图片的缓存
建议App缓存常用的图片,一旦从服务器成功下载图片,便在手机存储卡上保存图片文件。下次打开页面时,先从存储卡寻找商品图片,如果找到就显示找到的图片。
(3)关于各页面共同的标题栏
App界面支持局部的公共布局,把标题栏封装到单独的XML文件中,然后各页面通过include标签引用标题栏布局。
总结
掌握安卓数据存储技术是开发高效应用的关键。通过本文的介绍和实践示例,希望你对安卓数据存储方法有一个全面的了解,并在实际开发中灵活应用。持续学习和实践是掌握数据存储技术的关键,祝你在安卓开发的道路上取得成功!
 安卓数据存储使用&spm=1001.2101.3001.5002&articleId=147047549&d=1&t=3&u=da71cea42cbd493fae96331acad2882b)
4448

被折叠的 条评论
为什么被折叠?



