Android最全Kotlin 协程和 Android SQLite API 中的线程模型(1),2024年最新面试的总结语

最后:学习总结——Android框架体系架构知识脑图(纯手绘xmind文档)

学完之后,若是想验收效果如何,其实最好的方法就是可自己去总结一下。比如我就会在学习完一个东西之后自己去手绘一份xmind文件的知识梳理大纲脑图,这样也可方便后续的复习,且都是自己的理解,相信随便瞟几眼就能迅速过完整个知识,脑补回来。

下方即为我手绘的Android框架体系架构知识脑图,由于是xmind文件,不好上传,所以小编将其以图片形式导出来传在此处,细节方面不是特别清晰。但可给感兴趣的朋友提供完整的Android框架体系架构知识脑图原件(包括上方的面试解析xmind文档)

除此之外,前文所提及的Alibaba珍藏版 Android框架体系架构 手写文档以及一本 《大话数据结构》 书籍等等相关的学习笔记文档,也皆可分享给认可的朋友!

——感谢大家伙的认可支持,请注意:点赞+点赞+点赞!!!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

try {

// 协程可以在与调度器(这里就是 Dispatchers.IO)相关联的任何线程上绑定并继续执行。同时,由于事务也是在 IO-Thread-1 中开始的,因此我们可能恰好可以成功执行查询。

moneyDao.decrease(accountA, amount) //挂起函数

// 如果协程又继续在 IO-Thread-2 上执行,那么下列操作数据库的代码可能会引起死锁,因为它需要等到 IO-Thread-1 的线程执行结束后才可以继续。

moneyDao.increase(accountB, amount) //挂起函数

database.setTransactionSuccessful() //永远不会执行这一行

} finally {

database.endTransaction() //永远不会执行这一行

}

}

}

Android 的 SQLite 事务受制于单个线程


上述代码中的问题在于 Android 的 SQLite 事务是受制于单个线程的。当一个正在进行的事务中的某个查询在当前线程中被执行时,它会被视为是该事务的一部分并允许继续执行。但当这个查询在另外一个线程中被执行时,那它就不再属于这个事务的一部分了,这样的话就会导致这个查询被阻塞,直到事务在另外一个线程执行完成。这也是 beginTransaction 和 endTransaction 这两个 API 能够保证原子性的一个前提。当数据库的事务操作都是在一个线程上完成的,这样的 API 不会有任何问题,但是使用协程之后问题就来了,因为协程是不绑定在任何特定的线程上的。也就是说,问题的根源就是在协程挂起之后会继续执行所绑定的那个线程,而这样是不能保证和挂起之前所绑定的线程是同一个线程。

在协程中使用数据库事务操作可能会引起死锁

简单实现


为了解决 Android SQLite 的这个限制,我们需要一个类似于 runInTransaction 这样可以接受挂起代码块的 API,这个 API 实现起来就像写一个单线程的调度器一样:

suspend fun RoomDatabase.runInTransaction(

block: suspend () -> T

): T = withContext(newSingleThreadContext(“DB”)) {

beginTransaction()

try {

val result = block.invoke(this)

setTransactionSuccessful()

return@runBlocking result

} finally {

endTransaction()

}

}

以上实现仅仅是个开始,但是当在挂起代码块中使用另一个调度器的话就会出问题了:

// 一个很简单的退税函数

suspend fun sendTaxRefund(federalAccount: String, taypayerList: List) {

database.runInTransaction {

val refundJobs = taypayerList.map { taxpayer ->

coroutineScope {

// 并行去计算退税金额

async(Dispatchers.IO) {

val amount = irsTool.calculateRefund(taxpayer)

moneyDao.decrease(federalAccount, amount)

moneyDao.increase(taxpayer.account, amount)

}

}

}

// 等待所有计算任务结束

refundJobs.joinAll()

}

}

因为接收的参数是一个挂起代码块,所以这部分代码就有可能使用一个不同的调度器来启动子协程,这样就会导致执行数据库操作的是另外的一个线程。因此,一个比较好的实现是应该允许使用类似于 async、launch 或 withContext 这样的标准协程构造器。而在实际应用中,只有数据库操作才需要被调度到单事务线程。

介绍 withTransaction


为了解决上面的问题,我们构建了 withTransaction API,它模仿了 withContext API,但是提供了专为安全执行 Room 事务而构建的协程上下文,您可以按照如下方式编写代码:

fun transferMoney(

accountA: String,

accountB: String,

amount: Int

) = GlobalScope.launch(Dispatchers.Main) {

roomDatabase.withTransaction {

moneyDao.decrease(accountA, amount)

moneyDao.increase(accountB, amount)

}

Toast.makeText(context, “Transfer Completed.”, Toast.LENGTH_SHORT).show()

}

在深入研究 Room withTransaction API 的实现前,让我们先回顾一下已提到的一些协程的概念。CoroutineContext 包含了需要对协程任务进行调度的信息,它携带了当前的 CoroutineDispatcherJob 对象,以及一些额外的数据,当然也可以对它进行扩展来使其包含更多信息。CoroutineContext 的一个重要特征是它们被同一协程作用域下的子协程所继承,比如 withContext 代码块的作用域。这一机制能够让子协程继续使用同一个调度器,或在父协程被取消时,它们会被一起取消。本质上,Room 提供的挂起事务 API 会创建一个专门的协程上下文来在同一个事务作用域下执行数据库操作。

withTransaction API 在上下文中创建了三个关键元素:

  • 单线程调度器,用于执行数据库操作;

  • 上下文元素,帮助 DAO 函数判断其是否处在事务中;

  • ThreadContextElement,用来标记事务协程中所使用的调度线程。

事务调度器


CoroutineDispatcher 会决定协程该绑定到哪个线程中执行。比如,Dispatchers.IO 会使用一个共享线程池分流执行那些会发生阻塞的操作,而 Dispatchers.Main 会在 Android 主线程中执行协程。由 Room 创建的事务调度器能够从 Room 的 Executor 获取单一线程,并将事务分发给该线程,而不是分发给一个随意创建的新线程。这一点很重要,因为 executor 可以由用户来配置,并且可作为测试工具使用。在事务开始时,Room 会获得 executor 中某个线程的控制权,直到事务结束。在事务执行期间,即使调度器因子协程发生了变化,已执行的数据库操作仍会被分配到该事务线程上。

获取一个事务线程并不是一个阻塞操作,它也不应该是阻塞操作,因为如果没有可用线程的话,应该执行挂起操作,然后通知调用方,避免影响其他协程的执行。它还会将一个 runnable 插入队列,然后等待其运行,这也是线程可运行的一个标志。suspendCancellableCoroutine 函数为我们搭建了连接基于回调的 API 和协程之间的桥梁。在这种情况下,一旦之前入队列的 runnable 执行了,就代表着一个线程可用,我们会使用 runBlocking 启动一个事件循环来获取此线程的控制权。然后 runBlocking 所创建的调度器会将要执行的代码块分发给已获得的线程。另外,Job 被用来挂起和保持线程的可用性,直到事务执行完成为止。要注意的是,一旦协程被取消了或者是无法获取到线程,就要有防范措施。获取事务线程的相关代码如下:

/**

*构建并返回一个 [ContinuationInterceptor] 用来将协程分发到获取到的线程中,并执行事务。[controlJob] 用来通过取消任务来控制线程的释放。

*/

private suspend fun Executor.acquireTransactionThread(

controlJob: Job

): ContinuationInterceptor = suspendCancellableCoroutine { continuation ->

continuation.invokeOnCancellation {

// 当我们在等待获取到可用线程时,如果失败了或者任务取消,我们是不能够停止等待这一动作的,但我们可以取消 controlJob,这样一旦获取到控制权,很快就会被释放。

controlJob.cancel()

}

try {

execute {

// runBlocking 创建一个 event loop 来执行协程中的任务代码

runBlocking {

// 获取到线程后,通过返回有 runBlocking 创建的拦截器来恢复 suspendCancellableCoroutine,拦截器将会被用来拦截和分发代码块到获取的线程中

continuation.resume(coroutineContext[ContinuationInterceptor]!!)

// 挂起 runBlocking 协程,直到 controlJob 完成。由于协程是空的,所以这将会阻止 runBlocking 立即结束。

controlJob.join()

}

}

} catch (ex: RejectedExecutionException) {

// 无法获取线程,取消协程

continuation.cancel(

IllegalStateException(

“Unable to acquire a thread to perform the transaction.”, ex)

)

}

}

事务上下文元素


有了调度器后,我们就可以创建事务中的元素来添加到上下文中,并保持着对调度器的引用。如果在事务作用域内调用了 DAO 函数,就可以把 DAO 函数重新路由到相应的线程中。我们创建的事务元素如下:

internal class TransactionElement(

private val transactionThreadControlJob: Job,

internal val transactionDispatcher: ContinuationInterceptor

) : CoroutineContext.Element {

// Singleton key 用于检索此上下文中的 element

companion object Key : CoroutineContext.Key

override val key: CoroutineContext.Key

get() = TransactionElement

/**

*这个 element 用来统计事务数量(包含嵌套事务)。调用 [acquire] 来增加计数,调用 [release] 来减少计数。如果在调用 [release] 时计数达到 0,则事务被取消,事务线程会被释放

*/

private val referenceCount = AtomicInteger(0)

fun acquire() {

结语

网上高级工程师面试相关文章鱼龙混杂,要么一堆内容,要么内容质量太浅, 鉴于此我整理了上述安卓开发高级工程师面试题以及答案。希望帮助大家顺利进阶为高级工程师。
目前我就职于某大厂安卓高级工程师职位,在当下大环境下也想为安卓工程师出一份力,通过我的技术经验整理了面试经常问的题,答案部分是一篇文章或者几篇文章,都是我认真看过并且觉得不错才整理出来。

大家知道高级工程师不会像刚入门那样被问的问题一句话两句话就能表述清楚,所以我通过过滤好文章来帮助大家理解。

1307页字节跳动Android面试真题解析火爆全网,完整版开放下载

现在都说互联网寒冬,其实只要自身技术能力够强,咱们就不怕!我这边专门针对Android开发工程师整理了一套【Android进阶学习视频】、【全套Android面试秘籍】、【Android知识点PDF】。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

232606464)]

现在都说互联网寒冬,其实只要自身技术能力够强,咱们就不怕!我这边专门针对Android开发工程师整理了一套【Android进阶学习视频】、【全套Android面试秘籍】、【Android知识点PDF】。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值