【Android】数据存储,文件,数据库

Android中数据存储

一、在内部存储读写文件

1.文件io读写

  • 写文件
//写入数据
private fun saveFile() {
    //将文件写入内部存储空间时,只能在本应用的目录中写入,不能写入其他应用的目录中
    val file = File("data/data/com.wzh.a01_filestore/innerFile.txt")
    BufferedWriter(FileWriter(file)).use {
        it.write("你好啊")
    }
}

文件保存在此位置

  • 读文件
	//读取文件数据
	private fun readFile() {
        val file = File("data/data/com.wzh.a01_filestore/innerFile.txt")
        val sb = StringBuilder()
        BufferedReader(FileReader(file)).useLines {
            it.iterator().apply {
                while (this.hasNext()){
                    sb.append(this.next())
                }
            }
        }
        textView.text = sb.toString()
    }
  • 应用只能在自己的包名目录下读写文件,在本应用的目录下读写文件是不需要权限的

2.使用api方法获取内部存储空间路径

  • 获取files文件夹路径context.getFilesDir()

    • 路径是data/data/包名/files
    • 这个文件夹会永久保存
  • cache文件路径context.getCacheDir()

    • 路径是data/data/包名/cache
    • 如果存储空间满了,系统可能会删除此文件夹
  • 系统管理应用界面的清除缓存,会清除cache文件夹下的东西,清除数据,会清除整个包名目录下的东西


二、外部空间存储文件

2.2之前:sdcard
2.2~4.2:mnt/sdcard
4.3开始:storage/sdcard

	val file = File("sdcard/info.txt")
    BufferedWriter(FileWriter(file)).use {
        it.write("你好啊")
    }
 //需要申请权限,6.0后需要动态申请权限

由于每个厂商定制的系统获取外部存储路径都不同,所以需要使用系统提供的api进行访问

Environment.getExternalStorageDirectory() 这个方法一定能获取到sdcard的路径的
Environment.getExternalStorageState() sd卡状态。
常见的状态:
MEDIA_MOUNTED:sd卡可用
MEDIA_UNMOUNTED:sd卡存在,但是没有挂载
MEDIA_REMOVED:sd卡不存在
MEDIA_CHECKING:sd卡正在遍历

使用外部存储时,先要判断sd卡是否可用

if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {}

获取sd卡空间

  • 存储设备被分成若干个区块
  • 每个区块的大小 * 区块总数 = 存储设备的总大小
  • 每个区块的大小 * 可用区块总数 = 可用空间大小
 val path = Environment.getExternalStorageDirectory().absolutePath
            //构造一个新的StatF来查看path处的文件系统的统计信息
            val statFs = StatFs(path)
            //获取总区块
            val blockCount = statFs.blockCountLong
            //每个区块的大小
            val blockSizeLong = statFs.blockSizeLong

            //可用空间的总区块
            val availableBlocksLong = statFs.availableBlocksLong

            val text = Formatter.formatFileSize(this, blockCount * blockSizeLong)
            btn_total_space.text = text
            

三、文件的访问权限

每一个应用都是一个独立的用户

权限drwxrwxrwx

  • 第一位:d表示文件夹,-表示文件,不是任何权限
    - 第一组rwx:文件拥有者(owner)的权限
    • r:读
    • w:写
    • x:执行
-	第二组rwx:与文件拥有者同一用户组的用户(grouper)
-	第三组rwx:其他用户(other)的权限

openFileOutput的四种模式

context.openFileOutput(“文件名”,模式),相当于data/data/包名/files

  • MODE_PRIVATE:-rw-rw----
  • MODE_APPEND:-rw-rw----
  • MODE_WORLD_WRITEABLE:-rw-rw–w-
  • MODE_WORLD_READABLE:-rw-rw-r–

创建其他用户可读写的模式,是非常危险的,google已经把后两个废弃了

创建全局可读可写的文件(不推荐)

val fos = openFileOutput("info.txt", Context.MODE_WORLD_READABLE or Context.MODE_WORLD_WRITEABLE)
fos.write("哈哈".toByteArray())
fos.close()

//高版本已经废弃
//Caused by: java.lang.SecurityException: MODE_WORLD_READABLE no longer supported

四、xml

使用XMl序列化器生成xml文件

  • 写xml文件,使用XmlSerializer生成xml文件
private fun writeXml(){
        val xs = Xml.newSerializer()
        //设置xml的输出
        xs.setOutput(openFileOutput("sms.xml",Context.MODE_PRIVATE),"UTF-8")

        //生成头节点
        //encoding  编码属性
        //standalone    是否需要别的文件约束,true表示独立的,不需要约束
        xs.startDocument("utf-8",true)

        //生成开始标签
        xs.startTag(null,"smss")

        val list = listOf(
            Sms("haha", "123", 1),
            Sms("haha2", "12345", 2),
            Sms("haha3", "123123123", 1)
        )

        list.forEach {sms->
            xs.startTag(null,"sms")

            xs.startTag(null,"body")
            xs.text(sms.body)
            xs.endTag(null,"body")

            xs.startTag(null,"number")
            xs.text(sms.number)
            xs.endTag(null,"number")

            xs.startTag(null,"type")
            xs.text(sms.type.toString())
            xs.endTag(null,"type")

            xs.endTag(null,"sms")
        }


        xs.endTag(null,"smss")

        //生成尾节点
        xs.endDocument()
    }
  • 读xml文件,使用pull解析,读一行,解析一行,以事件类型驱动的。
private lateinit var list: MutableList<Sms>

    private fun readXml(): String {
        val stream = openFileInput("sms.xml")

        //获取xmlpull解析器,读取一行,解析一行
        val xp = Xml.newPullParser()
        //设置输入
        xp.setInput(stream, "utf-8")
        //获取当前节点事件类型
        var eventType = xp.eventType
        var sms: Sms? = null
        //如果不是结束节点,遍历获取
        while (eventType != XmlPullParser.END_DOCUMENT) {
            when (eventType) {
                //开始节点
                XmlPullParser.START_TAG -> {
                    //当前节点的名称
                    when (xp.name) {
                        "smss" -> {
                            list = mutableListOf()
                        }

                        "sms" -> {
                            sms = Sms()
                        }
                        "body" -> {
                            //获取文本节点
                            sms?.body = xp.nextText()
                        }
                        "number" -> {
                            sms?.number = xp.nextText()
                        }
                        "type" -> {
                            sms?.type = xp.nextText().toInt()
                        }
                    }
                }

                //结束节点
                XmlPullParser.END_TAG -> {
                    sms?.let {
                        if (xp.name == "sms") {
                            list.add(it)
                        }
                    }

                }
            }
            //把指针移动到下个节点,并返回该节点的事件类型
            eventType = xp.next()
        }
        val sb = StringBuilder()
        list.forEach {
            sb.append(it)
        }
        return sb.toString()
    }

五、Json

使用JSONObject封装

  • 写json文件
private fun writeJson(){
        val list = listOf(
            Sms("haha", "123", 1),
            Sms("haha2", "12345", 2),
            Sms("haha3", "123123123", 1)
        )

        val ja = JSONArray()
        list.forEach {
            val jo = JSONObject()
            jo.put("body",it.body)
            jo.put("number",it.number)
            jo.put("type",it.type)
            ja.put(jo)
        }
        openFileOutput("sms.json",Context.MODE_PRIVATE).use {
            it.write(ja.toString().toByteArray())
        }
    }
  • 读json文件
private fun readJson():String{
        val sb = StringBuilder()
        openFileInput("sms.json").use {
            BufferedReader(InputStreamReader(it)).useLines {
                val iterator = it.iterator()
                while (iterator.hasNext()){
                    sb.append(iterator.next())
                }
            }
        }
        return sb.toString()
    }

六、SQLite数据库

轻量级数据库

创建数据库

  • 需要定义一个类继承SQLiteOpenHelper,传入数据库名,版本号等。版本号必须大于等于1
  • 数据库创建时,会调用onCreate,如果数据库已存在,则不会调用此方法
  • 数据库升级时,会调用onUpgrade
  • 创建数据库
class MyOpenHelper(context: Context?, name: String?) : SQLiteOpenHelper(context, name, null, 1) {
    override fun onCreate(db: SQLiteDatabase?) {
    }
    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
    }
}

val openHelper = MyOpenHelper(appContext, "my.db")
//创建数据库,如果数据库不存在时,先创建,再打开,如果存在就直接打开
val sqliteDb = openHelper.writableDatabase//可读可写的

//getWritableDatabase():打开可读写的数据库
//getReadableDatabase():在磁盘空间不足时打开只读数据库,否则打开可读写数据库

创建表

在sqlite中的oncreate方法中执行sql语句,在sqlite中,数据类型不是很重要,不管定义什么类型,在底层都会使用char接收

 db?.execSQL("CREATE TABLE person(_id integer primary key autoincrement,name char(10),phone char(20))")

①使用sql拼接

  • 插入(insert)
    @Test
    fun insert() {
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.wzh.a01_filestore", appContext.packageName)

        val helper = MyOpenHelper(appContext, "my.db")
        val db = helper.readableDatabase
        db.execSQL("INSERT INTO person(name,phone) VALUES(?,?)", arrayOf("张三", "123132131435"))
        db.close()
    }
  • 删除(delete)
    @Test
    fun delete() {
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.wzh.a01_filestore", appContext.packageName)

        val helper = MyOpenHelper(appContext, "my.db")
        val db = helper.readableDatabase
        db.execSQL("DELETE FROM person where _id = ?", arrayOf("1"))
        db.close()
    }
  • 更新(update)
	@Test
    fun update() {
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.wzh.a01_filestore", appContext.packageName)

        val helper = MyOpenHelper(appContext, "my.db")
        val db = helper.readableDatabase

        db.execSQL("UPDATE person SET phone = ? WHERE _id = ?", arrayOf("8966878", 2))
        db.close()
    }
  • 查询(select)
    @Test
    fun select() {
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.wzh.a01_filestore", appContext.packageName)

        val helper = MyOpenHelper(appContext, "my.db")
        val db = helper.readableDatabase

        val cursor = db.rawQuery("SELECT phone FROM person", null)

        cursor?.apply {
            val list = mutableListOf<String>()
            while (this.moveToNext()){
                val phone = this.getString(getColumnIndex("phone"))
                list.add(phone)
            }
            list.forEach {
                println(it)
            }
        }
    }

②使用api的方式

  • 插入
/**
 * @param table 表名
 * @param nullColumnHack    这个字段没什么用
 * @param values    插入保存的字段
 * @return  -1表示插入失败
 */
public long insert(String table, String nullColumnHack, ContentValues values)
    @Test
    fun insertApi() {
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.wzh.a01_filestore", appContext.packageName)

        val helper = MyOpenHelper(appContext, "my.db")
        val db = helper.readableDatabase
        val cv = ContentValues()
        cv.put("name", "李四")
        cv.put("phone", "4535345345")
        //table:表名
        //nullColumnHack:这个字段基本没用,传null
        //values:保存的字段
        val rowId = db.insert("person", null, cv)
        //返回值如果是-1,则插入失败
        if (rowId != -1L) {
            //成功
        } else {
            //插入失败
        }
    }
  • 删除
/**
 * @param table 表名
 * @param whereClause  条件
 * @param whereArgs    条件参数的值
 * @return  被影响的行数
 */
public int delete(String table, String whereClause, String[] whereArgs)
    fun deleteApi() {
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.wzh.a01_filestore", appContext.packageName)

        val helper = MyOpenHelper(appContext, "my.db")
        val db = helper.readableDatabase

        //返回影响的行数
        val rowCount = db.delete("person", "_id = ?", arrayOf("2"))
    }
  • 更新
/**
 * @param table 表名
 * @param values    修改的字段值
 * @param whereClause  条件
 * @param whereArgs    条件参数的值
 * @return  被影响的行数
 */
public int update(String table, ContentValues values, String whereClause, String[] whereArgs)
    @Test
    fun updateApi() {
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.wzh.a01_filestore", appContext.packageName)

        val helper = MyOpenHelper(appContext, "my.db")
        val db = helper.readableDatabase

        //要修改的值
        val cv = ContentValues()
        cv.put("name", "马云")
        //返回影响的行数
        val rowCount = db.update("person", cv, "_id = ?", arrayOf("3"))
    }
  • 查询
/**
 * @param table 表名
 * @param columns   查询的列名,如果查全部的话,传入null
 * @param selection  查询条件
 * @param selectionArgs    条件参数的值数组
 * @param groupBy    分组
 * @param having    having
 * @param orderBy    排序
 * @param limit    分页
 * @return  被影响的行数
 */
public Cursor query(String table, String[] columns, String selection,
            String[] selectionArgs, String groupBy, String having,
            String orderBy, String limit)
    @Test
    fun selectApi() {
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.wzh.a01_filestore", appContext.packageName)

        val helper = MyOpenHelper(appContext, "my.db")
        val db = helper.readableDatabase

        val cursor =
            db.query("person", arrayOf("name"), "_id = ?", arrayOf("3"), null, null, null, null)
        while (cursor.moveToNext()) {
            val name = cursor.getString(cursor.getColumnIndex("name"))
            println("name:${name}")
        }
    }

③使用事务

//开启事务
db.beginTransaction()
//设置事务执行成功,如果这行代码没执行,就会回滚
db.setTransactionSuccessful()
//关闭事务,提交数据
db.endTransaction()

    @Test
    fun transaction() {
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.wzh.a01_filestore", appContext.packageName)

        val helper = MyOpenHelper(appContext, "my.db")
        val db = helper.readableDatabase

        try {
            //开启事务
            db.beginTransaction()

            val cv = ContentValues()
            cv.put("name", "哈哈")
            db.update("person", cv, "_id = ?", arrayOf("3"))

            cv.clear()
            cv.put("name","呵呵")
            db.update("person", cv, "_id = ?", arrayOf("4"))

            //设置事务执行成功,如果这行代码没执行,就会回滚
            db.setTransactionSuccessful()
        }catch (e:Exception){
            e.printStackTrace()
        }finally {
            //关闭事务,提交数据
            db.endTransaction()
        }

    }

七、Room数据库

对象关系映射,采用注解的方式,底层封装SQLite数据库。采用编译时注解将sqlite进行封装

①引入room库

apply plugin: 'kotlin-kapt'

def room_version = "2.2.5"	
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"

②创建数据库类

定义一个抽象类继自RoomDatabase

  • 创建内存数据库,这种数据库的数据只存在于内存中,也就是进程被杀之后,数据随之丢失
Room.inMemoryDatabaseBuilder(MainApp.application, MsgDatabase::class.java).build()
  • 创建持久化的数据库
/**
 * entities 对应表的实体字节码数组
 * version  数据库版本号,升级时,需要增加
 * exportSchema 导出数据库的信息,true为开启,默认是为true,可以指定注解处理器导出的目录,需要指定目录的属性是room.schemaLocation
 *              在对应的build.gradle中的defaultConfig下使用
 *              javaCompileOptions{
                        annotationProcessorOptions{
                            arguments=["room.schemaLocation":"$projectDir/schemas".toString()]
                        }
                }
 */
@Database(entities = [Msg::class], version = 1, exportSchema = true)
abstract class MsgDatabase() : RoomDatabase() {
    companion object {
        private var db: MsgDatabase? = null

        /**
         * 获取当前实例
         */
        fun getInstance(context: Context): MsgDatabase {
            if (db == null) {
                synchronized(MsgDatabase::class) {
                    if (db == null) {
                        db = createDatabase(context)
                    }
                }
            }
            return db!!
        }

        /**
         * 初始化数据库
         */
        private fun createDatabase(context: Context): MsgDatabase {
            return Room.databaseBuilder(context, MsgDatabase::class.java, "msg.db")
                .allowMainThreadQueries()//是否允许在主线程进行查询,一般都是在子线程中
                .addCallback(object : RoomDatabase.Callback() {
                    override fun onCreate(db: SupportSQLiteDatabase) {
                        super.onCreate(db)
                        println("activity create database")
                    }

                    override fun onOpen(db: SupportSQLiteDatabase) {
                        super.onOpen(db)
                        println("activity open database")
                    }
                }).build()
            //.setQueryExecutor()//设置查询的线程池,一般使用默认的就行
            //.setJournalMode()//设置日志模式,默认使用JournalMode.AUTOMATIC
            //.fallbackToDestructiveMigration()//数据库升级异常后的回滚
            //.fallbackToDestructiveMigrationFrom()//数据库升级异常后根据指定版本进行回滚
            //.addMigrations(MsgDatabase.migration)//数据库升级

        }

        /**
         * 升级
         * @param   startVersion    开始版本
         * @param   endVersion  结束版本
         */
        private val migration = object : Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) {
                //操作数据库语句
                //database.update()
                //database.delete()
                //database.execSQL()
            }
        }
    }

    /**
     * 获取表对应的dao
     * 此dao需要创建一个接口,使用@Dao注解
     */
    abstract fun getMsgDao(): MsgDao
}

③创建表(实体类),使用@Entiry注解进行描述

@Entity
data class Msg(
    val name: String,
    @ColumnInfo
    val phone: String,
    val money: Float
) {
    @PrimaryKey(autoGenerate = true)
    var id: Int = 0 //自增长时,需要初始化为0
}

④创建访问对象dao接口

创建一个接口,使用@Dao注解进行描述,这个类就是提供操作数据库表的类

@Dao
interface MsgDao {

    /**
     * 插入
     * @param   onConflict  冲突策略
     * @return  如果返回-1,则插入失败
     */
    @Insert(entity = Msg::class, onConflict = OnConflictStrategy.REPLACE)
    fun insertApi(msg: Msg): Long


    /**
     * 删除
     * @return  影响的行数
     */
    @Delete(entity = Msg::class)
    fun deleteApi(msg: Msg): Int

    /**
     * 更新
     * @return  影响的行数
     */
    @Update(entity = Msg::class, onConflict = OnConflictStrategy.REPLACE)
    fun updateApi(msg: Msg): Int


    /**
     * 查询
     */
    @Query("SELECT * FROM msg")
    fun selectApi(): List<Msg>
}

注意:需要调用到数据库的操作方法,才会创建数据库,也就是调用到Dao中的方法。

  • 使用@Query注解,可以在注解参数列表中传入sql语句
    /**
     * 根据id删除
     */
    @Query("DELETE FROM msg WHERE id = :id")
    fun deleteMsgById(id: Int): Int

    /**
     * 根据名称更新数据
     */
    @Query("UPDATE msg SET phone = :phone WHERE name = :name")
    fun updateMsgByName(name: String, phone: String): Int

⑤在外部调用dao方法即可

⑥Room常用的注解

@Database(entities:Array>,version:Int,exportSchema:Boolean) 数据库创建抽象类的注解。
entities:被@Entity标记的实体对象数组。
version版本号。
exportSchema是否输出日志,默认是true,导出数据库的信息,true为开启,默认是为true,可以指定注解处理器导出的目录,需要指定目录的属性是room.schemaLocation
在对应的build.gradle中的defaultConfig下使用
javaCompileOptions{
annotationProcessorOptions{
arguments=[“room.schemaLocation”:“$projectDir/schemas”.toString()]

@Dao 标记在接口

@Entity(tableName:String,indices:Array,inheritSuperIndices:Boolean,primaryKeys:Array,foreignKeys:Array,ignoredColumns:Array) 表对应的实体对象。
indices = {@Index(value = “key”, unique = false)}//本表索引,用于大量数据的查询优化,unique有时候需要保证数据表的某个或者某些字段只有唯一的记录,可以通过设置@Index注解的unique属性实现。以下实例代码实现了避免有两条记录包含一样的key值。
inheritSuperIndices = false//如果 该值为true,那么父类中标记的indices{}索引也会算作该表的索引
primaryKeys = {“key”}//主键,一些策略逻辑会用到,比如插入一条数据时如果已存在,则更新否则算新的插入,那么怎么判断 ,数据库中是否已存在该条数据呢?就判断提供的主键,在表中是否已存在
foreignKeys = {
//外键,一般用于多表数据查询.可以配置多个外键
//ForeignKey用来设置关联表数据更新时所进行的操作,比如可以在@ForeignKey注解中设置onDelete=CASCADE,这样当Cache表中某个对应记录被删除时,ForeignTable表的所有相关记录也会被删除掉。
//对于@Insert(OnConflict=REPLACE)注解,SQLite是进行REMOVE和REPLACE操作,而不是UPDATE操作,这个可能影响到foreign key的约束。
//value:关联查询的表的Java.class,这里给定ForeignTable.class
//parentColumns:与之关联表ForeignTable表中的列名
//childColumns:本表的列的名称,必须要和parentColumns个数一致。这两个可以理解为根据cache表中的那个字段去比对ForeignTable表中的那个字段,认为是有关联关系的数据。
//onDelete:关联表中某条记录被delete或update时,本表应该怎么做:
// NO_ACTION:什么也不做,
// RESTRICT:本表跟parentColumns有关系的数据会立刻删除或更新,但不允许一对多的关系,
// SET_NULL:本表所跟parentColumns有关系的数据被设置为null值,
// SET_DEFAULT:本表所有跟parentColumns有关系的数据被设置为默认值,也是null值
// CASCADE:本表所有跟parentColumns有关系的数据一同被删除或更新
//onUpdate:本表中某条记录被更新时,与之关联的表应该怎么做
//deferred:本表某条记录变更时,与之关联表的数据变更是否要立即执行,还是等待本表事务处理完再来处理关联表。默认是同时处理。
// @ForeignKey(value = ForeignTable.class,
// parentColumns = “foreign_key”,
// childColumns = “key”,
// onDelete = 1,
// onUpdate = 1,
// deferred = false)}
ignoredColumns = {“data”}//本表中 那些字段 不需要 映射到表中

@PrimaryKey(autoGenerate:Boolean)//必须要有,且不为空,autoGenerate 主键的值是否由Room自动生成,默认false

@ColumnInfo(name = "_data"),指定该字段在表中的列的名字

@Embedded 对象嵌套

@TypeConverter 类型转换器,例如在对象中存在列表类型,Date日期类型,可以只用此注解将类型转换成可以保存到数据库的数据

//类型转换,根据自己的项目需要,转换成对应的数据
class PermissionConverters {
    @TypeConverter
    fun listToJson(value: ArrayList<PersonPermissionInfo>?): String {
        return GsonUtils.toJson(value)
    }

    @TypeConverter
    fun jsonToList(value: String): ArrayList<PersonPermissionInfo>? {
        return GsonUtils.fromJson(value,
            object : TypeToken<ArrayList<PersonPermissionInfo>>() {}.type)
    }
}

@TypeConverters(clazz:Array) 将类型转换器注解在数据库上

@TypeConverters(PermissionConverters::class)
abstract class XXX: RoomDatabase()

你可能感兴趣的:(Android基础知识,sqlite,android,数据库)