美文网首页
Android面试Kotlin高阶篇(八)

Android面试Kotlin高阶篇(八)

作者: 肖义熙 | 来源:发表于2021-03-30 20:43 被阅读0次
问:你对Kotlin的协程理解

答:协程可以看作是一种轻量级的线程,只不过线程有系统调度,而协程由代码来控制。协程允许我们在单线程模式下模拟多线程编程效果,代码的执行与挂起完全由编程语言来控制,与系统无关。
本篇文章文字内容可能较多,跟着官方文档的思路来解答Kotlin的协程:Kotlin协程

一、特点:
  • 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。(单线程模拟多线程编程效果)
  • 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
  • 内置取消支持取消操作会自动在运行中的整个协程层次结构内传播。
  • Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。
二、在后台线程中执行

Android中,如果在主线程(UI线程)中执行耗时操作,则会导致线程阻塞直到收到响应,如果一直处于阻塞状态(Activity 五秒钟),即可能发生ANR,所以需要子线程来执行耗时操作,这里所说的在后台线程中执行就是子线程中执行。

//使用密封类,后续解析Result时不需要特别指定else
sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}
//执行网络请求或其他耗时操作
class LoginRepository(private val responseParser: LoginResponseParser) {
    fun makeLoginRequest(jsonBody: String): Result<LoginResponse> {
        //执行网络请求,如果成功
        if(success){
            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection")
    }
}

class LoginViewModel(private val loginRepository: LoginRepository):ViewModel() {
    fun login(username: String, token: String) {
        //当调在主线程中调用login方法时,耗时方法则阻塞主线程,可能产生ANR
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

如上代码,如果在Activity或其他主线程所在线程中调用login方法,则可能造成主线程阻塞,造成ANR。在协程以前,一般使用Thread、AsyncTask、RxJava 等来进行 异步 、开启线程、线程切换 等等一系列操作。但是在协程出现以后:

class LoginViewModel(private val loginRepository: LoginRepositor): ViewModel() {
    fun login(username: String, token: String) {
        //创建一个协程来执行线程操作
        viewModelScope.launch(Dispatchers.IO) {
            //viewModelScope其实是androidx.lifecycle.viewModelScope,用于开启协程,Dispatchers.IO 指定协程运行在IO线程
            //其中Dispatchers还有 Dispatchers.MAIN(指定协程在主线程中执行) , Dispatchers.DEFAULT,用于在CPU密集型计算的协程中。
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

前面提到过,协程是用来执行异步、耗时任务的,标记makeLoginRequest()方法为“耗时任务”需要使用 suspend 关键字(这里叫 挂起函数 )其实: 凡是耗时函数/方法,就应该标上suspend关键字前缀,让别人知道它是个耗时函数/方法。并且它只能在协程里,或者另外一个同样标记了suspend关键字的函数/方法里调用
修改LoginRepository中的代码:

class LoginRepository(...) {
    ...
    suspend fun makeLoginRequest( jsonBody: String): Result<LoginResponse> {
        // Move the execution of the coroutine to the I/O dispatcher
        return withContext(Dispatchers.IO) {
            // Blocking network request code
        }
    }
}

使用suspend关键字修饰makeLoginRequest函数,并且使用withContext(Dispatchers.IO)挂起函数,同时指定其运行的线程为IO线程。此时,当前协程会阻塞,不影响主线程执行。修改代码:

class LoginViewModel(private val loginRepository: LoginRepositor): ViewModel() {
    fun login(username: String, token: String) {
        //创建一个协程来执行线程操作
        viewModelScope.launch() {
           //makeLoginRequest方法为挂起函数,同时使用withContext(Dispatchers.IO)指定挂起函数运行于IO线程
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            //协程执行完成网络请求后返回结果,
            val result = loginRepository.makeLoginRequest(jsonBody)
            //doSomething
        }
    }
}

以上即为协程的简单使用与理解。

聊聊协程的一些关键字/函数的理解:
GlobalScope.launch{} 开启一个顶级协程,创建一个协程作用域,当应用程序运行结束也会跟着一起结束。

fun main(){
  GlobalScope.launch{
    System.out.println("GlobalScope.launch 顶级协程中运行的代码")
  }
}//运行程序会发现没有任何打印,因为应用程序结束的时候还没来得及打印协程也跟着停止了。

fun main(){
  GlobalScope.launch{
    //delay(1500)  //挂起函数,在协程或其他挂起函数中可以调用,非阻塞式挂起函数,只会挂起协程,不影响当前线程下的其他协程。
    //如果开启,则协程还没打印时,应用程序已经结束,则不打印。
    System.out.println("GlobalScope.launch 顶级协程中运行的代码")
  }
  Thread.sleep(1000)
}   //线程睡眠1000毫秒,协程执行打印时,应用还没有停止,则打印。

runBlocking: 创建一个协程作用域,阻塞当前线程,直到协程作用域中的代码全部执行完毕。不常用。
launch: 创建一个协程,只能在协程作用域中调用。
suspend: 声明一个函数为挂起函数,协程中遇到挂起函数则会 挂起 ,直到函数执行完成后 恢复 到挂起位置
coroutineScope: 一个挂起函数,继承外部的协程作用域并创建一个子作用域,这样就可以给任意函数提供协程作用域了。注意:挂起函数只能在协程作用域或其他挂起函数中调用,所以,最终,coroutineScope函数一定是在协程作用域中的。coroutineScope函数会一直阻塞当前 协程 如:

fun main(){
  GlobalScope.launch{
    coroutineScope{  //继承外部协程作用域,一直阻塞当前协程。
      launch{  //运行在coroutineScope的子作用域中
        delay(1500)
        System.out.println("我是协程1呀")
      }
    }
    coroutineScope{  //继承外部协程作用域,一直阻塞当前协程。
      launch{  //运行在coroutineScope的子作用域中
        delay(500)
        System.out.println("我是协程2呀")  
      }
    }
  }
  System.out.println("我是线程打印的呀")  
  Thread.sleep(1600)
}
//执行结果:
我是线程打印的呀
我是协程1呀
//我是协程2不打印

Job: GlobalScope.launch{}、launch{} 函数都会返回一个Job对象,其可以调用cancel()方法取消协程。
CoroutineScope(job): CoroutineScope(job).launch中创建的所有协程,都会关联在Job对象的作用域下,只需要调用一次cancel 就可以将其创建的所有协程一次性取消。
async: 函数只能在协程作用域中调用,创建一个新的协程并返回 Deferred 对象,Deferred对象可以调用await()函数来获取协程执行结果。同时await函数是一个阻塞函数,会将当前协程作用域阻塞,影响其他协程执行。如果这个协程作用域中有两个及以上,想要同时并发执行的话,可以之后进行调用。

fun main(){
  runBlocking{
    val result = async{
      delay(1000)
      5+5
    }
    val result2 = async{
      delay(1000)
      4+6
    }
    System.out.println("执行结果:$result.await() + $result2.await()")    //打印执行结果:20
    //如果在async后面直接加await()函数的话耗时差不多2000毫秒多(串行),但是上面的例子只耗时1000毫秒多(并行)。
  }
}

withContext(): withContext函数必须指定执行线程withContext(Dispatchers.IO)、 withContext(Dispatchers.Main)、withContext(Dispatchers.Default), IO毫无疑问执行在IO操作较多的协程(网络请求、数据库、文件等IO操作), Main指定协程在主线程中执行,Default低并发线程策略,所有协程默认Default,在CPU进行密集型计算时使用。
suspendCoroutine: 能将传统回调机制的写法大幅简化,必须在协程作用域或挂起函数中调用。接收一个Lambda表达式,将当前协程挂起,在Lambda表达式中午传入Coroutine对象,调用它的resume、resumeWithException既可以让当前协程恢复执行。

//传统写法:
fun test(object: CallListener{
  override fun finish(){
    //doSomething()
  }
  override fun error(){
    //doSomething()
  }
})

//使用suspendCoroutine写法
suspend fun httpRequest(){
  suspendCoroutine{ continuation->
    test(object: CallListener{
      override fun finish(){
        continuation.resume()
      }
      override fun error(){
        continuation.resumeWithException()
      }
    })
  }
}
//之后调用test方法则只需要调用 httpRequest()方法即可。

相关文章

网友评论

      本文标题:Android面试Kotlin高阶篇(八)

      本文链接:https://www.haomeiwen.com/subject/fbnphltx.html