Kotlinコルーチン-コルーチンの一般的な高度な使用
Kotlinコルーチンシリーズ:
- コルーチンの基本的な使用法
- コルーチンのコンテキスト理解
- コルーチンスコープ管理
- コルーチンの一般的な高度な使用法(この記事)
これまでの記事を通じて、コルーチンを開始する方法、スレッドを切り替える方法、関数を一時停止する方法、およびブロックと非ブロックの違いを理解しました。
コルーチンのコンテキストを理解し、スレッドをスケジュールし、コルーチンのジョブを管理し、例外管理などを行います。
コルーチンの範囲、親子コルーチンの概念、GlobalScop、MainScope、Android固有のlifecycleScopeviewModelScopeなどを理解します。それらの類似点と相違点、およびそれらの使用方法について学びます。
では、実際の開発では、コルーチンをどのように使用するのでしょうか。注意すべき点は何ですか?たとえば、ネットワークリクエストの使用方法とカプセル化方法、カスタムコルーチンの使用方法、コルーチンの同時実行とロックの方法などです。
一緒に見下ろしましょう。
1.コルーチンでのネットワーク要求とカプセル化の使用
一般的に、ネットワークリクエストは次のようになります
private fun doNetWork() {
val api = DemoRetrofit.apiService.getNews("1", "2")
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
YYLogUtils.e(throwable.message ?: "网络错误")
}
MainScope().launch(exceptionHandler) {
val work = async(Dispatchers.IO) {
api.execute() //同步请求
}
//获取到异步的网络请求结果
val response = work.await()
response?.let {
if (response.isSuccessful) {
val bean = response.body()
YYLogUtils.w(bean.toString())
} else {
when (response.code()) {
402 -> {
toast("token过期了")
}
500 -> {
toast("服务器错误")
}
}
}
}
work.invokeOnCompletion {
// 协程关闭时,取消任务
if (work.isCancelled) {
api.cancel()
}
}
}
}
前に述べたことと組み合わせて、HttpエラーとカスタムApiエラーを処理できるこのようなネットワークを要求します。
このようにすべてのネットワークリクエストを作成する必要がありますか?カプセル化できますか?もちろん、拡張メソッドをツールクラスとしてカプセル化し、DSLコールバックメソッドを使用して結果をコールバックできます(インターフェイスよりも少し便利です) 、そうでない場合は、DSLを理解している場合は、コメントをクリアしようとします。理解していない場合は、Kotlinの高度な機能に関する以前の記事を読むことができます)。
まず、DSLの実装を定義します
class RetrofitResultCallbackDsl<ResultType> {
var api: (Call<ResultType>)? = null
internal var onSuccess: ((ResultType?) -> Unit)? = null
internal var onComplete: (() -> Unit)? = null
internal var onFailed: ((error: String?, code: Int) -> Unit)? = null
fun onSuccess(block: (ResultType?) -> Unit) {
this.onSuccess = block
}
fun onComplete(block: () -> Unit) {
this.onComplete = block
}
fun onFailed(block: (error: String?, code: Int) -> Unit) {
this.onFailed = block
}
fun clean() {
onSuccess = null
onComplete = null
onFailed = null
}
}
DSLとRetrofitを使用した同期リクエストのカプセル化
fun <ResultType> CoroutineScope.retrofit(
init: RetrofitResultCallbackDsl<ResultType>.() -> Unit
) {
//先初始化DSL类
val retrofitCoroutine = RetrofitResultCallbackDsl<ResultType>()
init(retrofitCoroutine)
//如果DSL是接口实现类,这里需要绑定接口,我们这里没有实现接口,就无需绑定了
//DSL初始化完成之后启动协程
launch(Dispatchers.Main) {
retrofitCoroutine.api?.let { it ->
val work = async(Dispatchers.IO) {
try {
it.execute()
} catch (e: Exception) {
//这里DSL如果是绑定接口的无需我们自己手动调用,一般都是接口调用,我们这里没有绑定接口就自己手动调用了,
retrofitCoroutine.onFailed?.invoke("网络错误", -1)
null
}
}
work.invokeOnCompletion {
if (work.isCancelled) {
it.cancel()
retrofitCoroutine.clean()
}
}
val response = work.await()
retrofitCoroutine.onComplete?.invoke()
response?.let {
if (response.isSuccessful) {
retrofitCoroutine.onSuccess?.invoke(response.body())
} else {
when (response.code()) {
402 -> {
toast("token过期了")
}
500 -> {
toast("服务器错误")
}
}
retrofitCoroutine.onFailed?.invoke(response.errorBody()?.toString(), response.code())
}
}
}
}
}
ここでのDSLの実装は、一般的なDSLとは異なります。一般的に、誰もがDSLにインターフェイスの実装を教えています。ここではインターフェイスを実装していないため、コールバックするときに手動でコールバックする必要があります。
次に、使用する場合:
MainScope().retrofit<String> {
api = DemoRetrofit.apiService.getNews("1", "2")
onComplete {
YYLogUtils.w("网络请求执行完毕")
}
onSuccess { result ->
YYLogUtils.w("成功的结果:" + result)
}
onFailed { error, code ->
YYLogUtils.e("网络请求出错了")
}
}
これは非常にOkHttpコールバックスタイルですが、これは確かにコールバックです。以前にインターフェイスを使用していたコールバックをDSLコールバックに変更するだけで、本質的にはコールバックのままです。
よりエレガントな方法はありますか?それは、コルーチンがコールバックを排除することであるという意味ではありませんか?このようにコルーチンを使用する場合と使用しない場合の違いは何ですか。。。
第二に、コルーチンとレトロフィットのサスペンション法のカプセル化
确实,上面都是使用协程+同步的方式,其实协程在2.6之后就支持了挂起 suspend 的方式。那么我们使用 协程 + 挂起函数岂不是绝配。
例如我们定义Api的时候,我们就能直接标记方法为 suspend
@GET("/index.php/api/employee/industry")
suspend fun getIndustry(
@Header("Content-Type") contentType: String,
@Header("Accept") accept: String
): BaseBean<List<Industry>>
那么我们封装这样的网络请求带错误处理的时候就要这么来
suspend fun <T : Any> Any.extRequestHttp(call: suspend () -> BaseBean<T>): OkResult<T> {
return try {
val response = call()
if (response.code == 200) {
OkResult.Success(response.data)
} else {
OkResult.Error(ApiException(response.code, response.message))
}
} catch (e: Exception) {
e.printStackTrace()
OkResult.Error(handleExceptionMessage(e))
}
}
fun handleExceptionMessage(e: Exception): IOException {
return when (e) {
is UnknownHostException -> IOException("Unable to access domain name, unknown domain name.")
is JsonParseException -> IOException("Data parsing exception.")
is HttpException -> IOException("The server is on business. Please try again later.")
is ConnectException -> IOException("Network connection exception, please check the network.")
is SocketException -> IOException("Network connection exception, please check the network.")
is SocketTimeoutException -> IOException("Network connection timeout.")
is RuntimeException -> IOException("Error running, please try again.")
else -> IOException("unknown error.")
}
}
直接定义一个扩展方法,请求网络,需要注意的是我们的参数是 suspend 我们内部的实现并没有开启协程,所以我们自己的方法必须也要加上 suspend 前缀,确保它最后实在协程内执行的。
当然了,如果不想用try catch的话,使用 runCatching 也是一样的。
runCatching {
call.invoke()
}.onSuccess { response: BaseBean<T> ->
if (response.code == 200) {
OkResult.Success(response.data)
} else {
OkResult.Error(ApiException(response.code, response.message))
}
}.onFailure { e ->
e.printStackTrace()
OkResult.Error(handleExceptionMessage(Exception(e.message, e)))
}
其实 runCatching 的实现和try-catch是一样的,语法糖而已,内部还是try-catch的实现。
使用的时候,一般都是现在Repository中定义
suspend fun getIndustry(): OkResult<List<Industry>> {
return extRequestHttp {
DemoRetrofit.apiService.getIndustry(
Constants.NETWORK_CONTENT_TYPE,
Constants.NETWORK_ACCEPT_V1
)
}
}
这里一样的内部的实现并没有开启协程,所以我们自己的方法必须也要加上 suspend 前缀,确保它最后实在协程内执行。
真正的调用在ViewModel中
fun requestIndustry() {
viewModelScope.launch {
//开始Loading
loadStartLoading()
val result = mRepository.getIndustry()
result.checkResult({
//处理成功的信息
toast("list:$it")
liveData.value = it
}, {
//失败
liveData.value = null
})
loadHideProgress()
}
}
这样就完成一次封装到使用的全部过程,可以看到确实是比协程+同步的要简单一些了。如果想看更完整的协程+挂起可以看看我之前的文章协程的使用与封装。
三、自定义协程的几种方法
之前讲到协程作用域的时候,我们有些特殊情况需要自定义协程作用域。一起看看自定义协程有哪几种方式
3.1 自己管理Job
其实最简单也是最直接的做法是像RxJava那样,自己管理Dispose对象
open class BaseVM : ViewModel(){
val jobs = mutableListOf<Job>()
override fun onCleared() {
super.onCleared()
jobs.forEach { it.cancel() }
}
}
class UserVM : BaseVM() {
val userData = StateLiveData<UserBean>()
fun login() {
jobs.add(GlobalScope.launch {
YYLogUtils.w("切换到一个协程3")
delay(3000)
YYLogUtils.w("协程3执行完毕")
})
}
自己关联和管理job,destroy的时候关闭job
3.2 委托 MainScope 管理
我们可以使用 MainScope 管理当前类的协程作用域,这里不止使用MainScope,但是用它最方便。
比如我们可以在一个自定义View中都可以使用协程了:
class AsyncView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
View(context, attrs, defStyleAttr), CoroutineScope by MainScope() {
fun doSth(block: (View) -> Unit) {
launch {
YYLogUtils.w("切换到一个协程")
delay(3000)
YYLogUtils.w("协程执行完毕")
block(view)
}
}
override fun onDetachedFromWindow() {
cancel()
super.onDetachedFromWindow()
}
}
3.3 原始的 CoroutineScope 实现
我们可以直接让我们的类实现 CoroutineScope 接口,但是我们需要指定协程的上下文,我们可以这样。
abstract class CoroutineDialog : Dialog, CoroutineScope {
// 默认上下文使用context.dispatcher()
override val coroutineContext: CoroutineContext by lazy { context.dispatcher() }
...
}
比如我想封装一个带协程的PopupWindow,我这样封装一个基类
/**
* 自定义带协程作用域的弹窗
*/
abstract class CoroutineScopeCenterPopup(activity: FragmentActivity) : CenterPopupView(activity), CoroutineScope {
private lateinit var job: Job
private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
YYLogUtils.e(throwable.message ?: "Unkown Error")
}
//此协程作用域的自定义 CoroutineContext
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job + CoroutineName("CenterPopupScope") + exceptionHandler
override fun onCreate() {
job = Job()
super.onCreate()
}
override fun onDismiss() {
job.cancel() // 关闭弹窗后,结束所有协程任务
YYLogUtils.w("关闭弹窗后,结束所有协程任务")
super.onDismiss()
}
}
那么我就可以直接使用这个基类的实现了:
class InterviewAcceptPopup(private val mActivity: FragmentActivity) : CoroutineScopeCenterPopup(mActivity) {
override fun getImplLayoutId(): Int {
return R.layout.dialog_interview_accept
}
override fun onCreate() {
super.onCreate()
val btnYes = findViewById<TextView>(R.id.btn_y)
btnYes.click {
doSth()
}
}
private fun doSth() {
launch {
YYLogUtils.w("执行在协程中...")
delay(1000L)
YYLogUtils.w("执行完毕...")
dismiss()
}
}
}
实际开发中如果是涉及到 Android 页面的一些生命周期的,我们可以使用viewModelScope、lifecycleScope 。如果是其他的页面比如 View 或者 Dialog 或者干脆不涉及到页面的一些地方,我们就可以使用以上的几种方法来实现自定义的协程作用域。
当然还有更多的自定义协程作用域方法,我没有穷举出来,如果大家有更多更好的方法也可以评论区讨论一下。
四、协程的并发与锁
之前我们就讲到过 并发使用 async ,切换线程使用 withContext 。本质原因是 async 函数是创建一个协程,而 withContext 函数只是一个挂起函数。
launch {
YYLogUtils.w("查看运行的线程1:" + Thread.currentThread().name)
val start = System.currentTimeMillis()
val res1 = async {
YYLogUtils.w("查看运行的线程2:" + Thread.currentThread().name)
delay(1000)
"123"
}
val res2 = async(Dispatchers.IO) {
YYLogUtils.w("查看运行的线程3:" + Thread.currentThread().name)
delay(2000)
"456"
}
val res3 = res1.await() + res2.await()
YYLogUtils.w("并发耗时:" + (System.currentTimeMillis() - start))
withContext(Dispatchers.IO) {
YYLogUtils.w("查看运行的线程4:" + Thread.currentThread().name)
delay(1000)
YYLogUtils.w("res3:$res3")
}
YYLogUtils.w("查看运行的线程4:" + Thread.currentThread().name)
}
这样的代码大家就能看懂了,打印结果如下:
为什么要这么写,是因为有人认为 async 就是异步的,异步的才是并发的。No No No ,async 翻译过来是异步的意思,但是这里它并不是异步的,async 只是创建一个新的协程而已,并发是协程的并发,而不是异步而并发,可以看到我们创建一个 async 函数,我们打印它的线程默认是主线程的,除非你指定线程运行,如第二个 async 函数,我们指定了线程才是异步的。
这么写就是为了不要混淆协程的并发与线程的异步并发的区别,并发协程与并发异步线程不同,再次强调,因为它是创建了协程所以并发,而不是异步才并发的。
再举例,创建协程的又不止 async 函数一家,我们的 launch 一样的能创建协程,我们试试看能不能并发:
launch {
YYLogUtils.w("查看运行的线程1:" + Thread.currentThread().name)
val start = System.currentTimeMillis()
val res1 = async {
YYLogUtils.w("查看运行的线程2:" + Thread.currentThread().name)
delay(1000)
"123"
}
val res2 = async(Dispatchers.IO) {
YYLogUtils.w("查看运行的线程3:" + Thread.currentThread().name)
delay(2000)
"456"
}
launch {
YYLogUtils.w("查看运行的线程4:" + Thread.currentThread().name)
delay(1000)
}
val res3 = res1.await() + res2.await()
YYLogUtils.w("并发耗时:" + (System.currentTimeMillis() - start))
withContext(Dispatchers.IO) {
YYLogUtils.w("查看运行的线程5:" + Thread.currentThread().name)
delay(1000)
YYLogUtils.w("res3:$res3")
}
YYLogUtils.w("查看运行的线程6:" + Thread.currentThread().name)
}
打印结果证明是并发的:
那为什么我们一般并发都使用 async 而不用 launch 。那是因为他们虽然都是创建了协程,但是 launch 返回的是 Job 对象 ,而 async 返回的是一个 Deferred 可以通过它拿到处理之后的值。
比如这样:
launch {
YYLogUtils.w("查看运行的线程1:" + Thread.currentThread().name)
val start = System.currentTimeMillis()
val res1 = async {
YYLogUtils.w("查看运行的线程2:" + Thread.currentThread().name)
delay(1000)
"123"
}
val res2 = async(Dispatchers.IO) {
YYLogUtils.w("查看运行的线程3:" + Thread.currentThread().name)
delay(2000)
"456"
}
val res4 = launch {
YYLogUtils.w("查看运行的线程4:" + Thread.currentThread().name)
delay(1000)
"789"
}
val res3 = res1.await() + res2.await()
YYLogUtils.w("并发耗时:" + (System.currentTimeMillis() - start))
}
我们能拿到 res4 的值吗? 它返回的是 789 吗?不是它返回的是Job对象 。所以我们才在需要接收返回值的并发中使用 async ,而如果不需要返回值 那么我们使用 launch 也是可以并发的。
协程的并发跟线程是否在主线程和子线程没关系,我们再举例:
launch {
YYLogUtils.w("查看运行的线程1:" + Thread.currentThread().name)
val start = System.currentTimeMillis()
val res1 = async {
YYLogUtils.w("查看运行的线程2:" + Thread.currentThread().name)
delay(1000)
"123"
}
val res2 = async {
YYLogUtils.w("查看运行的线程3:" + Thread.currentThread().name)
delay(2000)
"456"
}
val res3 = res1.await() + res2.await()
YYLogUtils.w("并发耗时:" + (System.currentTimeMillis() - start))
withContext(Dispatchers.IO) {
YYLogUtils.w("查看运行的线程5:" + Thread.currentThread().name)
delay(1000)
YYLogUtils.w("res3:$res3")
}
YYLogUtils.w("查看运行的线程6:" + Thread.currentThread().name)
}
打印结果:
并发的线程如果都在主线程,一样的并发的。
那有同学要问了,newki newki,你这个不对啊,我们使用 Retrofit 请求网络,我们标记 suspend 之后再协程里面直接使用,它就是异步的 , 你看我这样,这样,再这样。并发 + 异步 so easy , async 我不也没有指定异步吗,它可不就是异步的吗!
viewModelScope.launch {
val industryResult = async {
mRepository.getIndustry()
}
val schoolResult = async {
mRepository.getSchool()
}
val industry = industryResult.await()
val school = schoolResult.await()
}
好吧,其实 Retrofit 是比较特殊的情况,他的 ApiService 虽然标记了 suspend ,看起来我们是直接使用了,但是其实内部 Retrofit 的动态代理的时候会找到你是否标记了 suspend ,然后它会对 suspend 做单独的处理 。我不太会讲源码,大家如果有兴趣可以全局搜索一下 SuspendForResponse
类 和 awaitResponse
类,看看 Retrofit 怎么把 suspend 转换为协程处理的,这里我就不贴 Retrofit 的源码了。
并发是没有问题了,协程与线程一样,对于数据的操作无法保持原子性,所以在协程中,需要使用原子性的数据结构,例如使用 mutex.withLock
来处理。
线程中锁都是阻塞式,在没有获取锁时无法执行其他逻辑,而协程可以通过挂起函数解决这个,没有获取锁就挂起协程,获取后再恢复协程,协程挂起时线程并没有阻塞可以执行其他逻辑。这种互斥锁就是Mutex。这也是为什么在协程中不推荐使用 synchronized 关键字的原因,因为会导致阻塞。
使用Mutex的方式
suspend fun CoroutineScope.massiveRun(action: suspend () -> Unit) {
val n = 100 // 启动的协程数量
val k = 1000 // 每个协程重复执行同一动作的次数
val time = measureTimeMillis {
List(n) {
launch {
repeat(k) { action() }
}
}
}
YYLogUtils.w("Completed ${n * k} actions in $time ms")
}
//使用
var counter = 0
val mutex = Mutex()
runBlocking {
massiveRun {
mutex.withLock {
counter++
}
}
}
YYLogUtils.w("Counter = $counter")
打印Log:
总结
急いで、このシリーズは至る所で終わります。この時点で、コルーチン比較システムの知識はほとんど同じであり、基本的な使用法に問題はありません。他の散在する知識ポイントは個別に公開する必要はなく、次のコードに表示されます。
実際、このシリーズを行うために、私自身がコルーチンに関する知識のポイントを体系的に整理しました。1か月も経たないうちに忘れてしまいますが、このシステムを整理することで、誰もが簡単に確認できるようになります。
最後に、実際、コルーチンであろうとコルーチンであろうと挂起
、并发
、阻塞
コルーチンとスレッドの間にはまだ違いがあることがわかります。コルーチンはスレッドのカプセル化であると誰もが言っていますが、それでもコルーチンとスレッドから学んでもらいたいので、理解しやすくなっています。私の謙虚な意見ですが、気に入らない場合はスプレーしないでください。
理解できない場合は、シリーズの最初の記事から始めることをお勧めします。内部実装は段階的に段階的に行われます。
コルーチンの概念や枠組みは比較的大きいので、説明に間違いや間違いがあれば、生徒たちに指摘して伝えてもらいたいと思います。
この記事が少し刺激を受けたと感じたら、私をサポートしていただければ幸いです点赞
。あなたのサポートが私の最大のモチベーションです。
これでこのシリーズは終わりです。
ナゲッツテクノロジーコミュニティのクリエイター署名プログラムの募集に参加しています。リンクをクリックして登録し、送信してください。