Sqlite简易性能优化方案,给你的应用插上“翅膀”

4,096 阅读3分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

最近对数据库进行了一番优化,增加耗时统计,以及优化现有的sql语句操作,减少读写耗时,这篇文章就从三个方面来讲述下相关的知识点,希望能对你有所帮助。

准备工作

由于下面的优化操作需要一个操作环境,所以我说下当前我做了哪些准备工作:

  1. 数据表的结构

    image.png
  2. 映射的数据Model类

class UserInfo {
    @JvmField
    var xuhao = 0
    @JvmField
    var name = ""
    @JvmField
    var height  = 0L
    @JvmField
    var weight = 0.0f
    @JvmField
    var married = false
    @JvmField
    var update_time = ""
}
  1. 准备好的数据

准备工作期间,为了下面的优化对比操作,我已经向该数据库的表中插入了几万条数据:

image.png

接下来我们就开始具体的实验工作吧。

一. 善于使用事务

操作数据库读写时一定要记得开启事务,事务的好处大家都知道,特别是某次sql指令执行的非常频繁的时候,开启事务能极大减少读写磁盘的次数,减少读写执行耗时

为了验证上面的结论,我们分别通过开启事务和不开启事务,一次性向数据库执行2500次update操作,对比下这两个条件下具体的执行耗时。

1. 不开启事务

代码如下:

fun updateByCommon() {
    for (i in 0..2500) {
        val update = "update user_info set update_time = 'qy2_$i' where age = $i"
        userDBHelper.writableDatabase.execSQL(update)
    }
}

耗时统计使用的是kotlin官方库提供的扩展函数measureTimeMillis{}非常的方便:

mBinding.tsUpdate.setOnClickListener {
    measureTimeMillis {
        updateByTs()
    }.also {
        Log.i("SqliteActivity", "tsUpdate: time = $it")
    }
}

接下来我们该场景下的耗时:

image.png

我总共执行了4次,差不多不开启事务一次性执行2500次update操作的耗时平均为7.2s左右。可以看到,相当的耗时。

2. 开启事务

实验环境和上面的相同,除了开启事务来执行2500次update更新操作,先看下实验代码:

fun updateByTs() {
    writeDb.beginTransaction()
    for (i in 0..2500) {
        val update = "update user_info set update_time = 'qy1_$i' where age = $i"
        userDBHelper.writableDatabase.execSQL(update)
    }
    writeDb.setTransactionSuccessful()
    writeDb.endTransaction()
}

最终的执行耗时如下:

image.png

执行了4次,每次的平均耗时为5.2s左右,可以看到开启事务相比较于不开启事务执行,2500次的update操作耗时减少了大概2s

这就的优化就结束了吗?不,我们还可以再减少几百ms,下面的SQLiteStatement会带给你更大的惊喜哈!!

二. 善于显示创建SQLiteStatement

这个SQLiteStatement是什么呢,其实平常我们调用SQLiteDatabaseinsertupdate等操作,最终都会将sql语句和对应的参数包装成一个SQLiteStatement对象,通过该对象执行最终的增删改查操作:

SQLiteDatabase.update()源码:

image.png image.png

SQLiteDatabase.insert()源码:

image.png image.png

从源码中都会看到,每一次insertupdate操作都会创建一个SQLiteStatement对象,像我们第一小节做的实验,执行了2500次update操作,那就相当于频繁创建了2500个SQLiteStatement对象,想想都可怕。

所以这里的优化就是:在执行update操作之前,显示创建一个SQLiteStatement对象,通过?占位符bindXXX系列方法实现一次创建、多次update的操作。

说的再多不如数说话,我们直接开启测试,先看下相关代码:

fun updateBySQLiteStatement() {
    val statement = writeDb.compileStatement("update user_info set update_time = ? where age = ?")
    writeDb.beginTransaction()

    for (i in 0..2500) {
        statement.clearBindings()
        statement.bindString(1, "qy2_$i")
        statement.bindLong(2, i.toLong())
        statement.executeUpdateDelete()
    }
    writeDb.setTransactionSuccessful()
    writeDb.endTransaction()
}

通过compileStatement()方法显示创建一个SQLiteStatement对象,其中clearBindings()是清理上次绑定的数据,接下来我们看下执行耗时:

image.png

平均耗时为5.1s,相比较开启事务读写,减少了100ms左右,不多但也是一个优化角度。

PS:显示创建SQLiteStatement的方式在我们项目中的优化很明显,至少减少了百分之50的耗时,不知道这里为啥就优化了那么一丢丢哎!!

三. 如何准确统计query耗时?

如果我们要统计一个query查询操作的耗时,直接在query相关的sql语句执行前和执行后统计耗时是不准确的,因为的查询的耗时还要包括Cursor的读取。最正确的方法是在Cursor执行close()操作后再去统计查询耗时。

所以这里的优化操作是,在基类查询方法向外部暴漏Cursor对象时,对这个暴漏的Cursor借助静态代理的思想增加一层包装,这样就可以监听到Cursorclose()方法执行时机,在这个方法回调中插入我们最终的 耗时统计代码:

  1. 首先创建一个静态代理对象,并支持传入close()监听回调
class CursorProxy(
    private val cursor: Cursor,
    private val mCloseCallback: () -> Unit
) : Cursor by cursor {

    override fun close() {
        cursor.close()
        mCloseCallback()
    }
}

这里我们借助kotlin的by类委托关键字实现了更为简单的静态代理,以及通过mCloseCallback这个函数类型传入外部的监听回调代码,比如这里的最终耗时统计代码。

  1. 在基类查询方法对外暴漏CursorProxy
public Cursor query(String sql) {
    //耗时统计开启
    Cursor cursor = db.rawQuery(sql, null);

    return new CursorProxy(cursor, new Function0<Unit>() {
        @Override
        public Unit invoke() {
            //耗时统计结束代码
            return null;
        }
    });
}

总结

本篇文章主要是讲解了sql操作的一些优化操作,减少执行耗时,同时也给出了准确统计query查询耗时的优化方案,本篇文章不难理解,希望能对你有所帮助。