Android中竞争的现代方法:Kotlin的Corotins

哈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 imageView.setImageBitmap(image) //     } 

该代码作为单线程函数很简单。 而且,尽管在分配的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() //    ,      

asynclaunch类似,但返回的是deferred (相当于Future的Kotlin实体),因此可以使用await()获得其结果。 不带参数调用时,它将在当前作用域的默认上下文中工作。

同样,在等待2个值时,主线程保持空闲状态。
如您所见, launch函数返回Job ,它可以用来等待操作完成-这是通过join()函数完成的。 它的工作方式与其他任何语言一样,但要注意的是,它只是暂停协程,而不阻塞流程

派遣

使用协同程序时,调度是一个关键概念。 此操作使您可以从一个线程“跳转”到另一个线程。

考虑一下Java中Main分派的等效情况,即

 runOnUiThread: public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action); //  } else { action.run(); //   } } 

Android的Main上下文实现是基于处理程序的调度程序。 因此,这确实是一个非常合适的实现:

 launch(Dispatchers.Main) { ... } vs launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) { ... } //   kotlinx 0.26: launch(Dispatchers.Main.immediate) { ... } 

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

现在,默认的launchasync管理器将成为当前的作用域管理器。

 launch { val foo = withContext(Dispatchers.IO) { … } // -    CoroutineContext   … } launch(Dispatchers.Default) { // -        … } 

协程的自动启动(在任何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

同样, ActorHandler非常相似:我们定义协程的上下文(即,我们将在其中执行操作的线程)并按顺序使用它。

当然,区别在于这里使用了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) } } } //  fun filter(query: String?) = updateActor.offer(Filter(query)) //  suspend fun filter(query: String?) = updateActor.send(Filter(query)) 

在此示例中,我们使用密封的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() //  Job    override val coroutineContext = Dispatchers.Main.immediate+job override fun onDestroy() { super.onDestroy() job.cancel() //      } } 

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) { //         val scope = (context as? CoroutineScope)?: AppScope val eventActor = scope.actor<Unit>(capacity = Channel.CONFLATED) { for (event in channel) action() } //       setOnClickListener { eventActor.offer(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 } //  GlobalScope.launch(Dispatchers.Main, parent = untilDestroy) { /*    ! */ } 

通过回调简化情况(第1部分)

这是通过Channel转换基于回调的API的用法的方法。

API的工作方式如下:

  1. requestBrowsing(url, listener)解析位于url的文件夹。
  2. listener收到此文件夹中找到的任何媒体文件的onMediaAdded(media: Media)
  3. 解析文件夹时调用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来等待媒体,而不是selectconsumeEach并且该循环将在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) //        for (media in browserChannel) refreshList.add(media) //   dataset.value = refreshList parseSubDirectories() } 

通过回调简化情况(第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) } //  (   Main) livedata.value = Repo.browse(path) 

因此,阻止网络的呼叫是在专用的Retrofit线程中进行的,协程在这里,等待服务器的响应,并且在应用程序中无处可使用!

此实现受到gildor / kotlin-coroutines-retrofit库的启发。

还有一个JakeWharton / retrofit2-kotlin-coroutines-adapter ,另外一个实现也给出了类似的结果。

结语

Channel可以以许多其他方式使用; 请查看BroadcastChannel,以获得可能有用的更强大的实现。

您还可以使用生产功能创建频道。

最后,使用通道可以方便地组织UI组件之间的通信:适配器可以通过Channel或例如Actor点击事件传输到其片段/活动。

Source: https://habr.com/ru/post/zh-CN457224/


All Articles