简单介绍一下Android 协程

2023-07-10  本文已影响0人  BlueSocks

前言

最近新项目开始,老总发话说我们要用新技术,不能再使用老的架构和技术了。迫于无奈,开始Google推荐的新架构学习,基于单一数据源和单项数据流驱动的MVVM架构。在学习的过程中,又系统的了解了一遍Android协程的使用。有了一些新的感悟,就记录在此了。 本文基本转载自Android官方文档,加了少许个人见解。大佬们看到请轻喷。

一、协程的诞生

众所周知,Android为了主线程安全,是不能在主线程上去执行任何耗时操作的。开发者在进行耗时操作时,需要自己启动子线程后放在子线程中运行,过程中会产生大量的线程管理代码。协程的诞生就是为了优化这一操作,协程是一种并发的设计模式,可以在Android平台上使用它来简化需要异步执行的代码。

协程的特点

协程是Google推荐的在 Android 上进行异步编程的推荐解决方案。它具有一下特点:

二、协程的使用

在后台线程中执行

如果在主线程上发出网络请求,则主线程会处于等待或阻塞状态,直到收到响应。由于线程处于阻塞状态,因此操作系统无法调用 onDraw(),这会导致应用冻结,并有可能导致弹出“应用无响应”(ANR) 对话框。为了解决这个问题,通常开发中我们会在后台线程上执行网络请求等耗时操作。

下面我们以一个简单的登录请求为例,看一下协程操作的使用方法。

首先,我们先看一下在Google推荐的架构中,Repository 类是如何发出请求的:

//网络请求响应结果实体封装类
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) {
    //具体的请求地址
    private const val loginUrl = "https://example.com/login"

    // Function that makes the network request, blocking the current thread
    //具体的网络请求函数,会阻塞当前线程,直到结果返回。
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())
            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

上面的代码中为了对网络请求的响应数据做处理,我们创建了自己的 Result 类。其中在 makeLoginRequest 是同步执行函数,会阻塞发起调用的线程。

ViewModel 会在用户与界面发生交互(例如,点击登录按钮)时触发网络请求:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

如果我们直接使用上述代码,LoginViewModel 就会在网络请求发出时阻塞界面线程。如需将执行操作移出主线程,我们以往的方式是启动一个新的线程去执行:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        //创建线程并启动,执行登录请求。
        Thread{
            Runnable {
                loginRepository.makeLoginRequest(jsonBody)
            }
        }.start()
    }
}



上面这样的做法,会让我们在每次执行登网络请求时都创建一个线程,并且在请求完成后需要使用回调和handler把请求结果重新传递给主线程处理。而协程的出翔让我们有个更简单的方法,就是创建一个新的协程,然后在 I/O 线程上执行网络请求:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        // Create a new coroutine to move the execution off the UI thread
        //创建一个新的协程,使其移出UI线程执行, Dispatchers.IO: I/O 操作预留的线程
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            //执行网络请求操作,该请求会在I/O 操作预留的线程上执行
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

下面我们仔细分析一下 login 函数中的协程代码:

login 函数按以下方式执行:

由于此协程通过 viewModelScope 启动,因此在 ViewModel 的作用域内执行。如果 ViewModel 因用户离开屏幕而被销毁,则 viewModelScope 会自动取消,且所有运行的协程也会被取消。

前面的示例存在的两个问题是,一是调用 makeLoginRequest 的任何项都需要记得将执行操作显式移出主线程,即在 launch 函数后传入 (Dispatchers.IO) 参数。二是没有对登录请求的结果做处理。下面我们来看看如何修改 Repository 以解决这一问题。

使用协程确保主线程安全

如果函数不会在主线程上阻止界面更新,我们即将其视为是主线程安全的。makeLoginRequest 函数不是主线程安全的,因为从主线程调用 makeLoginRequest 确实会阻塞界面。在上面的代码示例中,我们可以在 ViewModel 中启动协程,并分配对应的调度程序,但是这种做法需要我们每次在调用 makeLoginRequest 时都要去尾货调度程序。为了解决该问题我们可以使用协程库中的 withContext() 函数将协程的执行操作移至其他线程:

class LoginRepository(...) {
    private const val loginUrl = "https://example.com/login"
    //suspend 关键字表示改方法会阻塞线程,Kotlin 利用此关键字强制从协程内调用函数。
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        // Move the execution of the coroutine to the I/O dispatcher
        //表示协程的后续执行会被放在IO线程中
        return withContext(Dispatchers.IO) {
            val url = URL(loginUrl)
            (url.openConnection() as? HttpURLConnection)?.run {
                requestMethod = "POST"
                setRequestProperty("Content-Type", "application/json; utf-8")
                setRequestProperty("Accept", "application/json")
                doOutput = true
                outputStream.write(jsonBody.toByteArray())
                return Result.Success(responseParser.parse(inputStream))
            }
            return Result.Error(Exception("Cannot open HttpURLConnection"))
            
        }
    }
}

withContext(Dispatchers.IO) 将协程的执行操作移至一个 I/O 线程,这样一来,我们的调用函数便是主线程安全的,并且支持根据需要更新界面。

makeLoginRequest 还会用 suspend 关键字进行标记。Kotlin 利用此关键字强制从协程内调用函数。

接下来我们在 ViewModel 中,由于 makeLoginRequest 将执行操作移出主线程,login 函数中的协程现在可以在主线程中执行:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {

        // Create a new coroutine on the UI thread
        //直接在UI主线程中启动一个协程
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // Make the network call and suspend execution until it finishes
            //执行网络操作,并且等待被suspend标记的函数执行完成。
            //该等待并不会阻塞主线程,因为被suspend标记的函数会被分配到IO线程执行
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            //当收到请求结果后,向用户现实请求结果,并更行对应界面
            when (result) {
                is Result.Success<LoginResponse> -> // 登录成功,跳转主页。。。
                else -> // 登录失败,提示用户错误信息
            }
        }
    }
}

请注意,此处仍需要协程,因为 makeLoginRequest 是一个 suspend 函数,而所有 suspend 函数都必须在协程中执行。

此代码与前面的 login 示例的不同之处体现在以下几个方面:

login 函数现在按以下方式执行:

处理异常

在进行网络请求或者耗时操作时,经常会抛出异常。为了处理 Repository 可能出现的异常,我们可以使用 try-catch 块捕捉并处理对应异常:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun makeLoginRequest(username: String, token: String) {
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            //使用try-catch捕捉异常
            val result = try {
                loginRepository.makeLoginRequest(jsonBody)
            } catch(e: Exception) {
                Result.Error(Exception("Network request failed"))
            }
            when (result) {
                is Result.Success<LoginResponse> -> // 登录成功,跳转主页。。。
                else -> // 登录失败,提示用户错误信息
            }
        }
    }
}

在上面代码示例中,makeLoginRequest() 调用抛出的任何意外异常都会处理为界面错误。

总结

这样我们就使用协程完整实现了一个登录请求的操作,在此过程中,我们只需要在 loginRepository 中使用 withContext 函数声明调度程序,就可以避免耗时操作阻塞主线程的问题,并且不需要开发者自己去管理对应的线程。并且因为有 viewModelScope 的存在,使得我们也不需要去特意处理页面销毁后的请求取消问题。优化性能的同时,又大大减少了我们的代码量。是一种优秀的异步代码处理模式。

上一篇 下一篇

猜你喜欢

热点阅读