第8章 Android数据存储与I/O
本章要点
-
SharedPreferences的概念和作用
- 使用SharedPreferences保存程序的参数、选项
- 读写其他应用的SharedPreferences
-
Android的文件I/O
- 读写SD卡上的文件
-
了解SQLite数据库
- 使用Android的API操作SQLite数据库
- 使用sqlite3工具管理SQLite数据库
- SQLiteOpenHelper类的功能与用法
-
Android的手势支持
- 手势检测
- 向手势库中添加手势
- 识别用户手势
-
让应用说话(TTS)
所有应用程序都必然涉及数据的输入和输出,Android应用也不例外。应用程序的参数设置、程序运行状态数据等,都需要保存到外部存储器上,这样在系统关机之后数据才不会丢失。Android应用是使用Java或Kotlin语言来开发的,因此开发者在Java I/O中的编程经验大部分都可以“移植”到Android应用开发中。Android系统还提供了一些专门的I/O API,通过这些API可以更有效地进行输入和输出。
如果应用程序只有少量数据需要保存,那么使用普通文件就可以了;但如果应用程序有大量数据需要存储和访问,就需要借助于数据库了。Android系统内置了SQLite数据库,SQLite数据库是一个真正轻量级的数据库,它没有后台进程,整个数据库就对应于一个文件,这样可以非常方便地在不同设备之间移植。Android不仅内置了SQLite数据库,而且为访问SQLite数据库提供了大量便捷的API。本章将会详细介绍如何在Android应用中使用SQLite数据库。
8.1 使用 SharedPreferences
有些时候,应用程序有少量的数据需要保存,而且这些数据的格式很简单,都是普通的字符串、标量类型的值等,比如应用程序的各种配置信息(如是否打开音效、是否使用振动效果等)、小游戏的玩家积分(如扫雷英雄榜之类的)等。对于这种数据,Android 提供了 SharedPreferences 进行保存。
8.1.1 SharedPreferences 与 Editor 简介
SharedPreferences 保存的数据主要是类似于配置信息格式的数据,因此它保存的数据主要是简单类型的 key-value 对。
SharedPreferences 接口主要负责读取应用程序的 Preferences 数据,它提供了如下常用方法来访问 SharedPreferences 中的 key-value 对:
boolean contains(String key)
: 判断 SharedPreferences 是否包含特定 key 的数据。Map<String, ?> getAll()
: 获取 SharedPreferences 数据里全部的 key-value 对。getXxx(String key, xxx defValue)
: 获取 SharedPreferences 数据里指定 key 对应的 value。如果该 key 不存在,则返回默认值 defValue。其中 xxx 可以是 boolean、float、int、long、String 等各种基本类型的值。
SharedPreferences 接口并没有提供写入数据的能力,通过 SharedPreferences.Editor 才允许写入。SharedPreferences 调用 edit()
方法即可获取它所对应的 Editor 对象。Editor 提供了如下方法来向 SharedPreferences 写入数据:
SharedPreferences.Editor clear()
: 清空 SharedPreferences 里所有数据。SharedPreferences.Editor putXxx(String key, xxx value)
: 向 SharedPreferences 存入指定 key 对应的数据。其中 xxx 可以是 boolean、float、int、long、String 等各种基本类型的值。SharedPreferences.Editor remove(String key)
: 删除 SharedPreferences 里指定 key 对应的数据项。boolean apply()
: 当 Editor 编辑完成后,调用该方法提交修改。与apply()
功能类似的方法还有commit()
,区别是commit()
会立即提交修改;而apply()
在后台提交修改,不会阻塞前台线程,因此推荐使用apply()
方法。
提示:从用法角度来看,SharedPreferences 和 SharedPreferences.Editor 组合起来非常像 Map,其中 SharedPreferences 负责根据 key 读取数据,而 SharedPreferences.Editor 则用于写入数据。
SharedPreferences 本身是一个接口,程序无法直接创建 SharedPreferences 实例,只能通过 Context 提供的 getSharedPreferences(String name, int mode)
方法来获取 SharedPreferences 实例,该方法的第二个参数支持如下几个值:
Context.MODE_PRIVATE
: 指定该 SharedPreferences 数据只能被本应用程序读写。Context.MODE_WORLD_READABLE
: 指定该 SharedPreferences 数据能被其他应用程序读,但不能写。Context.MODE_WORLD_WRITEABLE
: 指定该 SharedPreferences 数据能被其他应用程序读写。
提示:从 Android 4.2 开始,Android 不再推荐使用 MODE_WORLD_READABLE、MODE_WORLD_WRITEABLE 这两种模式,因为这两种模式允许其他应用程序来读或写本应用创建的数据,因此容易导致安全漏洞。如果应用程序确实需要把内部数据暴露出来供其他应用访问,则应该使用本书后面介绍的 ContentProvider。
8.1.2 SharedPreferences 的存储位置和格式
下面的程序示范了如何向 SharedPreferences 中写入、读取数据。该程序的界面很简单,它只是提供了两个按钮,其中一个用于写入数据;另一个用于读取数据,故此处不再给出界面布局文件。程序代码如下。
程序清单: codes\08\8.1\SharedPreferencesTest\qs\src\main\java\org\crazyit\io\MainActivity.java
public class MainActivity extends Activity {
private SharedPreferences preferences;
private SharedPreferences.Editor editor;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取只能被本应用程序读写的 SharedPreferences 对象
preferences = getSharedPreferences("crazyit", Context.MODE_PRIVATE);
editor = preferences.edit();
Button read = findViewById(R.id.read);
Button write = findViewById(R.id.write);
read.setOnClickListener(view -> {
// 读取字符串数据
String time = preferences.getString("time", null);
// 读取 int 类型的数据
int randNum = preferences.getInt("random", 0);
String result = time == null ? "您暂时还未写入数据" : "写入时间为: " + time + "\n上次生成的随机数为: " + randNum;
// 使用 Toast 提示信息
Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show();
});
write.setOnClickListener(view -> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss");
// 存入当前时间
editor.putString("time", sdf.format(new Date()));
// 存入一个随机数
editor.putInt("random", (int) (Math.random() * 100));
// 提交所有存入的数据
editor.apply();
});
}
}
上面程序中的第一段粗体字代码用于读取 SharedPreferences 数据,当程序所读取的 SharedPreferences 数据文件根本不存在时,程序也返回默认值,并不会抛出异常;第二段粗体字代码用于写入 SharedPreferences 数据,由于 SharedPreferences 并不支持写入 Date 类型的值,故程序使用了 SimpleDateFormat 将 Date 格式化成字符串后写入。
运行上面的程序,单击程序界面上的“写入数据”按钮,程序将完成 SharedPreferences 的写入,写入完成后打开 Android Studio 的 File Explorer 面板,然后展开文件浏览树,将看到如图 8.1 所示的窗口。
从图 8.1 可以看出,SharedPreferences 数据总是保存在 /data/data/<package name>/shared_prefs
目录下,SharedPreferences 数据总是以 XML 格式保存。通过 File Explorer 面板的导出文件按钮导出该 XML 文档,打开该 XML 文档可以看到如下文件内容:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<int name="random" value="40" />
<string name="time">2018年10月30日 03:44:10</string>
</map>
从上面的文件不难看出,SharedPreferences 数据文件的根元素是 <map.../>
元素,该元素里每个子元素代表一个 key-value 对,当 value 是整数类型时,使用 <int.../>
子元素;当 value 是字符串类型时,使用 <string.../>
子元素……依此类推。
单击程序界面上的“读取数据”按钮,程序弹出一个 Toast 对话框显示上次写入的数据。
实例: 记录应用程序的使用次数
这个简单的实例可以记录应用程序的使用次数:当用户第一次启动该应用程序时,系统创建 SharedPreferences 来记录使用次数。用户以后启动应用程序时,系统先读取 SharedPreferences 中记录的使用次数,然后将使用次数加 1。
本实例与程序界面无关,直接使用 Android Studio 自动生成的界面布局文件即可,因此此处不再给出界面布局文件。本实例程序的代码如下。
程序清单: codes\08\8.1\SharedPreferencesTest\usecount\app\src\main\java\org\crazyit\io\MainActivity.java
public class MainActivity extends Activity {
private SharedPreferences preferences;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
preferences = getSharedPreferences("count", Context.MODE_PRIVATE);
setContentView(R.layout.activity_main);
// 读取 SharedPreferences 里的 count 数据
int count = preferences.getInt("count", 0);
// 显示程序以前使用的次数
Toast.makeText(this, "程序以前被使用了 " + count + " 次。", Toast.LENGTH_LONG).show();
SharedPreferences.Editor editor = preferences.edit();
// 存入数据
editor.putInt("count", ++count);
// 提交修改
editor.apply();
}
}
上面程序中的第一行粗体字代码用于读取 SharedPreferences 中记录的使用次数;第二行粗体字代码将使用次数增加 1,并再次将使用次数写入 SharedPreferences 中。
8.2 File 存储
在学习 Java SE 的过程中,您可能已经了解了 Java 提供的一套完整的 IO 流体系,包括 FileInputStream
、FileOutputStream
等。通过这些 IO 流可以非常方便地访问磁盘上的文件内容。Android 同样支持以这种方式来访问手机存储器上的文件。
8.2.1 openFileOutput
和 openFileInput
Context
提供了如下两个方法来打开应用程序的数据文件夹里的文件 IO 流。
FileInputStream openFileInput(String name)
: 打开应用程序的数据文件夹下的 name 文件对应的输入流。FileOutputStream openFileOutput(String name, int mode)
: 打开应用程序的数据文件夹下的 name 文件对应的输出流。
上面两个方法分别用于打开文件输入流和输出流,其中第二个方法的第二个参数指定打开文件的模式,该模式支持如下值:
MODE_PRIVATE
: 该文件只能被当前程序读写。MODE_APPEND
: 以追加方式打开该文件,应用程序可以向该文件中追加内容。MODE_WORLD_READABLE
: 该文件的内容可以被其他程序读取。MODE_WORLD_WRITEABLE
: 该文件的内容可由其他程序读写。
注意: 从 Android 4.2 开始,Android 不推荐使用 MODE_WORLD_READABLE
和 MODE_WORLD_WRITEABLE
两种模式。
除此之外,Context
还提供了如下几个方法来访问应用程序的数据文件夹:
getDir(String name, int mode)
: 在应用程序的数据文件夹下获取或创建 name 对应的子目录。File getFilesDir()
: 获取应用程序的数据文件夹的绝对路径。String[] fileList()
: 返回应用程序的数据文件夹下的全部文件。boolean deleteFile(String name)
: 删除应用程序的数据文件夹下的指定文件。
下面的程序简单示范了如何读写应用程序数据文件夹内的文件。该程序的界面布局同样很简单,只包含两个文本框和两个按钮,其中第一组文本框和按钮用于处理写入,文本框用于接受用户输入,当用户单击“写入”按钮时,程序将会把数据写入文件;第二组文本框和按钮用于处理读取,当用户单击“读取”按钮时,该文本框显示文件中的数据。
程序清单: codes\08\8.2\FileTest\filetest\src\main\java\org\crazyit\io\MainActivity.java
public class MainActivity extends Activity {
public static final String FILE_NAME = "crazyit.bin";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取两个按钮
Button read = findViewById(R.id.read);
Button write = findViewById(R.id.write);
// 获取两个文本框
EditText edit1 = findViewById(R.id.edit1);
TextView edit2 = findViewById(R.id.edit2);
// 为 write 按钮绑定事件监听器
write.setOnClickListener(view -> {
// 将 edit1 中的内容写入文件中
write(edit1.getText().toString());
edit1.setText("");
});
read.setOnClickListener(view ->
edit2.setText(read()) /* 读取指定文件中的内容,并显示出来 */
);
}
private String read() {
try (FileInputStream fis = openFileInput(FILE_NAME)) {
byte[] buff = new byte[1024];
int hasRead;
StringBuilder sb = new StringBuilder();
// 读取文件内容
while ((hasRead = fis.read(buff)) > 0) {
sb.append(new String(buff, 0, hasRead));
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private void write(String content) {
try (FileOutputStream fos = openFileOutput(FILE_NAME, Context.MODE_APPEND);
PrintStream ps = new PrintStream(fos)) {
// 输出文件内容
ps.println(content);
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面程序中的第一段粗体字代码用于读取应用程序的数据文件,第二段粗体字代码用于向应用程序的数据文件中追加内容。从上面的代码可以看出,当 Android 系统调用 Context
的 openFileInput()
、openFileOutput()
打开文件输入流或输出流之后,接下来 IO 流的用法与 Java SE 中 IO 流的用法完全一样:直接用节点流读写也行,用包装流包装之后再处理也没问题。
运行上面的程序,当单击程序界面中的“写入”按钮时,用户在第一个文本框中输入的内容将会被保存到应用程序的数据文件中。打开 Android Studio 的 File Explorer 面板,可以看到应用程序的数据文件默认保存在 /data/data/<package name>/files
目录下。
8.2.2 读写 SD 卡上的文件
当程序通过 Context
的 openFileInput()
或 openFileOutput()
来打开文件输入流、输出流时,程序所打开的都是应用程序的数据文件夹里的文件,这样所存储的文件大小可能比较有限,因为手机内置的存储空间是有限的。
为了更好地存取应用程序的大文件数据,应用程序需要读写 SD 卡上的文件。SD 卡大大扩充了手机的存储能力。
读写 SD 卡上的文件请按如下步骤进行:
-
请求动态获取读写 SD 卡的权限,只有当用户授权读写 SD 卡时才执行读写。例如使用如下代码:
requestPermissions(new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0x123);
-
调用
Environment
的getExternalStorageDirectory()
方法来获取外部存储器,也就是 SD 卡的目录。 -
使用
FileInputStream
、FileOutputStream
、FileReader
或FileWriter
读写 SD 卡里的文件。
注意点:
- 手机上应该已插入 SD 卡。对于模拟器来说,可通过
mksdcard
命令来创建虚拟存储卡。关于虚拟存储卡的管理请参考第 1 章。 - 为了读写 SD 卡上的数据,必须在应用程序的清单文件 (
AndroidManifest.xml
) 中添加读写 SD 卡的权限。例如如下配置:<!-- 向 SD 卡写入数据的权限 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
下面的程序示范了如何读写 SD 卡上的文件。该程序的主界面与上一个程序的界面完全相同,只是该程序数据读写是基于 SD 卡的。
程序清单: codes\08\8.2\FileTest\sdtest\src\main\java\org\crazyit\io\MainActivity.java
public class MainActivity extends Activity {
private static final String FILE_NAME = "/crazyit.bin";
private EditText edit1;
private TextView edit2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取两个按钮
Button read = findViewById(R.id.read);
Button write = findViewById(R.id.write);
// 获取两个文本框
edit1 = findViewById(R.id.edit1);
edit2 = findViewById(R.id.edit2);
// 为 write 按钮绑定事件监听器
write.setOnClickListener(view -> {
// 运行时请求获取写入 SD 卡的权限
requestPermissions(new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0x123); // ①
});
read.setOnClickListener(view ->
requestPermissions(new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0x456)
);
}
private String read() {
// 如果手机插入了 SD 卡,而且应用程序具有访问 SD 卡的权限
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
File sdCardDir = Environment.getExternalStorageDirectory();
try (FileInputStream fis = new FileInputStream(sdCardDir.getCanonicalPath() + FILE_NAME);
BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {
StringBuilder sb = new StringBuilder();
String line;
// 循环读取文件内容
while ((line = br.readLine()) != null) {
sb.append(line);
}
return sb.toString();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
private void write(String content) {
// 获取 SD 卡的目录
File sdCardDir = Environment.getExternalStorageDirectory();
try {
File targetFile = new File(sdCardDir.getCanonicalPath() + FILE_NAME);
try (RandomAccessFile raf = new RandomAccessFile(targetFile, "rw")