欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android开发学习笔记——Android的持久化数据存储

程序员文章站 2022-05-16 14:35:09
...


在我们的开发过程中,我们可以发现,应用程序的使用实际上就是对数据的交互,我们的应用所实现的实际上就是对不同数据的展示和操作。在实际开发中,我们往往会通过网络请求来获取数据,但是有时候我们也需要将数据保存起来以备下一次的使用,当然我们也可以重新从服务器拉取数据,但是网络请求操作是属于耗时操作,而且需要消耗网络资源,因此在开发中我们应该尽量减少网络请求的次数。所以,我们就需要去在本地存储一些应用相关信息,比如:常见的记住密码、保留登录状态等。
我们知道,在代码中,无论全局变量又或是局部变量,这些数据都是保存在内存中的,一旦因为程序关闭又或是其它原因造成内存被回收,那么内存中的数据均会被销毁而丢失,因此这些数据也都被称为瞬时数据。如果使用瞬时数据来保存密码或是登录状态,这显然不合理的,因为当应用关闭后数据就丢失了,在下次开启应用时无法再次获取到数据。此时,我们就需要使用到Android的持久化数据技术了。Android为我们提供了文件存储、SharedPreference存储和SQLite数据库存储等本地持久化存储方式。

文件存储

提到持久化存储,我们其实可以很容易想到,既然内存中的数据会丢失,那么我们将数据保存为一个文件不久可以了吗?确实,这种方式是可行的,文件存储时Android中最基本的一种数据存储方式,它可以将数据保存到文件中,比较适合用于存储一些简单的文本数据或是二进制数据。在我们使用应用过程中,通常会产生很多缓存文件,那就是应用将需要经常访问的数据通过文件存储的形式将数据保存起来以减少网络请求,即缓存。
文件存储实际上简单来说就是在手机上创建一个文件,然后来存储数据,因此其本质上来说就是对文件的I/O操作,使用输入输出流来完成。

openFileOutput和openFileInput

Android中,context提供了openFileOutput和openFileInput两个方法来操作本应用程序中数据文件夹中即/data/data/< package name >/files/目录下的文件IO流。
openFileOutput方法可以接收两个参数,其中第一个参数为文件名,我们不可以包含路径,因为使用openFileOutput方法开启的文件均为内部存储/data/data/< package name >/files/目录下的文件;第二个参数为文件操作模式,我们常用的主要为MODE_PRIVATE和MODE_APPEND,前者为默认模式,如果文件已存在就会覆盖原文件内容,后者会追加到原内容之后。而openFileInput方法只接收一个文件名一个参数。在获取到输入输出流之后,其它操作就属于IO操作了。
具体使用方式如下:

//保存数据
val output = openFileOutput("test.txt", Context.MODE_PRIVATE)
val writer = BufferedWriter(OutputStreamWriter(output))
writer.write("this is a test string")
writer.close()

//读取数据
val input = openFileInput("test.txt")
val reader = BufferedReader(InputStreamReader(input))
val str = StringBuilder()
reader.forEachLine {
    str.append(it)
}
reader.close()
Log.e("test_file", str.toString())

查看Device File Explored,如下图:
Android开发学习笔记——Android的持久化数据存储
日志输出如下:
Android开发学习笔记——Android的持久化数据存储
需要注意的时,文件的IO操作属于耗时操作,我们在实际开发中要将其放在子线程中执行,以防阻塞进程。同时,我们也要注意在使用完IO流之后要调用close方法进行关闭。
通过openFileOutput和openFileInput获取到的文件都是位于内部存储的指定路径下的文件,在该路径下的文件只有本应用能够访问且当应用被卸载时,文件也会同步被删除。实际上,使用文件存储进行持久化数据存储,我们完全可以设置文件路径为任意路径下,我们可以将文件保存至外部存储中,不过此时,我们可能会需要申请SD卡的读写权限。

SharedPreference存储

文件存储能够简单地将数据存储在文件中,但是我们往往难以将其格式化存储,比如说我们需要存储用户名和用户密码两个信息,尽管我们可以通过文件存储将其保存到本地文件中,但是在之后数据使用时确显得不太方便,因为文件存储只是将两个数据简单的存储起来,我们无法对其进行区分,必须提前设置一定的格式,然后再进行解析。
与文件存储不同的是,SharedPreference是使用键值对的方式来存储数据的,也就是说对于每一条数据value都有一条对应的键key,而且支持不同的数据类型,我们可以轻易通过key来获取对应的value值且数据类型不变,因此与文件存储相比SharedPreference会方便很多。

基本使用

SharedPreference的使用非常简单,基本可以分为以下几步:

  • 首先,获取SharedPreference对象;
  • 通过SharedPreference对象的edit()方法获取对应的Editor对象,然后调用对应的putXXX方法并调用commit或是apply来存储数据;
  • 调用SharedPreference对象的getXXX方法根据键值key获取对应的数据。

在Android中主要提供了三种方法用于得到SharedPreference对象,具体如下:

方法名 说明
getSharedPreferences 通过Context调用;存在两个参数,第一个参数为文件名,第二个参数为操作模式目前只能指定为MODE_PRIVATE(自 API 级别 17 起,MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 模式已被弃用。 从 Android 7.0(API 级别 24)开始,如果您使用这些模式,Android 会抛出 SecurityException。如果您的应用需要与其他应用共享私有文件,可以通过 FLAG_GRANT_READ_URI_PERMISSION 使用 FileProvider。)
getPreferences 通过Activity调用;只存在一个操作模式参数,其文件名为当前Activity的类名
getDefaultSharedPreferences 通过PreferenceManager类(已被废弃)调用的静态方法,只接收一个context参数,使用当前应用包名作为文件名

具体使用如下:

 //第一个参数为文件名,第二个参数为操作模式
 val sp = getSharedPreferences("test_sp", Context.MODE_PRIVATE)
 //只有一个操作模式参数,使用当前Activity类名作为文件名
 val sp2 = getPreferences(Context.MODE_PRIVATE)
 //接收一个context参数,使用当前应用包名作为文件名(已废弃)
 val sp3 = PreferenceManager.getDefaultSharedPreferences(this)

事实上,SharedPreference也是通过XML文件的形式存储的,所有的SharedPreference文件都存储在/data/data/< package name>/shared_prefs/目录下,如果指定的SharedPreference文件不存在就会新建一个。使用Device File Explorer查看如下图:
Android开发学习笔记——Android的持久化数据存储
SharedPreference中数据以XML的格式存储,如下:
Android开发学习笔记——Android的持久化数据存储

保存数据

使用SharedPreference保存数据,首先我们需要通过edit()方法获取对应的SharedPreference.Editor对象,然后调用对应数据类型的put方法指定数据的key-value键值对,最后调用commit()或者是apply()即可。SharedPreference支持的数据类型包括Int、String、Boolean、Float、Long和StringSet,其对应的put方法为:PutInt、PutString、PutBoolean、PutFloat、PutLong和PutStringSet,具体使用如下:

val sp = getSharedPreferences("test_sp", Context.MODE_PRIVATE)
val editor = sp.edit()
editor.putString("user_name", "yang")
editor.putString("password", "password")
editor.putBoolean("flag", false)
editor.putInt("age", 12)
editor.apply()

查看SharedPreference文件,我们就能够看到以xml格式存储的数据,如下:
Android开发学习笔记——Android的持久化数据存储
commit和apply的功能相同,不同的是,apply() 会立即更改内存中的 SharedPreferences 对象,但会将更新异步写入磁盘;而commit() 将数据同步写入磁盘。但是,由于 commit() 是同步的,我们应避免从主线程调用它,防止阻塞主进程。

数据获取

SharedPreferences获取数据的方式非常简单,我们只需要获取到对应的SharedPreferences对象,然后利用getXX方法,根据key就能够获取到对应的数据。具体如下:

val sp = getSharedPreferences("test_sp", Context.MODE_PRIVATE)
val name = sp.getString("user_name", "default_value")
val password = sp.getString("password", "default_value")
val flag = sp.getBoolean("flag", true)
val age = sp.getInt("age", 12)
Log.e("test_sp", "name:$name--password:$password--flag:$flag--age:$age")

日志输出如下:
Android开发学习笔记——Android的持久化数据存储
注意,获取数据时SharedPreferences的文件名以及数据的key值必须和保存数据时相同,否则就无法正常获取到对应的数据。在实际开发中,我们通常会封装好一个SharedPreferences的工具类并以常量来指定key,从而提升代码的可读性并防止出错。

SQLite数据库存储

SharedPreferences的使用非常方便,但是只适用于保存一些简单的数据和键值对,无法存储大量的关系型数据。Android系统中内置了SQLite数据库,SQLite是一款轻量级的关系型数据库,SQLite支持标准的SQL语法,其运算速度非常快,占用资源很少,从本质来看,SQLite数据库只是一个文件其存储位置为/data/data/< PackageName>/databases,其操作方式只是一种更为便捷的文件操作,当应用程序创建或打开一个SQLite数据库时,其实只是打开一个文件进行读写。

基本使用

Android提供了SQLiteDatabase类作为SQLite数据库管理类,我们可以通过SQLiteDatabase对象来创建并访问数据库,从而完成SQLite数据库存储。
SQLiteDatabase类无法直接进行实例化,而是提供了如下的静态方法来创建一个实例:

  • openDatabase:开启一个对应的SQLite数据库,当数据库不存在时报错
  • openOrCreateDatabase:开启一个对应的SQLite数据库,当该数据库不存在时创建数据库

获取到SQLiteDatabase之后,我们就可以使用调用SQLiteDatabase的相应的方法来操作数据库了,其常用方法如下:

方法名 方法说明
execSQL 执行sql语句,SQLite支持标准sql语法,我们可以直接使用sql语句来操作数据库
insert 插入数据
update 修改更新数据
delete 删除数据
query 查询数据
rawQuery 使用带占位符的sql语句查询数据
beginTransaction 开始事务
endTransaction 结束事务,执行本方法时,系统会判断是否已执行setTransactionSuccessful,如果之前已设置就提交,如果没有设置就回滚。

创建数据库并获取SQLiteDatabase

首先,我们需要通过openOrCreateDatabase和openDatabase来获取SQLiteDatabase对象。openOrCreateDatabase和openDatabase都拥有很多重载方法,但是其最主要的参数都是需要指定数据库的绝对路径的,通常我们都将数据库创建到默认的路径/data/data/packname/database目录下,如下:

val db = SQLiteDatabase.openOrCreateDatabase(getDatabasePath("my_db"), null)

其中getDatabasePath方法获取到的数据库路径,"my_db"为数据库名,运行代码,创建数据库my_db,查看Device File Explored,对应的数据库文件已创建,如下图:
Android开发学习笔记——Android的持久化数据存储

使用SQL语句操作数据库

使用execSQL我们可以直接使用sql语法来操作数据库,同时,execSQL也提供了一个使用占位符的重载方法,我们可以使用?进行占位,然后再传入一个数组来说明值,如下:

private fun insert(book : Book){
    val db = SQLiteDatabase.openOrCreateDatabase(getDatabasePath("my_db"), null)
    //使用占位符
    db.execSQL("insert into Book values(null, ?,?,?,?)", arrayOf(book.name, book.pages, book.author, book.price))
    Log.e("test_db", "create table Book")
}

private fun delete(){
    val db = SQLiteDatabase.openOrCreateDatabase(getDatabasePath("my_db"), null)
    db.execSQL("delete from Book where name = 'Android'")
    Log.e("test_db", "delete book <Android>")
}

private fun update(){
    val db = SQLiteDatabase.openOrCreateDatabase(getDatabasePath("my_db"), null)
    db.execSQL("update Book set author = 'Tom' where name = 'Android'")
    Log.e("test_db", "update book 1")
}

private fun query(){
    val db = SQLiteDatabase.openOrCreateDatabase(getDatabasePath("my_db"), null)
    val cursor = db.rawQuery("select * from Book", arrayOf())
    if (cursor.moveToFirst()){//不为空
        do {
            val id = cursor.getInt(cursor.getColumnIndex("id"))
            val name = cursor.getString(cursor.getColumnIndex("name"))
            val author = cursor.getString(cursor.getColumnIndex("author"))
            val pages = cursor.getInt(cursor.getColumnIndex("pages"))
            val price = cursor.getFloat(cursor.getColumnIndex("price"))
            Log.e("test_db", "book----id:$id, name:$name, author:$author, pages:$pages, price:$price")
        }while (cursor.moveToNext())
    }
    cursor.close()
}

我们可以看到,使用execSQL方法,我们只要直接使用对应的sql语句即可。如果,参数过多,不容易拼接成sql语句,我们可以再sql中使用?占位,然后再指定值。如上述的插入语句。

增删改查

除了使用sql语句外,Android还提供了对应的insert、delete、update和query方法,通过调用这些方法,我们也可以完成对数据库的操作。如下代码所示:

private fun insert(book : Book){
     val db = SQLiteDatabase.openOrCreateDatabase(getDatabasePath("my_db"), null)
     val values = ContentValues()
     values.put("name", "Java")
     values.put("author", "Tom")
     values.put("pages", 111)
     values.put("price", 99.8)
     //第一个参数为表名,第二个参数给某些可空的列自动赋值为null,第三个参数为插入数据
     db.insert("Book", null, values)
 }

 private fun delete(){
     val db = SQLiteDatabase.openOrCreateDatabase(getDatabasePath("my_db"), null)
     //第一个参数为表名;第二个参数为删除条件可带占位符(不指定就删除所有);第三个参数为占位符的值
     db.delete("Book", "name = ?", arrayOf("Java"))
 }

 private fun update(){
     val db = SQLiteDatabase.openOrCreateDatabase(getDatabasePath("my_db"), null)
     val values = ContentValues()
     values.put("price", 19.8)
     //第一个参数为表名;第二个参数为更新后的值;第三个参数为更新约束条件可带占位符(不指定就更新所有);第四个参数为占位符的值
     db.update("Book", values,"name = ?", arrayOf("Java"))
 }

 private fun query(){
     val db = SQLiteDatabase.openOrCreateDatabase(getDatabasePath("my_db"), null)
     //查询所有行
     val cursor = db.query("Book",null,null,null,null,null,null)
     if (cursor.moveToFirst()){//不为空
         do {
             val id = cursor.getInt(cursor.getColumnIndex("id"))
             val name = cursor.getString(cursor.getColumnIndex("name"))
             val author = cursor.getString(cursor.getColumnIndex("author"))
             val pages = cursor.getInt(cursor.getColumnIndex("pages"))
             val price = cursor.getFloat(cursor.getColumnIndex("price"))
             Log.e("test_db", "book----id:$id, name:$name, author:$author, pages:$pages, price:$price")
         }while (cursor.moveToNext())
     }
     cursor.close()
 }

其中,query方法的参数最为复杂,但其实这些参数也都是我们再sql语句中可能使用到的语法,因此只要对sql语句熟悉,这些方法我们都能够快速学会,query的参数如下:
Android开发学习笔记——Android的持久化数据存储

SQLiteOpenHelper

为了方便管理数据库,Android提供了一个SQLiteOpenHelper帮助类,通过这个类我们能够很方便的对数据库进行创建和升级。SQLiteOpenHelper是一个抽象方法,因此需要我们创建一个自己的帮助类去继承并重写onCreate和onUpgrade方法。SQLiteOpenHelper中的常用方法如下:

方法名 说明
onCreate 抽象方法,需要重写;当第一次创建数据库时的回调方法,通常执行一些建表和初始化数据操作
onUpgrade 抽象方法,需要重写;当数据库版本更新时回调该方法,即版本号改变时
getReadableDatabase 以只读方式打开数据库,获取对应的SQLiteDatabase对象
getWritableDatabase 以读写方式打开数据库,获取对应的SQLiteDatabase对象
close 关闭所有打开的SQLiteDatabase

首先,自定义DBHelper类继承自SQLiteOpenHelper,并实现onCreate和onUpgrade方法,在onCreate中创建User表并插入一条数据,代码如下:

class DBHelper(
    context: Context?,
    name: String?,
    factory: SQLiteDatabase.CursorFactory?,
    version: Int
) : SQLiteOpenHelper(context, name, factory, version) {

    override fun onCreate(db: SQLiteDatabase?) {
        //创建数据库建表
        db?.execSQL("create table User(id integer primary key autoincrement, name text, age integer)")
        db?.execSQL("insert into User values(null,?,?)", arrayOf("yang",12))
        Log.e("test_db", "create database")
    }

    override fun onUpgrade(db: SQLiteDatabase?, p1: Int, p2: Int) {

    }
}

使用时,我们需要创建DBHelper的实例,其构造方法中name代表了数据库名,第三个参数factory代表了我们查询数据时的自定义Cursor通常可以直接传入null,最后一个参数为数据库版本号,当数据库表结构发生变化时,版本号也应该改变,否则不会调用onUpgrade方法。获取到DBHelper的实例后,我们就可以通过getReadableDatabase和getWritableDatabase方法获取到SQLiteDatabase实例进入对数据库进行访问操作了。如下:

val dbHelper = DBHelper(this, "test_db", null, 1)
val db = dbHelper.readableDatabase
//访问数据库操作
......

创建后,由于时第一次访问test_db数据库,此时我们还没有创建该数据库,所以会调用onCreate方法创建数据库,日志如下:
Android开发学习笔记——Android的持久化数据存储
再次访问该数据库,onCreate不再调用。
查看Device File Explore,发现创建了对应的数据库文件如下:
Android开发学习笔记——Android的持久化数据存储
通过sqlite3命令行工具,我们可以查看User表,如下图:
Android开发学习笔记——Android的持久化数据存储
如果,此时我们的用户表添加了一个性别,此时User表的表结构改变了,我们需要重新创建User表,但是如果我们指定onCreate在数据库创建后就不会再被调用了,所以我们是无法在onCreate中改变User表的,这时,我们就需要使用到onUpgrade方法来升级数据库了,修改onUpgrade方法如下:

override fun onUpgrade(db: SQLiteDatabase?, p1: Int, p2: Int) {
    //升级用户表
    db?.execSQL("drop table if exists User")//删除原先的数据表
    db?.execSQL("create table User(id integer primary key autoincrement, name text, age integer, sex text)")
    db?.execSQL("insert into User values(null,?,?,?)", arrayOf("yang",12,"man"))
    Log.e("test_db", "upgrade database--oldVersion:$p1--newVersion$p2")
}

此时,我们只需要在创建DBHelper实例时改变一下版本号,系统就会判断当前数据库需要更新版本,然后调用onUpgrade方法,如下:

val dbHelper = DBHelper(this, "test_db", null, 2)

输出日志如下:
Android开发学习笔记——Android的持久化数据存储
使用sqlite3查看数据库,发现User表已更新,如下:
Android开发学习笔记——Android的持久化数据存储