今天咱们来自己写代码测试手机数据库读写性能

322 阅读5分钟

今天我们来做一件奇葩的事情,能不能自己写一个鲁大师?这貌似是一件不可思议的事情。写鲁大师功能太多了,今天我们单写一个Android数据库读写性能的测试。

做这件事之前,我们首先得有个ORM数据库框架,要不然手写一大堆SQL语句也是一件麻烦事。用什么ORM框架呢?Room?GreenDao?Realm?还是Ormlite?都不是。今天的主角是我原创且独立开发的一款ORM框架,考虑不完善的地方,随便批评,虚心接受。

老规矩,先看一下效果,再看代码。

效果展示

SVID_20240904_215037_1.gif

主要代码

先看下数据库写入测试的代码。里面包含了三种操作,数据插入、数据修改和数据删除,分别执行10000次。数据包含boolean、short、int、long、float和double类型,随机生成。计时我们采用操作的结束时间减去开始时间,貌似也只能这么算,用其他的框架也都是这么算的。有人问,为什么是net? 这个不是重点,我们就地取材一下,反正名字无关紧要,重要的是能实现功能,对吧。net作用域+request高阶函数可以做简单的阻塞操作。我们在request中开一个线程来执行耗时操作,执行完成调用releaseLock结束阻塞回来执行下一个request。一个接一个执行完成后,得出总的耗时。

package com.example.dcache.orm

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
import com.example.dcache.R
import dora.db.builder.WhereBuilder
import dora.db.dao.DaoFactory
import dora.http.DoraHttp.net
import dora.http.DoraHttp.request
import kotlin.random.Random

class OrmWriteTestActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_orm_write_test)
        net {
            writeObjects()
            updateObjects()
            deleteObjects()
        }
    }

    private fun getRandomObject(): TestCaseModel2 {
        val model = TestCaseModel2()
        model.booleanVal = Random.nextBoolean()
        model.shortVal = (Random.nextInt(65535) - 32767).toShort()
        model.intVal = Random.nextInt()
        model.longVal = Random.nextLong()
        model.floatVal = Random.nextFloat()
        model.doubleVal = Random.nextDouble()
        return model
    }

    private suspend fun writeObjects() {
        val tvOrmInsertResult = findViewById<TextView>(R.id.tvOrmInsertResult)
        tvOrmInsertResult.text = "开始执行写入数据操作"
        val time = request {
            Thread(Runnable {
                val start = System.currentTimeMillis()
                for (i in 0 until 10000) {
                    val model = getRandomObject()
                    model.index = i
                    DaoFactory.getDao(TestCaseModel2::class.java).insert(model)
                    runOnUiThread {
                        tvOrmInsertResult.text = "正在写入第${i+1}条数据,${model.toString()}"
                    }
                }
                val end = System.currentTimeMillis()
                it.releaseLock(end - start)
            }).start()
        }
        tvOrmInsertResult.text = "数据写入测试完成,共耗时${time}ms"
    }

    private suspend fun updateObjects() {
        val tvOrmUpdateResult = findViewById<TextView>(R.id.tvOrmUpdateResult)
        tvOrmUpdateResult.text = "开始执行更新数据操作"
        val time = request {
            Thread(Runnable {
                val start = System.currentTimeMillis()
                for (i in 0 until 10000) {
                    val model = getRandomObject()
                    model.index = i
                    DaoFactory.getDao(TestCaseModel2::class.java).update(
                        WhereBuilder.create().addWhereEqualTo("data_index", i),
                        model)
                    runOnUiThread {
                        tvOrmUpdateResult.text = "正在更新第${i+1}条数据,${model.toString()}"
                    }
                }
                val end = System.currentTimeMillis()
                it.releaseLock(end - start)
            }).start()
        }
        tvOrmUpdateResult.text = "数据更新测试完成,共耗时${time}ms"
    }

    private suspend fun deleteObjects() {
        val tvOrmDeleteResult = findViewById<TextView>(R.id.tvOrmDeleteResult)
        tvOrmDeleteResult.text = "开始执行删除数据操作"
        val time = request {
            Thread(Runnable {
                val start = System.currentTimeMillis()
                for (i in 0 until 10000) {
                    val ok = DaoFactory.getDao(TestCaseModel2::class.java).delete(WhereBuilder.create().addWhereEqualTo("data_index", i))
                    runOnUiThread {
                        tvOrmDeleteResult.text = "正在删除第${i+1}条数据,删除结果:${ok}"
                    }
                }
                val end = System.currentTimeMillis()
                it.releaseLock(end - start)
            }).start()
        }
        tvOrmDeleteResult.text = "数据删除测试完成,共耗时${time}ms"
    }
}

数据库读取的思路大同小异,测试数据库读取10000次的耗时。

package com.example.dcache.orm

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
import com.example.dcache.R
import dora.db.builder.WhereBuilder
import dora.db.dao.DaoFactory
import dora.http.DoraHttp
import dora.http.DoraHttp.net
import kotlin.random.Random

class OrmReadTestActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_orm_read_test)
        net {
            writeObjects()
            readObjects()
            DaoFactory.getDao(TestCaseModel2::class.java).deleteAll()
        }
    }

    private fun getRandomObject(): TestCaseModel2 {
        val model = TestCaseModel2()
        model.booleanVal = Random.nextBoolean()
        model.shortVal = (Random.nextInt(65535) - 32767).toShort()
        model.intVal = Random.nextInt()
        model.longVal = Random.nextLong()
        model.floatVal = Random.nextFloat()
        model.doubleVal = Random.nextDouble()
        return model
    }

    private suspend fun writeObjects() {
        val tvOrmReadResult = findViewById<TextView>(R.id.tvOrmReadResult)
        tvOrmReadResult.text = "正在准备数据"
        val time = DoraHttp.request {
            Thread(Runnable {
                val start = System.currentTimeMillis()
                for (i in 0 until 10000) {
                    val model = getRandomObject()
                    model.index = i
                    DaoFactory.getDao(TestCaseModel2::class.java).insert(model)
                    runOnUiThread {
                        tvOrmReadResult.text = "正在准备第${i + 1}条数据,${model.toString()}"
                    }
                }
                val end = System.currentTimeMillis()
                it.releaseLock(end - start)
            }).start()
        }
        tvOrmReadResult.text = "数据准备完成"
    }

    private suspend fun readObjects() {
        val tvOrmReadResult = findViewById<TextView>(R.id.tvOrmReadResult)
        tvOrmReadResult.text = "开始执行读取数据操作"
        val time = DoraHttp.request {
            Thread(Runnable {
                val start = System.currentTimeMillis()
                for (i in 0 until 10000) {
                    val model = DaoFactory.getDao(TestCaseModel2::class.java).selectOne(
                        WhereBuilder.create().addWhereEqualTo(
                            "data_index", i))
                    runOnUiThread {
                        tvOrmReadResult.text = "正在读取第${i + 1}条数据,${model.toString()}"
                    }
                }
                val end = System.currentTimeMillis()
                it.releaseLock(end - start)
            }).start()
        }
        tvOrmReadResult.text = "数据读取测试完成,共耗时${time}ms"
    }
}

代码地址

github.com/dora4/DoraC…

主要缺陷

  • 暂不支持char和byte类型。
  • 复杂数据类型性能略有降低。

对于第一个缺陷,影响并不是很大,首先这两个数据类型用得比较少,其次char可以用String替代,byte的替代选择就多了。对于第二个缺陷,是由于动态代理中的反射导致的,对于ORM框架提升的开发效率来说,这个也可以忽略不计。由于dcache是一个Android数据缓存框架,侧重点在于缓存的架构和算法逻辑。如果你对内置的ORM框架性能不满意,这里也提供了整合你自己的ORM框架的解决方案。

优势

优势当然是使用内置的包体积更小,开发效率高了。这样可以快速扫清业务障碍。对于性能,可以后期进行优化。同时,它也整合了主流网络库Retrofit,无缝衔接,学习成本低。

写ORM框架不是为了造轮子而造轮子

很多人会说,市面上那么多优秀的ORM框架,你再造一个轮子,是不是闲得没事做?我给的答复是,原生的再差那毕竟也是原生的。那古代那么多手足相残是不是都是否定原生的地位导致的。质量怎么样是一回事,有没有则是另一回事。ORM框架为数据的缓存框架提供底层支持。接下来看我的数据分页缓存框架。

SVID_20240904_225619_1.gif

分页缓存的使用

以展示banner图为例,后端返回给用户端的数据是一次性全部返回的,因为就那么几条数据。但是以系统管理员身份登录的时候,另外的界面则应该显示所有数据,包括开关是关闭不展示给用户端看的。这个时候应该把所有数据都缓存下来,如果离线了,能以两种身份读取离线缓存的数据。即可以一次性全部获取,也可以分页。

   package com.dorachat.dorachat.repository
   
   import android.content.Context
   import android.os.Build
   import androidx.annotation.RequiresApi
   import com.dorachat.dorachat.common.AppConfig.Companion.PRODUCT_NAME
   import com.dorachat.dorachat.http.ApiResult
   import com.dorachat.dorachat.http.PageDTO
   import com.dorachat.dorachat.http.service.HomeService
   import com.dorachat.dorachat.model.BannerInfo
   import com.dorachat.dorachat.model.request.home.ReqProductByPage
   import dora.cache.DoraPageListCallback
   import dora.cache.data.adapter.ListResultAdapter
   import dora.cache.data.adapter.PageListResultAdapter
   import dora.cache.data.fetcher.OnLoadStateListener
   import dora.cache.factory.DatabaseCacheHolderFactory
   import dora.cache.repository.DoraPageDatabaseCacheRepository
   import dora.cache.repository.ListRepository
   import dora.db.builder.Condition
   import dora.db.builder.QueryBuilder
   import dora.http.retrofit.RetrofitManager.getService
   import retrofit2.Callback
   import javax.inject.Inject
   
   @ListRepository
   class BannerRepository @Inject constructor(context: Context) :
       DoraPageDatabaseCacheRepository<BannerInfo>(context) {
   
       private var isAdmin: Boolean = false
   
       fun setAdmin(isAdmin: Boolean) : BannerRepository {
           this.isAdmin = isAdmin
           return this
       }
   
       override fun query(): Condition {
           return if (isAdmin) {
               super.query()
           } else {
               // 不分页,返回全部数据
               QueryBuilder.create().toCondition()
           }
       }
   
       override fun onLoadFromNetwork(
           callback: DoraPageListCallback<BannerInfo>,
         	// 成功不用回调成功,框架会自动帮你回调。但错误要回调错误,让界面层显示错误,比如在解析到某个字段时,读取到特定
           // 的标识认定为失败,不过这种情况不常用。
           listener: OnLoadStateListener?
       ) {
           if (isAdmin) {
               val req = ReqProductByPage(PRODUCT_NAME, getPageSize(), getPageNo())
               getService(HomeService::class.java).getBanners(req.toRequestBody()).enqueue(
                   PageListResultAdapter<BannerInfo, ApiResult<BannerInfo>>(callback)
                           as Callback<ApiResult<PageDTO<BannerInfo>>>
               )
           } else {
               getService(HomeService::class.java).getBanners(PRODUCT_NAME).enqueue(
                   PageListResultAdapter<BannerInfo, ApiResult<BannerInfo>>(callback)
                           as Callback<ApiResult<MutableList<BannerInfo>>>
               )
           }
       }
   
       override fun createCacheHolderFactory(): DatabaseCacheHolderFactory<BannerInfo> {
           return DatabaseCacheHolderFactory(BannerInfo::class.java)
       }
   }

我们再看一下调用处怎么调用它。

   // UI层
   binding.slBannerInfoList.setOnSwipeListener(object : SwipeLayout.OnSwipeListener {
   
       override fun onRefresh(swipeLayout: SwipeLayout) {
       }
   
       override fun onLoadMore(swipeLayout: SwipeLayout) {
           bannerRepository.onLoadMore {
               swipeLayout.loadMoreFinish(if (it) SwipeLayout.SUCCEED else SwipeLayout.FAIL)
           }
       }
   })
   // 数据层
   bannerRepository.observeData(this, object : DoraPageDatabaseCacheRepository.AdapterDelegate<BannerInfo> {
   
       override fun addData(data: MutableList<BannerInfo>) {
           adapter.addData(data)
           binding.emptyLayout.showContent()
       }
   
       override fun setList(data: MutableList<BannerInfo>) {
           adapter.setList(data)
           binding.emptyLayout.showContent()
       }
   })
   // 使用默认的每页大小,也就是每页10条数据,加载第一页
   bannerRepository.setAdmin(true).onRefresh()

另外对于数据总条数是不断变化的场景,比如聊天消息,我们通常采用对数据进行快照的方式,也就是指定数据截止的时间戳。在这个时间节点之前的数据,我们可以认为是固定的大小。这样接口就需要多传一个timestamp的参数了,对于缓存也是一样的,也需要考虑这个timestamp进行数据的过滤。

总结

ORM框架是数据缓存的基础,所以我需要自己造一个“轮子”。没有这个ORM框架的基础,写一个网络数据缓存框架的难度就会更大,因为跟底层ORM框架的衔接架构会更难设计。内置的ORM框架还是能满足大部分场景的需求的,并非完全不堪入目的。