文章目录
- 数据持久化
- 文件存储
- 将数据存储进文件
- 实例
- 从文件中读取数据
- 实例
- SharedPreferences存储
- 将数据存储进文件
- 实例
- 从文件中读取数据
- 实例
- 实现记住密码的功能
- SQLite数据库存储
- 创建自己的帮助类
- 调用自己的帮助类
- 补全 onUpgrade() 方法
- 增删查改
- 增:SQLiteDatabase.insert()
- 改:SQLiteDatabase.update()
- 删:SQLiteDatabase.delete()
- 查:SQLiteDatabase.query()
- 通过 SQL语句 实现增删查改
数据持久化
保存在内存中的数据是属于瞬时状态的,而保存在存储设备中的数据上处于持久状态的,持久化技术提供了一种可以让数据在瞬时状态和持久状态之间转换的机制。
Android系统中主要提供了3种常用方式用于简单地实现数据持久化功能,即文件存储、SharedPreference存储以及数据库存储。
文件存储
将数据存储进文件
Context类 中提供了一个 openFileOutput
方法,用于将数据存储到指定的文件中。这个方法接收两个参数:
- 第一个参数是文件名:在文件创建的时候使用的就是这个名称,文件名不可以包含路径,因为所有的文件都是默认存储到
/data/data/<packagename>/files/
目录下的。 - 第二个参数是文件的操作模式:主要有两种模式可以选,
MODE_PRIVATE
默认的操作模式,写入的内容会覆盖原文件的内容;MODE_APPEND
则表示如果该文件已经存在,就往文件里面追加内容,不存在就创建新文件。
该方法返回一个 FileOutputStream
对象,得到了这个对象之后就可以使用 Java流 的方式将数据写入到文件中了。
实例
在布局文件中添加输入框 EditText
控件:
在活动文件中,定义不同生命周期的不同行为:
onCreate
方法:获取 EditText 实例;onDestroy
方法:获取 EditText 中的内容,并通过自定义方法save
保存到名为data
的文件中。
public class SecondActivity extends AppCompatActivity {private EditText editText;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.second_layout);editText = findViewById(R.id.edit);}@Overrideprotected void onDestroy() {super.onDestroy();String inputText = editText.getText().toString();save(inputText);}public void save(String inputText){FileOutputStream out = null; // 文件字节输出流,继承OutputStream类BufferedWriter writer = null; // 将文本写入字符输出流try {// 获得一个 字节输出流对象,规定数据存储到名为data的文件中,文件的操作模式为MODE_PRIVATEout = openFileOutput("data", Context.MODE_PRIVATE);// 借助out构建OutputStreamWriter临时对象,作为从字符流到字节流的桥接// 再通过临时对象构建 字符输出流对象 以便将文本内容写入到字节流中writer = new BufferedWriter(new OutputStreamWriter(out));// 将文本内容写入到字符流中writer.write(inputText);} catch (IOException e){e.printStackTrace();} finally {try {if(writer != null){writer.close();}} catch (IOException e){e.printStackTrace();}}}
}
PS:对于上述将文本存入文件的流程,一开始我理解错了,顺着代码顺寻看以为是字节流转成字符流再写入文件,把 inputText
当保存文本的文件了。。。
其实正确逻辑是:
运行结果:
在文本框内输入内容:
退出程序后,在AS中通过如下操作打开文件页面:
在下图路径中找到 data
文件,查看其内容:
从文件中读取数据
Context 类中还提供了一个 openFileInput
方法,用于从文件中读取数据。这个方法只接受一个参数:
- 要读取的文件名:然后系统会自动到
/data/data/<packagename>/files
目录下去加载这个文件。
该方法返回一个 FileInputStream
对象,得到了这个对象之后再通过 Java流 的方式就可以将数据读取出来了。
实例
若 EditText 为空则将文件中的文本读入到 EditText 中:
public class SecondActivity extends AppCompatActivity {private EditText editText;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.second_layout);editText = findViewById(R.id.edit);String inputText = load();if(!TextUtils.isEmpty(inputText)){editText.setText(inputText);editText.setSelection(inputText.length());Toast.makeText(this, "loading successed", Toast.LENGTH_LONG).show();}}public String load(){FileInputStream in = null;BufferedReader reader = null;StringBuilder content = new StringBuilder();try {in = openFileInput("data"); // 文本字节输入流// InputStreamReader作为字符流到字节流的桥接reader = new BufferedReader(new InputStreamReader(in));String line = "";// 从字符流中读取数据,每次读取文件的一行while((line = reader.readLine()) != null){content.append(line);}} catch (IOException e){e.printStackTrace();} finally {if(reader != null){try {// 处理完文本后关闭流reader.close();} catch (IOException e){e.printStackTrace();}}}return content.toString();}
}
PS:在判空时使用了 TextUtils.isEmpty()
而非 String.isEmpty()
,这是因为:
- String 类下的
isEmpty()
返回的只是 字符串的长度是否为0,如果 字符串为null 就会直接报 空指针。源码如下:
public boolean isEmpty() { return count == 0; }
- TextUtils.isEmpty() 会对 null 和 长度 进行判断,所以 不会报空指针。源码如下:
public static boolean isEmpty(CharSequence str) { if (str == null || str.length() == 0) return true; else return false;
}
此时,一打开界面即可显示:
SharedPreferences存储
将数据存储进文件
大致分为两步,第一步,获取对象:
SharedPreferences 通过 键值对 的方式来存储数据的。要想存储数据,需要先获取 SharedPreferences对象,Android 中主要提供了三种方法用于得到 SharedPreferences 对象:
- Context类 中的
getSharedPreferences
方法:此方法接受两个参数,- 第一个参数用于指定
SharedPreferences
文件的名称,如果文件不存在则创建一个。文件都存放在/data/data/<packagename>/shared_prefs/
目录下。 - 第二个参数用于指定 操作模式,目前只有 MODE_PRIVATE 这种默认的操作模式可选,和直接传入
0
效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences
文件进行读写。
- 第一个参数用于指定
- Acitvity类 中的
getPreferences
方法:只有一个参数——操作模式,自动使用当前活动的类名来作为SharedPreferences
的文件名。 - PreferenceManager类 中的
getDefaultSharedPreferences
方法:静态方法,接收一个Context
参数,并自动使用当前应用程序的包名作为前缀来命名SharedPreferences
文件。
第二步,存储数据:
- 调用 SharedPreferences对象 的
edit方法
来获取一个SharedPreferences.Editor对象
。 - 向 SharedPreferences.Editor对象 中添加数据,比如添加一个布尔型数据就使用
putBoolean
方法,添加一个字符串则使用putString
方法。 - 调用
apply
方法将添加的数据提交,从而完成数据存储操作。
实例
实现点击按钮保存数据的功能:
Button button_share = findViewById(R.id.button_share);button_share.setOnClickListener((View v)->{SharedPreferences.Editor editor = getSharedPreferences("data", MODE_PRIVATE).edit();editor.putString("name", "cmy");editor.putInt("weight", 120);editor.putBoolean("married", false);editor.apply();Toast.makeText(this, "share over", Toast.LENGTH_LONG).show();});
点击红框所示按钮:
即可将数据存在如下图所示的路径中:
从文件中读取数据
第一步仍是获取对象,上文已经讲过,这里不再赘述。
第二步,通过对应的 get**()方法
获取对应类型数据,如字符串使用 getString()
方法,这些 get
方法都接受两个参数:
- 第一个参数是键:也就是
KV模型
中的Key
; - 第二个参数是默认值:当传入的键找不到对应值时,以默认值返回。
实例
点击 get sharePreferences
按钮从 SharedPreferences文件
中读取数据:
再通过 Toast
显示出来:
Button button_getShare = findViewById(R.id.button_getShare);button_getShare.setOnClickListener((View v)->{SharedPreferences preferences = getSharedPreferences("data", MODE_PRIVATE);String name = preferences.getString("name", "");int weight = preferences.getInt("weight", 0);boolean married = preferences.getBoolean("married", false);String res = name+" "+String.valueOf(weight)+" "+String.valueOf(married);Toast.makeText(this, res, Toast.LENGTH_LONG).show();});
实现记住密码的功能
之前在本博客里实现过登陆界面,这里为登陆界面新加入一个记住密码的功能。
修改布局文件,添加以下代码,实现右侧所示布局:
这里使用到了一个新控件 复选框:CheckBox ,用户可以通过点击来进行选中/取消,以表是否需要记住密码。
修改 LoginActivity.java
代码,结合 SharedPreferences
实现记住密码的功能:
增添的内容主要是:
- 三个相关对象 CheckBox、SharedPreferences、SharedPreferences.Editor 的创建和实例化;
- 初始化布尔型对象
isRemember
作为 判断记住密码功能是否生效 的辅助变量;- 一开当然不存在
remember_password
这个键对应的值,isRemember
为默认值false
; - 成功登陆一次后,
remember_password
这个键对应的值就是true
了。
- 一开当然不存在
以及登陆成功后的操作:
- 调用 CheckBox 的
isChecked()
方法检查复选框是否被选中,被选中则返回true
; - 为
true
时表示用户希望记住密码,此时:- 将
remember_password
对应的值设为true
; - 把
account
和password
对应的值都存入到SharedPreferences
文件中并提交。
- 将
- 为
false
表示用户并不想记住密码,此时要调用clean()
方法清楚掉SharedPreferences
文件中的所有数据。
运行结果:
- 输入正确的账户和密码,并选中记住密码,点击登录:
- 通过强制下线跳转回登陆界面,此时发现账号密码已经自动填充了。
- 如果此时取消选中复选框,再点击登录:
- 再次返回登陆界面就不会被填充了:
PS:这里只做示例,实际项目中不能将密码以明文形式存储到 SharedPreferences 文件中,因为会被轻易盗取,必须结合加密算法对密码进行加密。
SQLite数据库存储
为了管理数据库,安卓专门提供了一个 SQLiteOpenHelper 帮助类,这是个抽象类,如果要使用它,需要创建一个 自己的帮助类 去继承它。下面列举几个该类中常用的方法:
- 两个抽象方法:
onCreate
和onUpgrade
,我们必须在 自己的帮助类 里重写这两个方法,然后分别在这两个方法中去实现创建、升级数据库的逻辑。 - 两个实例方法:
getReadableDatabase
和getWritableDatabase
。这两个方法都可以创建或者打开一个现有的数据库,数据库文件存放在/data/data/<packagename>/databases/
目录下,并返回一个可对数据库进行读写的对象。不同的是,当数据库不可写入的时候,getReadableDatabase
方法返回的对象会用只读的方式打开数据库,而getWritableDatabase
会出现异常。 - 两个构造函数:常用的一个构造方法接收4个参数:第一个是Context;第二个是数据库名;第三个参数允许我们在查询数据的时候返回一个自定义的Cursor,一般都是传入null;第四个参数表示当前数据库的版本号,可以用于升级数据库。
创建自己的帮助类
public class MyDatabaseHelper extends SQLiteOpenHelper {public static final String CREATE_STUDENT = "create table Student ("+ "id integer primary key autoincrement,"+ "gender text,"+ "weight real,"+ "age integer,"+ "name text)";private Context context;public MyDatabaseHelper( Context context, String name,SQLiteDatabase.CursorFactory factory, int version) {super(context, name, factory, version);this.context = context;}@Overridepublic void onCreate(SQLiteDatabase db) {db.execSQL(CREATE_STUDENT);Toast.makeText(context, "create succeeded", Toast.LENGTH_LONG).show();}@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
}
- 把建表语句定义成一个字符串常量,
integer
表示整型,real
表示浮点型,text
表示文本类型,blob
表示二进制类型。此外,使用了primary key
将 id 设置为主键,并且用autocrement
关键字表示 id 列是自增长的。 - 在
onCreate
方法中有调用了 SQLiteDatabase 的execSQL
方法去执行这条建表语句。
调用自己的帮助类
创建一个活动 SQLiteActivity
,其布局内有一个按钮,点击即可创建 Student.db
数据库:
public class SQLiteActivity extends AppCompatActivity {private MyDatabaseHelper myDatabaseHelper;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.sqlite_layout);myDatabaseHelper = new MyDatabaseHelper(this, "Student.db", null, 1);Button button_create = findViewById(R.id.button_create);button_create.setOnClickListener((View v)->{myDatabaseHelper.getWritableDatabase();Toast.makeText(this, "创建数据库成功", Toast.LENGTH_LONG).show();});}
}
- 第一次点击按钮时,会检测到当前程序并没有
Student.db
这个数据库,于是会创建该数据库并调用 MyDatabaseHelper 中的onCreate
方法,得以创建Student
表。 - 之后点击按钮就不会再调用 MyDatabaseHelper 中的
onCreate
方法了,因为Student.db
已经存在了。
布局文件 sqlite_layout.xml
:
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><Buttonandroid:id="@+id/button_create"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="create database"/>
</LinearLayout>
运行结果:
create succeeded
只会在数据库首次创建时出现:
而 创建数据库成功
会在每次点击按钮后出现:
查看数据库和表的创建情况
在环境变量中添加好 platform-tools
后:
打开 cmd,输入 adb shell
进入设备控制台:
通过 su
获取管理员权限,否则无法进入 /data/data/<packagename>/databases/
路径:
通过 cd
命令进入数据库文件所在目录:
该目录下有两个数据库文件,一个是我们创建的 Student.db
;一个是为了让支持事务的临时日志文件 Student.db-journal
。
打开数据库:
查看数据库中有哪些表:
PS:android_metadata
是每个数据库自动生成的。
查看建表语句:
通过 .exit
或 .quit
退出数据库:
补全 onUpgrade() 方法
该方法用于升级数据库,目前项目中已经有了一张 Student
表用于存放学生的各种详细数据,如果想再添加一张 Class
表用于记录学生的班级,怎么做呢?
将建表语句添加到自己的帮助类 MyDatabaseHelper
中:
该如上图所示在 onCreate
阶段执行一次 Class
的建表语句吗?
不是的,正如上一个实例中,数据库创建完成后,我们再点击按钮,只会弹出 创建数据库成功
而不会弹出 create succeeded
一样,两者的根本原因都是 onCreate
方法只会在创建数据库时执行一次,创建成功后不会再次执行。
因此无法在 Student.db
存在的情况下通过 onCreate
方法添加新表,而应通过 onUpgrade
方法添加新表。 具体做法如下:
@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {// 如果数据库中存在 Student 表或 Class表,就将他们删除。db.execSQL("drop table if exists Student");db.execSQL("drop table if exists Class");// 然后调用 onCreate 方法重新创建onCreate(db);}
PS:之所以 不跳过删除已有表直接调用 onCreate ,是因为 创建表时如果该表已存在会报错。
接下来重新调用 SQLiteOpenHelper 的构造方法,使第四个参数——数据库版本号大于之前传入的 1 即可让 onUpgrade
执行:
运行结果:
PS:使用 AS 时也可以通过一下流程查看数据库及建表情况:
增删查改
CRUD 操作当然可以通过 SQL 语言实现,但 Android 也提供了一系列辅助方法,前面提到 getReadableDatabase
和 getWriteableDatabase
方法是会返回一个 SQLiteDatabase
对象,借助这个对象就可以对数据进行操作了。
增:SQLiteDatabase.insert()
该方法有三个参数:
- 表名
- 用于在未指定添加数据的情况下给某些可为空的列自动赋值为
null
ContentValues
对象,它提供了一系列的put
方法重载,用于向ContentValues
中添加数据,只需要将表中的每个列名及待添加数据传入即可。
在布局中添加了一个按钮用于增加数据:
在 SQLiteActivity 的 onCreate
方法中添加以下代码:
点击两次按钮的运行结果:
两张表各添加了两行数据。
改:SQLiteDatabase.update()
该方法有四个参数:
- 表名;
- ContentValues 对象;
- 第三个、第四个参数用于约束更新某一行或者某几行的数据,不指定的话默认更新所有行。
在布局中添加了一个按钮用于更新数据:
在 SQLiteActivity 的 onCreate
方法中添加以下代码:
Button button_update = findViewById(R.id.button_update);button_update.setOnClickListener((View v)->{SQLiteDatabase db = myDatabaseHelper.getReadableDatabase();ContentValues values = new ContentValues();// 第一条数据values.put("weight", 90);values.put("name", "zj");db.update("Student", values, "id = ?", new String[] {"2"});// 第二条数据values.put("class_name", "电子");values.put("class_num", 183);db.update("Class", values, "id = ?", new String[] {"1"});Toast.makeText(this, "更新完成", Toast.LENGTH_LONG).show();});
以第一条数据为例:
- values 用以更新
weight
和name
两项属性的值; - 第三、第四个参数指定更新
id=2
的行。
点击按钮后的运行结果:
删:SQLiteDatabase.delete()
该方法接受三个参数:
- 表名;
- 第二、三个用于约束删除某几行的数据,不指定则删除所有行。
在 SQLiteActivity 的 onCreate
方法中添加以下代码:
Button button_delete = findViewById(R.id.button_delete);button_delete.setOnClickListener((View v)->{SQLiteDatabase db = myDatabaseHelper.getReadableDatabase();db.delete("Student", "weight < ?", new String[] {"100"});Toast.makeText(this, "删除完成", Toast.LENGTH_LONG).show();});
- 删除
weight < 100
的行。
点击按钮后的运行结果:
查:SQLiteDatabase.query()
该方法比前三个复杂一些,最短的一个重载方法也需要传入七个参数:
- 表名;
- 用于指定查询哪几列;
- 三四个参数用于约束查询某几行的数据,不指定则默认查询所有行的数据;
- 第五个参数用于指定需要去
group by
的列,不指定则不对查询结果进行分组; - 第六个参数用于对
group by
之后的数据进一步过滤; - 第七个参数用于指定查询结果的排序方式。
调用该方法后会返回一个 Cursor
对象,查询到的所有数据都将从这个对象中取出。
Button button_query = findViewById(R.id.button_query);button_query.setOnClickListener((View v)->{SQLiteDatabase db = myDatabaseHelper.getReadableDatabase();// 查询 Class 表中所有数据Cursor cursor = db.query("Class", null, null, null,null, null, null);if(cursor.moveToFirst()){do{String res = "";res += cursor.getString(cursor.getColumnIndex("class_name")) + " ";res += cursor.getString(cursor.getColumnIndex("class_num"));Toast.makeText(this, res, Toast.LENGTH_LONG).show();}while(cursor.moveToNext());}cursor.close();});
- 将 query方法 首参数设置为
Class
,其余参数设置为null
,表示查询 Class表 所有数据。 - 调用 moveToFirst方法 将数据指针移动到第一行的位置;
- 通过 getColumnIndex方法 获取位置索引以遍历每一行数据,并将之通过
Toast
打印到屏幕上; - 通过 moveToNext方法 移动数据指针遍历下一行数据,如果指针已经到达了数据指针集的尾后位置,此方法将返回
false
。
通过 SQL语句 实现增删查改
除了查询语句通过 db.rawQuery()
执行之外,其他三种操作都可以通过 db.execSQL()
执行。