协程

115 阅读4分钟

协程思维

下载.jfif

非阻塞(挂起与恢复)

  • 线程

image.png

下载 (1).gif

  • 协程

image.png

下载.gif

启动协程

launch


    /* delay 函数的定义
    注意这个关键字
    ↓ */
    public suspend fun delay(timeMillis: Long) { ... }
    // 仅用于研究,生产环境不建议使用GlobalScope
    fun main() {
        GlobalScope.launch {
            delay(1000L)
            println("Hello World!")
        }
        Thread.sleep(2000L)
    }
/*
输出结果;
Hello World!

runBlocking

fun main() {
    runBlocking { // 1
        println("Coroutine started!") // 2
        delay(1000L) // 3
        println("Hello World!") // 4
    }
    println("After launch!") // 5
    Thread.sleep(2000L) // 6
    println("Process end!") // 7
}
/*
输出结果:
Coroutine started!
Hello World!
After launch!
Process end!
*/

async

fun main() = runBlocking {
    println("In runBlocking:${Thread.currentThread().name}")
    val deferred: Deferred<String> = async {
        println("In async:${Thread.currentThread().name}")
        delay(1000L) // 模拟耗时操作
        return@async "Task completed!"
    }
    println("After async:${Thread.currentThread().name}")
    val result = deferred.await()
    println("Result is: $result")
}
/*
输出结果:
In runBlocking:main @coroutine#1
After async:main @coroutine#1 // 注意,它比“In async”先输出
In async:main @coroutine#2
Result is: Task completed!
*/

总结

image.png

挂起函数

下载 (1).jfif

  1. 要定义挂起函数,我们只需在普通函数的基础上,增加一个 suspend 关键字。 suspend 这个关键字。

  2. 挂起函数,由于它拥有挂起和恢复的能力,因此对于同一行代码来说,“=”左右两边 的代码分别可以执行在不同的线程之上。

  3. 挂起函数的本质,就是 Callback。

Channel

image.png

    // 代码段1
    fun main() = runBlocking {
// 1,创建管道
        val channel = Channel<Int>()
        launch {
// 2,在一个单独的协程当中发送管道消息
            (1..3).forEach {
                channel.send(it) // 挂起函数
                logX("Send: $it")
            }
        }
        launch {
// 3,在一个单独的协程当中接收管道消息
            for (i in channel) { // 挂起函数
                logX("Receive: $i")
            }
        }
        logX("end")
    }
/*
================================
end
Thread:main @coroutine#1
================================
================================
Receive: 1
Thread:main @coroutine#3
================================
================================
Send: 1
Thread:main @coroutine#2
================================
================================
Send: 2
Thread:main @coroutine#2
================================
================================
Receive: 2
Thread:main @coroutine#3
================================
================================
Receive: 3
Thread:main @coroutine#3
================================
================================
Send: 3
Thread:main @coroutine#2
================================
// 4,程序不会退出
*/

Flow

下载 (2).jfif

Flow的基本用法

image.png

class MainViewModel : ViewModel() {

    val timeFlow = flow {
        var time = 0
        while (true) {
            emit(time)
            delay(1000)
            time++
        }
    }

}

class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            lifecycleScope.launch {
                mainViewModel.timeFlow.collect { time ->
                    textView.text = time.toString()
                }
            }
        }
    }
}


效果

880e56e9785b4fce969dda930e90dd19.gif

流速不均匀问题

class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            lifecycleScope.launch {
                mainViewModel.timeFlow.collect { time ->
                    textView.text = time.toString()
                    1:这里修改
                    delay(3000)
                }
            }
        }
    }
}

效果

e57e5e8ab1674dfa8818c2b95ac6c94e.gif

class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            lifecycleScope.launch {
                1: 修改这里
                mainViewModel.timeFlow.collectLatest { time ->
                    textView.text = time.toString()
                    delay(3000)
                }
            }
        }
    }
}

效果

01696cd20f4f44cca71b945e5e6b45c2.gif

Flow的生命周期管理

class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            lifecycleScope.launch {
                mainViewModel.timeFlow.collect { time ->
                    textView.text = time.toString()
                    Log.d("FlowTest", "Update time $time in UI.")
                }
            }
        }
    }
}

class MainViewModel : ViewModel() {

    val timeFlow = flow {
        var time = 0
        while (true) {
            emit(time)
            delay(1000)
            time++
        }
    }
    
}

效果

498ffa159df54c6b87ab19a333b3b687.gif

  1. 当我们按下Home键回到桌面后,控制台的日志依然会持续打印。
  2. 我们并没有很好地管理Flow的生命周期,它没有与Activity的生命周期同步。
class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            1: 修改这里
            lifecycleScope.launchWhenStarted {
                mainViewModel.timeFlow.collect { time ->
                    textView.text = time.toString()
                    Log.d("FlowTest", "Update time $time in UI.")
                }
            }
        }
    }
}

3a94c688b2a845119adf556ef3ffd461.gif

  1. 切回后台就停止打印了。
  2. Flow的管道中一直会暂存着一些的旧数据,这些数据不仅可能已经失去了时效性,而且还会造成一些内存上的问题。
class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.STARTED) {
                    mainViewModel.timeFlow.collect { time ->
                        textView.text = time.toString()
                        Log.d("FlowTest", "Update time $time in UI.")
                    }
                }
            }
        }
    }
}

c330df089ce740baa059fab0b72f52ef.gif

StateFlow的基本用法

class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            mainViewModel.startTimer()
        }
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mainViewModel.stateFlow.collect {
                    textView.text = it.toString()
                }
            }
        }
    }
}

a42ff36022f14da2a5b75c6e1534aa44.gif

  1. StateFlow其中一个重要的价值就是它和LiveData的用法保持了高度一致性。如果你的项目之前使用的是LiveData,那么终于可以放宽了心,零成本地迁移到Flow上了。

StateFlow的高级用法

class MainViewModel : ViewModel() {

    val timeFlow = flow {
        var time = 0
        while (true) {
            emit(time)
            delay(1000)
            time++
        }
    }
    
}

class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mainViewModel.timeFlow.collect { time ->
                    textView.text = time.toString()
                }
            }
        }
    }
}

效果

724e586f966a475bb1ab4a634a365882.gif

  1. 原来除了程序进入后台之外,手机发生横竖屏切换也会让计时器重新开始计时。
class MainViewModel : ViewModel() {

    private val timeFlow = flow {
        var time = 0
        while (true) {
            emit(time)
            delay(1000)
            time++
        }
    }

    val stateFlow =
        timeFlow.stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(5000), 
            0
        )
}
  1. stateIn函数可以将其他的Flow转换成StateFlow。
  2. stateIn函数接收3个参数,其中第1个参数是作用域,传入viewModelScope即可。第3个参数是初始值,计时器的初始值传入0即可。
  3. 这里我们通过stateIn函数的第2个参数指定了一个5秒的超时时长,那么只要在5秒钟内横竖屏切换完成了,Flow就不会停止工作。
class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                1:修改为stateFlow
                mainViewModel.stateFlow.collect { time ->
                    textView.text = time.toString()
                }
            }
        }
    }
}

效果

eac64104e0d9474dbf9e0ccd0d3fd2ae.gif

SharedFlow

class MainViewModel : ViewModel() {

    private val _clickCountFlow = MutableStateFlow(0)

    val clickCountFlow = _clickCountFlow.asStateFlow()

    fun increaseClickCount() {
        _clickCountFlow.value += 1
    }
}
class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            mainViewModel.increaseClickCount()
        }
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mainViewModel.clickCountFlow.collect { time ->
                    textView.text = time.toString()
                }
            }
        }
    }
}

效果

c53fe21844a14ea881e29653862f45a6.gif

  1. 当手机发生横竖屏切换时,整个Activity都重新创建了,则此调用clickCountFlow的collect函数之后,并没有什么新的数据发送过来,但我们仍然能在界面上显示之前计数器的数字。

  2. 由此说明,StateFlow确实是粘性的。

class MainViewModel : ViewModel() {

    private val _loginFlow = MutableStateFlow("")

    val loginFlow = _loginFlow.asStateFlow()

    fun startLogin() {
        // Handle login logic here.
        _loginFlow.value = "Login Success"
    }
}
class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            mainViewModel.startLogin()
        }
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mainViewModel.loginFlow.collect {
                    if (it.isNotBlank()) {
                        Toast.makeText(this@MainActivity, it, Toast.LENGTH_SHORT).show()
                    }
                }
            }
        }
    }
}

效果

bb003788839348c3bd00d15129ac8906.gif

  1. 当我们尝试去旋转一下屏幕,此时又会弹出一个Login Success的Toast,这就不对劲了。

  2. 而这,就是粘性所导致的问题。

class MainViewModel : ViewModel() {

    private val _loginFlow = MutableSharedFlow<String>()

    val loginFlow = _loginFlow.asSharedFlow()

    fun startLogin() {
        // Handle login logic here.
        viewModelScope.launch {
            _loginFlow.emit("Login Success")
        }
    }
}

效果

3593db2577a244e79379dd50737c225f.gif

  1. SharedFlow无法像StateFlow那样通过给value变量赋值来发送消息,而是只能像传统Flow那样调用emit函数。而emit函数又是一个挂起函数,所以这里需要调用viewModelScope的launch函数启动一个协程,然后再发送消息。

参考

  • 《朱涛 · Kotlin编程第一课》
  • Kotlin Flow响应式编程,基础知识入门
  • Kotlin Flow响应式编程,StateFlow和SharedFlow