今天我们来做一件奇葩的事情,能不能自己写一个鲁大师?这貌似是一件不可思议的事情。写鲁大师功能太多了,今天我们单写一个Android数据库读写性能的测试。
做这件事之前,我们首先得有个ORM数据库框架,要不然手写一大堆SQL语句也是一件麻烦事。用什么ORM框架呢?Room?GreenDao?Realm?还是Ormlite?都不是。今天的主角是我原创且独立开发的一款ORM框架,考虑不完善的地方,随便批评,虚心接受。
老规矩,先看一下效果,再看代码。
效果展示
主要代码
先看下数据库写入测试的代码。里面包含了三种操作,数据插入、数据修改和数据删除,分别执行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"
}
}
代码地址
主要缺陷
- 暂不支持char和byte类型。
- 复杂数据类型性能略有降低。
对于第一个缺陷,影响并不是很大,首先这两个数据类型用得比较少,其次char可以用String替代,byte的替代选择就多了。对于第二个缺陷,是由于动态代理中的反射导致的,对于ORM框架提升的开发效率来说,这个也可以忽略不计。由于dcache是一个Android数据缓存框架,侧重点在于缓存的架构和算法逻辑。如果你对内置的ORM框架性能不满意,这里也提供了整合你自己的ORM框架的解决方案。
优势
优势当然是使用内置的包体积更小,开发效率高了。这样可以快速扫清业务障碍。对于性能,可以后期进行优化。同时,它也整合了主流网络库Retrofit,无缝衔接,学习成本低。
写ORM框架不是为了造轮子而造轮子
很多人会说,市面上那么多优秀的ORM框架,你再造一个轮子,是不是闲得没事做?我给的答复是,原生的再差那毕竟也是原生的。那古代那么多手足相残是不是都是否定原生的地位导致的。质量怎么样是一回事,有没有则是另一回事。ORM框架为数据的缓存框架提供底层支持。接下来看我的数据分页缓存框架。
分页缓存的使用
以展示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框架还是能满足大部分场景的需求的,并非完全不堪入目的。