哈Ha!
我们提醒您,我们已经从著名的《大书呆子牧场指南》系列中
预订了人们
期待已久的有关Kotlin语言的
书 。 今天,我们决定提请您注意一篇文章的翻译,该文章讲述了Kotlin协程以及Android中流的正确工作。 对该主题的讨论非常活跃,因此,为完整起见,我们还建议您阅读Habr的
本文和Axmor Software博客的
详细文章 。
Java / Android中的现代竞争框架在回调上造成地狱,并导致阻塞状态,因为Android并没有相当简单的方法来保证线程安全。
Kotlin协程是非常有效和完整的工具包,可以使竞争管理变得更加轻松和高效。
暂停和阻止:有什么区别协程不替代线程,而是提供用于管理线程的框架。 corutin的理念是定义一个上下文,使您可以
等待后台操作完成而不会阻塞主线程。
在这种情况下,Corutin的目标是放弃回调并简化竞争。
最简单的例子首先,让我们以最简单的示例为例:在
Main
(主线程)的上下文中运行coroutine。 在其中,我们将从
IO
流中提取图像,并将该图像发送回
Main
进行处理。
launch(Dispatchers.Main) { val image = withContext(Dispatchers.IO) { getImage() }
该代码作为单线程函数很简单。 而且,尽管在分配的
IO
线程池中执行
getImage
,但主线程是空闲的,可以执行任何其他任务! withContext函数在其动作运行时暂停当前协程(
getImage()
)。 一旦
getImage()
返回并且主线程中的循环程序变得可用,协程将在主线程中恢复工作并调用
imageView.setImageBitmap(image)
。
第二个示例:现在我们需要完成2个后台任务,以便可以使用它们。 我们将使用async / await二重唱,以便并行执行这两个任务,并在两个任务准备就绪后立即在主线程中使用它们的结果:
val job = launch(Dispatchers.Main) { val deferred1 = async(Dispatchers.Default) { getFirstValue() } val deferred2 = async(Dispatchers.IO) { getSecondValue() } useValues(deferred1.await(), deferred2.await()) } job.join()
async
与
launch
类似,但返回的是
deferred
(相当于
Future
的Kotlin实体),因此可以使用
await()
获得其结果。 不带参数调用时,它将在当前作用域的默认上下文中工作。
同样,在等待2个值时,主线程保持空闲状态。
如您所见,
launch
函数返回
Job
,它可以用来等待操作完成-这是通过
join()
函数完成的。 它的工作方式与其他任何语言一样,但要注意的是,它只是
暂停协程,而不阻塞流程 。
派遣使用协同程序时,调度是一个关键概念。 此操作使您可以从一个线程“跳转”到另一个线程。
考虑一下Java中
Main
分派的等效情况,即
runOnUiThread: public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action);
Android的
Main
上下文实现是基于处理程序的调度程序。 因此,这确实是一个非常合适的实现:
launch(Dispatchers.Main) { ... } vs launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) { ... }
launch(Dispatchers.Main)
将
Runnable
发送给
Handler
,因此其代码不会立即执行。
launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED)
将立即在当前线程中执行其lambda表达式。
Dispatchers.Main
确保协程恢复工作时将其定向到主线程 ; 另外,这里将Handler用作原生Android实现,以发送到应用程序事件循环。
确切的实现如下所示:
val Main: HandlerDispatcher = HandlerContext(mainHandler, "Main")
这是一篇很好的文章,可以帮助您了解Android中调度的复杂性:
了解Android Core:Looper,Handler和HandlerThread 。
协程上下文协程上下文(也称为协程管理器)确定将在哪个线程中执行其代码,如果引发异常,该怎么办,并引用父上下文来传播取消。
val job = Job() val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> whatever(throwable) } launch(Disaptchers.Default+exceptionHandler+job) { ... }
job.cancel()
将取消所有父对象为
job
协程。 exceptionHandler将接收在这些协程中引发的所有异常。
适用范围coroutineScope
接口简化了错误处理:
如果其子协程中的任何一个失败,则整个范围和所有子协程也将被取消。
在
async
示例中,如果无法提取该值,而另一项任务继续工作,则我们处于损坏状态,我们需要对此进行一些处理。
使用
coroutineScope
,仅当两个值的提取成功时,才会调用
useValues
函数。 另外,如果
deferred2
失败,则
deferred1
将被取消。
coroutineScope { val deferred1 = async(Dispatchers.Default) { getFirstValue() } val deferred2 = async(Dispatchers.IO) { getSecondValue() } useValues(deferred1.await(), deferred2.await()) }
您还可以“放入作用域”整个类来为其设置默认的
CoroutineContext
并使用它。
实现
CoroutineScope
接口的示例类:
open class ScopedViewModel : ViewModel(), CoroutineScope { protected val job = Job() override val coroutineContext = Dispatchers.Main+job override fun onCleared() { super.onCleared() job.cancel() } }
在
CoroutineScope
运行
CoroutineScope
:
现在,默认的
launch
或
async
管理器将成为当前的作用域管理器。
launch { val foo = withContext(Dispatchers.IO) { … }
协程的自动启动(在任何CoroutineScope之外):
GlobalScope.launch(Dispatchers.Main) {
您甚至可以通过设置默认的
Main
调度程序来定义应用程序的范围:
object AppScope : CoroutineScope by GlobalScope { override val coroutineContext = Dispatchers.Main.immediate }
备注- 协程限制了与Java的互操作性
- 限制可变性以避免锁定
- 协程旨在等待而不是组织线程
- 避免在
Dispatchers.Default
(和Main
...)中使用I / O-这就是Dispatchers.IO的用途 - 流消耗资源,因此使用单线程上下文
Dispatchers.Default
基于Android 5+中引入的ForkJoinPool
- 协程可以通过渠道使用
使用通道摆脱锁和回调JetBrains文档中的渠道定义:
Channel Channel
概念上与BlockingQueue
非常相似。 关键区别在于,它不会阻止放置操作,而是提供了挂起send
(或非阻止offer
),并且提供了暂停receive
,而不是阻止了获取操作。
演员们考虑一个使用渠道的简单工具:
Actor
。
同样,
Actor
与
Handler
非常相似:我们定义协程的上下文(即,我们将在其中执行操作的线程)并按顺序使用它。
当然,区别在于这里使用了Corutins。
您可以指定电源以及执行的代码-pause 。
原则上,
actor
会将任何命令重定向到协程通道。 它
保证命令的执行,并限制其上下文中的操作 。 这种方法非常有助于摆脱
synchronize
调用并保持所有线程空闲!
protected val updateActor by lazy { actor<Update>(capacity = Channel.UNLIMITED) { for (update in channel) when (update) { Refresh -> updateList() is Filter -> filter.filter(update.query) is MediaUpdate -> updateItems(update.mediaList as List<T>) is MediaAddition -> addMedia(update.media as T) is MediaListAddition -> addMedia(update.mediaList as List<T>) is MediaRemoval -> removeMedia(update.media as T) } } }
在此示例中,我们使用密封的Kotlin类,选择要执行的动作。
sealed class Update object Refresh : Update() class Filter(val query: String?) : Update() class MediaAddition(val media: Media) : Update()
而且,所有这些动作都将排队,它们将永远不会并行执行。 这是达到
可变性限制的便捷方法。
Android生命周期+协程Actor对于控制Android用户界面,简化任务取消并防止主线程过载也非常有用。
让我们实现它,并在活动被破坏时调用
job.cancel()
。
class MyActivity : AppCompatActivity(), CoroutineScope { protected val job = SupervisorJob()
SupervisorJob
类与常规
Job
类似,唯一的区别是取消仅在下游方向上进行。
因此,当其中一个协程失败时,我们不会取消所有协程。
使用
扩展功能 ,您可以从
CoroutineContext
的任何
View
访问此
CoroutineContext
,从而使事情变得更好。
val View.coroutineContext: CoroutineContext? get() = (context as? CoroutineScope)?.coroutineContext
现在我们可以结合所有这些,
setOnClick
函数创建一个组合的actor来控制其
onClick
动作。 在多次轻击的情况下,中间动作将被忽略,从而消除了ANR错误(应用程序不响应),并且这些动作将在
Activity
的范围内执行。 因此,当活动被销毁时,所有这些都将被取消。
fun View.setOnClick(action: suspend () -> Unit) {
在此示例中,我们将
Channel
设置为
Conflated
以便在事件过多时忽略某些事件。 如果您希望将事件排队而不丢失任何事件,但仍希望保护应用程序免受ANR错误的影响,则可以将其替换为
Channel.UNLIMITED
。
您还可以结合协程和生命周期框架来自动取消与用户界面相关的任务:
val LifecycleOwner.untilDestroy: Job get() { val job = Job() lifecycle.addObserver(object: LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy() { job.cancel() } }) return job }
通过回调简化情况(第1部分)这是通过
Channel
转换基于回调的API的用法的方法。
API的工作方式如下:
requestBrowsing(url, listener)
解析位于url
的文件夹。listener
收到此文件夹中找到的任何媒体文件的onMediaAdded(media: Media)
。- 解析文件夹时调用
listener.onBrowseEnd()
这是VLC浏览器的内容提供程序中的旧
refresh
功能:
private val refreshList = mutableListOf<Media>() fun refresh() = requestBrowsing(url, refreshListener) private val refreshListener = object : EventListener{ override fun onMediaAdded(media: Media) { refreshList.add(media)) } override fun onBrowseEnd() { val list = refreshList.toMutableList() refreshList.clear() launch { dataset.value = list parseSubDirectories() } } }
如何改善呢?创建一个将
refresh
运行的通道。 现在,浏览器回调仅将媒体定向到该通道,然后将其关闭。
现在,
refresh
功能变得更加清晰。 她创建了一个频道,调用了VLC浏览器,然后形成了一个媒体文件列表并对其进行处理。
您可以使用
for
来等待媒体,而不是
select
或
consumeEach
并且该循环将在
browserChannel
关闭后立即中断。
private lateinit var browserChannel : Channel<Media> override fun onMediaAdded(media: Media) { browserChannel.offer(media) } override fun onBrowseEnd() { browserChannel.close() } suspend fun refresh() { browserChannel = Channel(Channel.UNLIMITED) val refreshList = mutableListOf<Media>() requestBrowsing(url)
通过回调简化情况(第2部分):改进第二种方法:我们根本不使用kotlinx协程,而是使用协程核心框架。
看看协程实际上是如何工作的!
retrofitSuspendCall
函数包装
Retrofit Call
请求以使其成为
suspend
函数。
使用
suspendCoroutine
我们调用
Call.enqueue
方法并暂停协程。 以这种方式提供的回调将在调用后立即调用
continuation.resume(response)
以使用服务器的响应来恢复协程。
接下来,我们只需要将Retrofit函数组合到
retrofitSuspendCall
即可使用它们返回查询结果。
suspend inline fun <reified T> retrofitSuspendCall(request: () -> Call <T> ) : Response <T> = suspendCoroutine { continuation -> request.invoke().enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { continuation.resume(response) } override fun onFailure(call: Call<T>, t: Throwable) { continuation.resumeWithException(t) } }) } suspend fun browse(path: String?) = retrofitSuspendCall { ApiClient.browse(path) }
因此,阻止网络的呼叫是在专用的Retrofit线程中进行的,协程在这里,等待服务器的响应,并且在应用程序中无处可使用!
此实现受到
gildor / kotlin-coroutines-retrofit库的启发。
还有一个
JakeWharton / retrofit2-kotlin-coroutines-adapter ,另外一个实现也给出了类似的结果。
结语Channel
可以以许多其他方式使用; 请查看
BroadcastChannel,以获得可能有用的更强大的实现。
您还可以使用
生产功能创建频道。
最后,使用通道可以方便地组织UI组件之间的通信:适配器可以通过
Channel
或例如
Actor
点击事件传输到其片段/活动。