在本文中,我将讨论协程如何工作以及如何创建协程。 在顺序,并行执行中考虑应用程序。 让我们谈谈错误处理,调试以及测试协程的方法。 最后,我将总结并讨论应用此方法后仍然留下的印象。
这篇文章是根据我关于
MBLT DEV 2018的报告的材料编写的,该材料在帖子末尾-链接到视频。
风格一致
图 2.1Corutin开发人员的目的是什么? 他们希望异步编程尽可能简单。 没有比使用该语言的语法结构“逐行”执行代码容易的了:try-catch-finally,循环,条件语句等等。
让我们考虑两个函数。 每个都在自己的线程上执行(图2.1)。 第一个在线程
B上执行并返回一些结果
dataB ,然后我们需要将此结果传递给第二个函数,该函数以
dataB作为参数并且已经在线程
A上运行
。 使用协程,我们可以编写我们的代码,如图2所示。 2.1。 考虑如何实现这一目标。
函数
longOpOnB,longOpOnA-所谓的
suspend-函数,在此之前释放线程,并在完成工作后再次变得繁忙。
为了在相对于被调用函数不同的线程中实际执行这两个函数,同时保持“一致”的编写代码风格,我们必须将它们浸入协程环境中。
这是通过使用所谓的Coroutine Builder创建协程来完成的。 在图中,这是
launch ,但是还有其他一些,例如
async ,
runBlocking 。 稍后再讨论。
最后一个参数是在协程的上下文中执行的代码块:调用暂挂函数,这意味着上述所有行为仅在协程或其他暂挂函数的上下文中才有可能。
Coroutine Builder方法中还有其他参数,例如,启动类型,将在其中执行块的线程等。
生命周期管理
Coroutine Builder将返回值作为返回值提供给我们-Job类的子类(图2.2)。 有了它,我们可以管理corutin的生命周期。
从
start()方法
开始 ,以
cancel()方法
取消 ,使用
join( )方法等待作业完成,订阅作业完成事件等等。
图 2.2流量变化
您可以通过更改负责调度的协程的上下文元素来更改协程执行流程。 (图2.3)
例如,corutin 1将在
UI线程中执行,而corutin 2将在从
Dispatchers.IO池中获取的线程中执行。
图2.3协程库还提供了一个
带有context(CoroutineContext)的暂停函数,您可以使用该函数在协程上下文中在线程之间进行切换。 因此,线程之间的跳转可能非常简单:
图 2.4。我们在UI线程1上启动协程→显示负载指示器→切换到工作线程2,释放主要线程→我们在UI线程中执行无法执行的长操作→将结果返回UI线程3→已经在那里工作使用它,呈现接收到的数据并隐藏加载指示符。
到目前为止,看起来很舒服,继续前进。
挂起功能
在最常见的示例中考虑corutin的工作-使用Retrofit 2库处理网络请求。
我们需要做的第一件事是将
回调调用转换为
暂停函数,以利用协同程序功能:
图 2.5为了控制协程的状态,该库提供了
suspendXXXXCoroutine形式的函数,该函数提供了一个参数,用于实现
Continuation接口,使用
resumeWithException和
resume方法,在发生错误和成功的情况下,我们可以分别恢复协程。
接下来,我们将弄清楚调用resumeWithException方法时发生的情况,首先,请确保我们需要以某种方式取消网络请求调用。
挂起功能。 取消通话
要取消与释放未使用资源有关的调用和其他操作,在实现暂停功能时,可以使用
开箱即用的
suspendCancellableCoroutine方法(图2.6)。 在这里,block参数已经实现了
CancellableContinuation接口,其中的其他方法之一
-invokeOnCancellation-允许您注册错误或成功的协程取消事件。 因此,在这里也有必要取消方法调用。
图 2.6在UI中显示更改
现在已经为网络请求准备了暂停功能,您可以在Coroutine UI流中按顺序使用其调用,而在执行请求期间,该流将是免费的,而改型流将用于该请求。
因此,我们实现了与UI流异步的行为,但是我们以一致的方式编写了行为(图2.6)。
如果在收到答案后需要进行艰苦的工作,例如将接收到的数据写入数据库,则可以使用
withContext在回流流池上轻松执行此功能(如已显示),然后在UI上继续执行而无需一行代码。
图 2.7不幸的是,这并不是我们进行应用程序开发所需的全部。 考虑错误处理。
错误处理:尝试最终捕获。 取消协程:CancellationException
未在协程内部捕获的异常被视为未处理,并且可能导致应用程序崩溃。 除正常情况外,通过在调用suspend函数的相应行上使用
resumeWithException方法恢复协程,
也会引发异常。 在这种情况下,作为参数传递的异常将保持不变。 (图2.8)
图 2.8对于异常处理,可以使用标准的try try final语言构造。 现在,可以在UI中显示错误的代码采用以下形式:
图 2.9在取消协程的情况下(可以通过调用Job#cancel方法来实现),将引发
CancellationException 。 默认情况下会处理此异常,并且不会导致崩溃或其他负面后果。
但是,当使用
try / catch构造时,它将被捕获在
catch块中 ,如果您只想处理真正的“错误”情况,则需要考虑它。 例如,当可以“取消”请求或错误日志记录时,提供UI中的错误处理。 在第一种情况下,该错误将显示给用户,尽管该错误实际上并不存在,而在第二种情况下,将记录无用的异常并使报告混乱。
要忽略取消协程的情况,您需要稍微修改一下代码:
图 2.10错误记录
考虑异常异常堆栈跟踪。
如果直接在协程代码块中抛出异常(图2.11),则堆栈跟踪看起来很整洁,只有少量协程调用,它正确地指示了有关异常的行和信息。 在这种情况下,您可以从堆栈跟踪中轻松了解在何处,在哪个类和哪个函数中引发了异常。
图 2.11但是,通常,传递给
暂挂函数的
resumeWithException方法的
异常不包含有关发生协程的信息。 例如(图2.12),如果您从先前实现的暂停函数中恢复协程,并且异常与上一个示例相同,则堆栈跟踪将不会提供有关在何处专门查找错误的信息。
图 2.12要了解有异常恢复的协程,可以使用
CoroutineName上下文
元素 。 (图2.13)
CoroutineName元素用于调试,将协程的名称传递到其中,您可以将其提取到挂起函数中,例如,补充异常消息。 也就是说,至少很清楚在哪里可以找到错误。
这种方法仅在以下情况下才有效:
图 2.13错误记录。 异常处理程序
要更改特定协程的异常日志记录,可以设置自己的ExceptionHandler,这是协程上下文的元素之一。 (图2.14)
处理程序必须实现
CoroutineExceptionHandler接口。 对协程上下文使用重写的+运算符,您可以用自己的替换标准异常处理程序。 未处理的异常将属于
handleException方法,您可以在其中执行所需的任何操作。 例如,完全忽略。 如果您将处理程序留空或添加自己的信息,则会发生这种情况:
图 2.14让我们看看异常的日志记录是什么样的:
- 您需要记住有关CancellationException的内容 ,我们要忽略它。
- 添加您自己的日志。
- 记住默认行为,包括登录和终止应用程序,否则异常将“消失”,并且不清楚发生了什么。
现在,对于引发异常的情况,堆栈跟踪列表将与添加的信息一起发送到logcat:
图 2.15并行执行。 异步的
考虑挂起函数的并行操作。
异步最适合组织来自多个功能的并行结果。 异步,就像
启动 -Coroutine Builder。 它的方便之处在于,使用
await()方法可成功返回数据或引发在协程执行期间发生的异常。 await方法将等待协程完成(如果尚未完成),否则它将立即返回工作结果。 注意,await是一个挂起函数,因此不能在协程或其他挂起函数的上下文之外执行。
使用异步,从两个函数并行获取数据将如下所示:
图 2.16想象一下,我们面临着从两个函数并行获取数据的任务。 然后,您需要将它们组合并显示。 出现错误时,有必要绘制UI,取消所有当前请求。 在实践中经常会发现这种情况。
在这种情况下,必须按以下方式处理错误:
- 将错误处理带入每个异步组件。
- 如果发生错误,请取消所有协程。 幸运的是,为此可以指定一个父作业,取消该作业后,其所有子作业都将取消。
- 我们提出了一个附加的实现,以了解是否所有数据都已成功加载。 例如,我们假设如果await返回null,则在接收数据时发生错误。
考虑到所有这些,实施父母协程变得越来越复杂。 async-corutin的实现也很复杂:
图 2.17这种方法不是唯一可行的方法。 例如,您可以使用
ExceptionHandler或
SupervisorJob进行具有错误处理的并行执行。
嵌套的协程
让我们看一下嵌套协程的工作。
默认情况下,嵌套协程使用外部作用域创建并继承其上下文。 结果,嵌套的协程成为子级,外部父级变为父级。
如果我们取消了外部协程,则先前示例中使用的以这种方式创建的嵌套协程也将被取消。 当您需要取消当前请求时,在离开屏幕时也将很有用。 另外,父母亲将经常等待女儿的完成。
您可以使用全局范围创建独立于外部的协程。 在这种情况下,当取消外部协程时,嵌套的协程将继续工作,好像什么都没发生:
图 2.18
您可以通过使用父项作业的
Job键替换context元素来使全局嵌套协程的子级,或者可以完全使用父协程的上下文。 但是在这种情况下,值得记住的是,父协程的所有元素都被接管了:线程池,异常处理程序等:
图 2.19现在很明显,如果您从外部使用协程,则需要为其提供安装作业实例或父级上下文的功能。 并且图书馆开发人员需要考虑将其作为儿童安装的可能性,这会带来不便。
断点
协程会影响调试模式下对象值的查看。 如果在
logData函数的下一个协程中放置一个断点,则在触发时,我们会发现此处一切正常,并且值显示正确:
图 2.20现在使用嵌套的协程获取
dataA ,在
logData上保留一个断点:
图 2.21尝试扩展this块以尝试找到所需的值失败。 因此,在存在挂起功能的情况下进行调试变得困难。
单元测试
单元测试非常简单。 您可以为此使用Coroutine Builder
runBlocking 。
runBlocking阻塞线程,直到所有嵌套的协程完成为止,这正是您测试所需要的。
例如,如果已知使用协程方法内部的某个地方来实现它,那么要测试该方法,您只需要将其包装在
runBlocking中
即可 。
runBlocking可用于测试暂停功能:
图 2.22例子
最后,我想展示一些使用Corutin的例子。
想象一下,我们需要并行执行三个查询A,B和C,显示它们的完成并反映请求A和B完成的时刻。
为此,您可以简单地将查询协程A和B包装到一个通用的协程中,并像对待单个整体一样使用它:
图 2.23下面的示例演示如何使用常规的for循环执行间隔为5秒的定期查询:
图 2.24结论
在这些缺点中,我注意到协程是一个相对较年轻的工具,因此,如果要在产品上使用协程,则应谨慎操作。 调试有困难,小样板在执行中很明显。
通常,协程非常易于使用,尤其是对于实现不复杂的异步任务。 特别地,由于可以使用标准语言构造的事实。 协程很容易进行单元测试,所有这些都是由开发该语言的同一家公司提供的。
举报视频
原来有很多信件。 对于那些喜欢听更多内容的人-我的
MBLT DEV 2018报告中的视频:
关于该主题的有用材料: