当前位置:首页 > 问答 > 正文

让你轻松点,主线程里怎么才能顺利拿到数据库数据,不用卡顿也不复杂

让你轻松点,主线程里怎么才能顺利拿到数据库数据,不用卡顿也不复杂

核心就一句话:别在主线程里直接干重活儿,尤其是等数据库这种可能慢吞吞的活儿。 你想啊,主线程就像是餐厅里唯一的服务员,他既要招呼客人点餐(响应用户点击),又要跑去后厨端菜(更新界面),如果你让他点完餐后,自己蹲在后厨门口等厨师炒菜(在主线程里查询数据库),那外面的客人就全干瞪眼了,整个餐厅就跟卡死了没区别,用户会觉得你的应用“一顿一顿的”,甚至直接报错闪退。

正确的做法是:让这个服务员(主线程)雇个帮手(后台线程/工作线程)去后厨等菜。 服务员只需要告诉帮手“去拿一份宫保鸡丁”,然后就可以继续去招呼其他客人了,等帮手从后厨把菜端出来(数据库查询完成),再喊一声“服务员,你的宫保鸡丁好了!”,服务员再过来把菜送给客人(在主线程更新界面)。

这个模式,在Android开发里,谷歌官方推荐用 Kotlin的协程(Coroutines) 来做,因为它写起来特别简单直观,不像以前的老办法那么啰嗦,下面我就用最直白的方式说说怎么用协程搞定这件事。

第一步:准备工作(引入协程支持)

你得确保你的项目里已经加上了协程的库,这个就像你要用微波炉,得先插上电一样,具体怎么加,你去查一下官方文档(来源:Android开发者官网 - 协程设置指南)就行,很简单,在build.gradle文件里加一两行代码。

让你轻松点,主线程里怎么才能顺利拿到数据库数据,不用卡顿也不复杂

第二步:把数据库操作放进“后台小房间”

你的数据库操作,比如从数据库里查询一个用户列表,原来可能是直接写在一个函数里的,你需要用协程把这个函数“包”一下,怎么包呢?用 withContext(Dispatchers.IO)

举个例子: 假设你原来有个函数叫 getAllUsers(),它会直接返回用户列表,但它在主线程里跑会卡。

用协程改造后,它看起来大概是这样的:

让你轻松点,主线程里怎么才能顺利拿到数据库数据,不用卡顿也不复杂

suspend fun getAllUsers(): List<User> {
    return withContext(Dispatchers.IO) {
        // 这一大块代码会在专门的IO线程池里运行,不会卡主线程
        database.userDao().getAll()
    }
}

你看,就多了两行东西:

  1. 在函数前面加了个 suspend 关键字,这个单词是“挂起”的意思,意思就是这个函数是可以被“暂停”一下的,等后台活干完了再继续,这是告诉协程:“我这个函数可能要花点时间哦。”
  2. 函数体用 withContext(Dispatchers.IO) 包起来。Dispatchers.IO 是谷歌准备好的一个专门用于输入/输出(比如网络请求、读写数据库、操作文件)的线程池,你把它扔进去,它就会自动找个合适的后台线程去执行里面的代码。

这样一来,getAllUsers() 自己就变成了一个“后台任务包”。

第三步:在主线程里“点火发射”这个后台任务

现在你有了一个 getAllUsers() 的后台任务包,你怎么在主线程(比如一个按钮的点击事件里)使用它呢?你不能直接调用它,因为 suspend 函数必须在协程作用域里才能被调用。

让你轻松点,主线程里怎么才能顺利拿到数据库数据,不用卡顿也不复杂

你需要一个“协程发射器”,最常用、也最推荐在Android的界面相关组件(如Activity、Fragment)里用的是 lifecycleScope

lifecycleScope 是跟你的界面生命周期绑定的,如果界面关闭了,它就会自动取消所有由它发起的协程,避免内存泄漏,这非常贴心。

在你的按钮点击事件里,这样写:

button.setOnClickListener {
    // 在 lifecycleScope 里启动一个协程
    lifecycleScope.launch {
        // 在这个花括号里,你就是在一个协程作用域里了,可以调用 suspend 函数
        // 显示一个加载中的圆圈,告诉用户正在干活
        showLoadingSpinner()
        try {
            // 关键一步:调用那个被 suspend 标记的函数
            // 注意:虽然这行代码写起来像是“等待”,但主线程并不会卡在这里傻等!
            val userList = getAllUsers()
            // 当上一行代码执行完,意味着数据已经从数据库取回来了
            // 协程会自动帮我们切回到主线程来执行后面的代码!
            // 所以这里可以直接安全地更新界面
            updateUserListOnScreen(userList)
        } catch (e: Exception) {
            // 如果取数据出错了(比如数据库挂了),也能在这里捕获到
            showErrorToast("获取数据失败啦!")
        } finally {
            // 无论成功失败,都隐藏加载圆圈
            hideLoadingSpinner()
        }
    }
}

这个过程的神奇之处在于:

  • 当你调用 getAllUsers() 时,协程会“挂起”当前这个协程(注意,是挂起协程,不是卡住主线程)。
  • 然后主线程就自由了,爱干嘛干嘛去,完全不受影响,界面依然流畅。
  • 后台线程默默地去数据库拿数据。
  • 数据拿回来后,withContext(Dispatchers.IO) 块结束,协程会聪明地自动切换回原先的线程(也就是主线程),然后继续执行后面的 updateUserListOnScreen(userList) 来更新界面。

你完全不用自己操心线程切换的问题,协程都帮你处理好了,代码写起来就像是在写顺序执行的代码一样,非常顺溜。

轻松不卡顿的三步曲:

  1. 定义后台任务:用 suspend 关键字和 withContext(Dispatchers.IO) 把你的数据库操作函数包装成一个“后台任务包”。
  2. 启动任务:在需要的地方(如按钮点击事件),使用 lifecycleScope.launch 启动一个协程,然后在这个协程里调用你的“后台任务包”。
  3. 处理结果:在 launch 的花括号里,任务包之后的代码会自动回到主线程执行,你就在这里安心更新界面,用 try-catch 处理好异常。

这样做,你的应用主线程就永远轻松愉快,只负责跟用户交互和更新界面,所有累活都交给后台帮手去干,用户感觉到的就是应用的流畅和响应迅速,这个方法就是目前最主流、最不复杂的最佳实践(来源:基于Android官方开发指南中关于后台任务和协程的推荐)。