数据存储-持久化技术
瞬时数据:存储在内存中,可能因为程序关闭或其他原因导致内存被回收而丢失的数据。
为了保存关键性数据,要用到数据持久化技术。
数据持久化:将内存中的瞬时数据保存到存储设备中,保证即使在手机或计算机关机的情况下,数据仍不会丢失。
持久化技术提供了一种机制,可让数据在瞬时状态和持久状态间进行转换。
实现数据持久化功能:文件存储、SharedPreferences存储、数据库存储
文件存储
不对存储内容进行任何格式化处理,数据原封不动地保存到文件当中,因而比较适合存储一些简单的文本数据或二进制数据。若是想保存一些复杂的结构化数据,就需要定义一套自己的格式规范,方便之后将数据从文件中重新解析出来。
Context类中提供了一个openFileOutput方法,可用于将数据存储到指定文件中,接收的参数有两个,参数1为文件名,不可包含文件名,参数2为文件的操作模式,分别为MODE_PRIVATE、MODE_APEND,前者为当指定文件已存在时,覆写内容,后者为指定文件存在时,追加内容,但两个都是指定文件不存在时直接创建文件。其实还有MODE_WORLD_READBLE、MODE_WORLD_WRITEABLE,这两个都允许其他应用程序对我们程序中的文件进行读写,在4.2中被废弃。
//此处读取edit框内容
fun save(inputText: String) {
//kotlin没有异常检查机制
try {
//openFileOutput方法返回的是一个FileOutputStream对象,
//通过java流的方式将数据写入文件中
val output = openFileOutput("data", Context.MODE_PRIVATE)
val writer = BufferedWriter(OutputStreamWriter(output))
writer.use {
//此处的use为kotlin的内置拓展函数,
//保证lambda表达式中的代码全部执行完后自动将外层的流关闭。
//无需自行通过finally手动关闭流
it.write(inputText)
}
} catch (e: IOException) {
e.printStackTrace()
}
}
//此处将文件内容写入edit框
edit.setText(load())
fun load(): String {
val content = StringBuilder()
try {
//openFileInput()获取了一个FileInputStream对象,原理同上
val input = openFileInput("data")
val reader = BufferedReader(InputStreamReader(input))
reader.use {
//将文件中的内容一行一行地进行读取
reader.forEachLine {
content.append(it)
}
}
} catch (e: IOException) {
e.printStackTrace()
}
return content.toString()
}
数据存储于/data/data/包名/files
Shared Preferences存储
不同于文件存储,SP是使用键值对进行存储数据的,且支持多种不同的数据类型存储。
使用SP前需要获取SP对象,android提供以下两种方法:
- context类中的getSharedPreferences(),接收两个参数,参数1为文件名称,参数2为操作模式,仅有MODE_PRIVATE,表示只有当前程序可对这个SP文件进行读写,其余模已被废弃。
- activity类中的getPreferences(),仅接收一个参数,操作模式。自动将当前activity的类名作为文件名。
获取SP对象后,通过三步操作完成储存
- 调用SharedPreferences对象的edit方法获取一个SharedPreferences.Editor对象。
- 向SharedPreferences.Editor对象中添加数据,添加布尔类型则使用putBoolean,添加字符串则使用putString。
- 调用apply方法将添加的数据提交,完成数据存储
saveButton.setOnClickListener {
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name", "Tom")
editor.putInt("age", 28)
editor.putBoolean("married", false)
editor.apply()
}
实际效果很像bundle
取出SP文件中的数据
SP对象中提供了一系列的get方法,对应SharedPreferences.Editor方法的每一种put方法,这些get方法都接收两个参数,参数1为键,参数2为默认值,即当传入的键找不到值时以怎样的默认值返回。
//取出数据
restoreButton.setOnClickListener {
val prefs = getSharedPreferences("data", Context.MODE_PRIVATE)
val name = prefs.getString("name", "")
val age = prefs.getInt("age", 0)
val married = prefs.getBoolean("married", false)
Log.d("MainActivity", "name is $name")
Log.d("MainActivity", "age is $age")
Log.d("MainActivity", "married is $married")
}
实现记住密码功能
class LoginActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
//使用SP,打开流对象
val prefs = getPreferences(Context.MODE_PRIVATE)
//读取数据,查询是否记住密码
val isRemember = prefs.getBoolean("remember_password", false)
if (isRemember) {
// 将账号和密码都设置到文本框中
val account = prefs.getString("account", "")
val password = prefs.getString("password", "")
accountEdit.setText(account)
passwordEdit.setText(password)
//此处增添了checkbox,即记住密码的复选框
rememberPass.isChecked = true
}
login.setOnClickListener {
val account = accountEdit.text.toString()
val password = passwordEdit.text.toString()
// 如果账号是admin且密码是123456,就认为登录成功
if (account == "admin" && password == "123456") {
val editor = prefs.edit()
if (rememberPass.isChecked) { // 检查复选框是否被选中
editor.putBoolean("remember_password", true)
editor.putString("account", account)
editor.putString("password", password)
} else {
//未勾选复选框
editor.clear()
}
//存储数据
editor.apply()
//跳转登录成功的页面
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
} else {
Toast.makeText(this, "account or password is invalid",
Toast.LENGTH_SHORT).show()
}
}
}
}
数据存储在/data/data/包名/shared_prefs
SQLite数据库存储
SQLite是轻量级的关系型数据库,运算速度快,占用资源少。不仅支持标准的SQL语法,还遵循了数据库的ACID事务。
SQLiteHelper
SQLiteHelper是抽象类,需要自己创建帮助类来继承,并重写onCreate()和onUpgrade(),SQLiteHelper内还有两个重要的实例方法:getReadableDatabase()和getWritableDatabase()这两个方法都可创建或打开一个现有数据库,并返回一个可对数据库进行读写操作的对象。而两者不同在于,当数据库不可写入时,前者返回的对象以只读的方式打开数据库,而后者则出现异常。
SQLiteHelper中有两个构造方法可供重写,使用参数较少那个即可。
该构造方法中接收4个参数,第一个参数为context。第二个参数为数据库名,第三个参数允许我们在查询数据时返回一个自定义的Cursor,一般传入null即可,第四个参数为数据库版本号。
构建出SQLiteHelper实例后,再调用它的getReadableDatabase()或getWritableDatabase()创建数据库,此时也会执行onCreate()。
数据库文件会存放在/data/data/包名/database。
//创建数据库
class bookDBHelper(val context: Context,name:String,version:Int):SQLiteOpenHelper(context,name,null,version) {
//建表
private val createBook = "create table Book(" +
"id integer primary key autoincrement," +
"author text," +
"price real, " +
"pages integer," +
"name text)"
override fun onCreate(p0: SQLiteDatabase) {
//执行语句
p0.execSQL(createBook)
Toast.makeText(context,"Create Succeeded",Toast.LENGTH_SHORT).show()
}
override fun onUpgrade(p0: SQLiteDatabase?, p1: Int, p2: Int) {
TODO("Not yet implemented")
}
}
//创建helper实例
private val dbHelper = bookDBHelper(this,"BookStore.db",1)
dbHelper.writableDatabase
添加插件:Database Navigator
由查询数据库文件得知(如上图),目录下存在另一个BookStore.db-journal文件,为了让数据库能够支持事务而产生的临时日志文件。
右键BookStore.db ,点击Save AS,导出电脑的任意位置。
添加插件后重启as,左侧会有一栏:DB Browser
添加 SQLite connection,在database file中间三个点处选定文件位置,点击ok即可,此时看到我们的数据库确实创建成功
升级数据库
onUpgrade()方法可用于对数据库进行升级的。
当我们想增添一张表时,仅仅让helper执行建表语句是不够的,因为此时BookStore.db已经被创建,如果想要创建新表,只能卸载应用重新安装,但这样是不可接受的。
因此我们利用onUpgrade,在upgrade中删除已经存在的表,并重新调用onCreate
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("drop table if exists Book")
db.execSQL("drop table if exists Category")
onCreate(db)//onCreate是创建Book和category两张表
}
//修改版本号为大于1的数,使得upgrade被执行
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
createDatabase.setOnClickListener {
dbHelper.writableDatabase
}
对数据库的操作CRUD
插入数据
//插入数据
addData.setOnClickListener {
val db = dbHelper.writableDatabase
//通过put向contentvalues中添加数据
val values1 = ContentValues().apply {
// 开始组装第一条数据
put("name", "The Da Vinci Code")
put("author", "Dan Brown")
put("pages", 454)
put("price", 16.96)
}
//参数1为表名、参数2为给未指定添加数据的列自动赋值,参数3为ContentValues对象
db.insert("Book", null, values1) // 插入第一条数据
val values2 = ContentValues().apply {
// 开始组装第二条数据
put("name", "The Lost Symbol")
put("author", "Dan Brown")
put("pages", 510)
put("price", 19.95)
}
db.insert("Book", null, values2) // 插入第二条数据
}
更新数据
updateData.setOnClickListener {
val db = dbHelper.writableDatabase
val values = ContentValues()
//说明要更新的列和值
values.put("price", 10.99)
//参数1为表名,
//参数2为contentvalue对象,
//参数3为条件,即where部分,而?是一个占位符,通过参数4给出对应内容
//参数4为字符数组,通过arrayof提供编辑创建数组,对应给出占位符指定的内容
//此处是在Book表中,更新书名为“The Da Vinci Code”的书本价格为10.99
db.update("Book", values, "name = ?", arrayOf("The Da Vinci Code"))
}
删除数据
deleteData.setOnClickListener {
val db = dbHelper.writableDatabase
db.delete("Book", "pages > ?", arrayOf("500"))
}
查询数据
SQLiteDatabase提供了query方法,参数最少的也有7个
不指定columns则默认查询所有列 select *,
不指定selection和selectionArgs则默认查询所有行 ,
不指定groupBy则表明不对查询结果进行group by操作,
不指定having则表明不进行进一步地过滤,
不指定orderby则表明使用默认的排序方式。
//通过按键触发
queryData.setOnClickListener {
val db = dbHelper.writableDatabase
// 查询Book表中所有的数据,此处等价 select * from Book,返回了一个Cursor对象
val cursor = db.query("Book", null, null, null, null, null, null)
if (cursor.moveToFirst()) {
do {
// 遍历Cursor对象,取出数据并打印
val name = cursor.getString(cursor.getColumnIndex("name"))
val author = cursor.getString(cursor.getColumnIndex("author"))
val pages = cursor.getInt(cursor.getColumnIndex("pages"))
val price = cursor.getDouble(cursor.getColumnIndex("price"))
Log.d("MainActivity", "book name is $name")
Log.d("MainActivity", "book author is $author")
Log.d("MainActivity", "book pages is $pages")
Log.d("MainActivity", "book price is $price")
} while (cursor.moveToNext())
}
//一定要关闭Cursor
cursor.close()
}
不使用API完成CRUD
//添加数据:
db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
arrayOf("The Da Vinci Code", "Dan Brown", "454", "16.96")
)
db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
arrayOf("The Lost Symbol", "Dan Brown", "510", "19.95")
)
//更新数据:
db.execSQL("update Book set price = ? where name = ?", arrayOf("10.99", "The Da Vinci Code"))
//删除数据:
db.execSQL("delete from Book where pages > ?", arrayOf("500"))
//查询数据:除了查询使用的rawQuery,其他都是execSQL
val cursor = db.rawQuery("select * from Book", null)
SQLite数据库是支持事务的,事务的特性可以保证一系列的操作要么全部完成,要么一个都不会完成。此特性很有用,就像转账,需要一方扣除金额,一方收到同等金额才能算成功。
//以下是事务的标准用法
//通过beginTransaction开启事务,然后在一个异常捕获的代码块中执行具体的数据库操作
//完成操作后调用setTransactionSuccessful()表示事务已经执行成功,
//最终在finally块中调用endTransaction结束事务
//此处手动抛出异常,检查数据库会发现数据未发生改变
replaceData.setOnClickListener {
val db = dbHelper.writableDatabase
db.beginTransaction() // 开启事务
try {
db.delete("Book", null, null)
if (true) {
// 手动抛出一个异常,让事务失败
throw NullPointerException()
}
val values = ContentValues().apply {
put("name", "Game of Thrones")
put("author", "George Martin")
put("pages", 720)
put("price", 20.85)
}
db.insert("Book", null, values)
db.setTransactionSuccessful() // 事务已经执行成功
}catch (e: Exception) {
e.printStackTrace()
} finally {
db.endTransaction() // 结束事务
}
}
升级数据库的最佳写法
//此处提供两次更新的范例,因为每次更新版本号,才会调用onUpgrade,因此我们对每次的版本号进行判定
//这样的判定能保证用户从任意低的版本可升级到最高版本。
//而新安装的用户在onCreate阶段就已经是最新版本了
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion <= 1) {
db.execSQL(createCategory)
}
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion <= 1) {
db.execSQL(createCategory)
}
if (oldVersion <= 2) {
db.execSQL("alter table Book add column category_id integer")
}
}
优化SharePreferences的写法
//这样的写法跟java很像,在kotlin里面应该写得更简洁
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name", "Tom")
editor.putInt("age", 28)
editor.putBoolean("married", false)
editor.apply()
//简洁写法
//此处使用了高阶函数和拓展函数的写法,在SharedPreferences拓展了open函数,且接收一个函数类型的参数
//
fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
//由于拓展函数拥有类的上下文,因此此处直接调用edit()
val editor = edit()
//调用函数参数
editor.block()
//提交数据
editor.apply()
}
//存储数据,无需自己调用apply,因为open最后会自动调用apply
getSharedPreferences("data", Context.MODE_PRIVATE).open {
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}
谷歌的ktx库其实已经提供了简便写法,原理就是上面这样,具体写法无异
//androidx.core:core-ktx:1.0.2
getSharedPreferences("data", Context.MODE_PRIVATE).edit {
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}
简化ContentValues写法
mapof()函数,允许使用 A to B这样的语法创建键值对,这在kotlin中为Pair对象。
//vararg指的是可变参数列表,可向此方法传入n个pair类型参数(n=0,1,2...)
//这些参数会被赋值到pairs里面,使用for-in即可遍历
//Pair是键值对类型数据,因此需要指定键和值的数据类型
//而ContentValues的键都为字符串类型,因此指定string类型,
//而值可以有多重类型,指定为Any?,any为kotlin所有类的基类,
//?则允许其传入空值,但由于any指定类型不明,需要通过when或if特判定
fun cvOf(vararg pairs: Pair<String, Any?>) = ContentValues().apply {
for (pair in pairs) {
val key = pair.first
val value = pair.second
when (value) {
//此处带有kotlin的smart cast功能,自动转换类型,
//的value已经是对应类型
is Int -> put(key, value)
is Long -> put(key, value)
is Short -> put(key, value)
is Float -> put(key, value)
is Double -> put(key, value)
is Boolean -> put(key, value)
is String -> put(key, value)
is Byte -> put(key, value)
is ByteArray -> put(key, value)
null -> putNull(key)
}
}
}
同样kotlin也提供了 通过ktx库实现的简便写法
val values = contentValuesOf("name" to "Game of Thrones", "author" to "George Martin",
"pages" to 720, "price" to 20.85)
db.insert("Book", null, values)