一.Lambda知识点
1.函数声明、实现与调用
2.函数推断
3.函数参数
fun test1(){
//1.函数声明、实现与调用
var method:()->String /*函数声明*/
method={
println("this is method impl")
"1" //最后一行为返回值
}//函数实现
method()//函数调用 运算符 重载 和下面invoke调用一致
println(method.invoke())
//2.函数推断,省去声明过程
var method2={
println("this is method impl")
"1" //lambda最后一行为返回值 普通函数不是
}//函数实现
method()//函数调用 运算符 重载 和下面invoke调用一致
println(method.invoke())
//3.参数的设置
var method3:(Int)->String={a:Int->a.toString()}
//如果是单个参数可以在实现中不声明类型 但是要声明
var method4:(Int)->String={a->a.toString()}
//如果是单个参数可以在实现中不定义形参,默认是it
var method5:(Int)->String={it.toString()}
//如果参数实现中未使用到可以使用_来替代,它就不会定义一个临时变量
var method6:(Int,String)->String={_,s->s}
}
//注意下面lambda和普通的函数的区别
fun method3(): () -> Boolean /*可省略*/= {
true
}
fun method4(): Boolean {
return true
}
//函数中套函数 函数的返回的是一个函数
fun k():(Int)->Unit={p:Int-> println(p)}
@Test
fun test3(){
k()(1)
}
//返回的是Unit
fun method() :Unit= run { println() }
//返回的是lambda函数
fun method2():()->Unit={
}
var method=::method4//将一个函数的引用赋值给变量
//使用函数体返回
var methodx:()->Boolean = fun ():Boolean{
return true
}
private fun test(){
methodx();
}
4.搭配扩展函数
@Test
fun test2(){
//为String添加一个扩展函数匿名函数
var method:String.(p:Int)->Unit={
//lambda内部持有的this指向当前string
//it是传过来的参数
println("$this,$it")
}
var s="1"
s.method(1);
}
二.高阶函数
高阶函数就是函数中的参数可以是一个函数。
@Test
fun test() {
//如果最后一个参数是lambda可以后面根大括号
val str=show(2) {
println("par:$it")
"1"
}
println("str:$str")
//
math(1,2,3){
a, b, c ->
a+b+c
}
}
//函数中参数是函数
private fun show(int: Int, block: (Int) -> String) :String{
//调用函数 有点回调的意思
return block(int)
}
//根据传入过来的函数来定义计算
private fun math(a: Int,b: Int,c: Int,cal:(a: Int,b: Int,c: Int)->Int):Int{
val result = cal(a, b, c)
println("result:$result")
return result
}
高阶函数搭配扩展函数使用,注意 this和it,扩展函数持有的是this
@Test
fun test2() {
var str = "false"
println(str.run2 {
//这里是this 获取的是当前对象
str == "true"
})
var str2 = "false"
str2.run3 {
//可以获得的是it 获取的是参数
true
}
}
//高阶函数和扩展函数的配合
//参数是一个泛型的匿名的扩展的lambda函数
private fun <T> T.run2(pMethod: T.() -> Boolean) =
pMethod()
//高阶函数和扩展函数的配合
//参数是一个匿名的lambda函数
private fun <T> T.run3(pMethod: (Double) -> Boolean) =
pMethod(1.0)
函数引用参数传递写法
//传入函数引用
@Test
fun test3(){
val a:Function1<Int,String> = ::blockImpl
fun b():Function1<Int,String> = ::blockImpl
run4(a)
run4(b())
run4 (::blockImpl)
var c:(Int)->String = ::blockImpl
var d:Int.()->String = ::blockImpl //Int.()和(Int)可以替换使用
}
fun blockImpl(value:Int):String{
println("$value")
return "$value"
}
private fun run4(block: (Int) -> String)=block(1)
函数声明返回匿名函数中包含匿名函数
/*函数的返回是一个高阶函数 包含了三个参数,最后一个是匿名lambda函数*/
fun higherTest(): (Int, String, (Int, String) -> Unit) -> Unit =
{ num: Int, str: String, lambda: (Int, String) -> Unit ->
lambda(num, str)
}
@Test
fun test4() {
higherTest()/*返回函数*/(1,"2"){
i,str->
println("$i,$str")
}
}
搭配泛型使用转换
//搭配泛型使用 将T 转成T1 R转成R1
fun <T, T1, R, R1> covertHigher()/*: (T, T1, (T) -> R, (T1) -> R1) -> Unit*/ =
{ t: T, t1: T1, lambda: (T) -> R, lambda2: (T1) -> R1 ->
println(lambda(t))
println(lambda2(t1))
}
@Test
fun test5() {
covertHigher<String, Int, Int, String>()("1",1,{
it.length
},{
it.toString()
})
}
三.手写Kotlin中的内置高阶函数
1.手写rx
@Test
fun testRx() {
create { "1" }
.mappper { 2 }
.comsumer {
println(it)
}
}
//手写create map操作符 类似Rxjava 写法1
//保存数据
class Helper<T>(var item: T) {
//将T转成R
fun <R> mappper(action: (T) -> R): Helper<R> {
return Helper(action(item))
}
fun comsumer(action: (T) -> Unit) {
action(item)
}
}
//返回Help用来保存创建的数据
fun <R> create(action: () -> R): Helper<R> {
return Helper(action())
}
@Test
fun testRx2() {
create2 { "1" }
.mappper2 {
println(length)
}
.comsumer2 {
println(this)
}
}
//手写create map操作符 类似Rxjava 写法2 使用扩展函数
//保存数据
class Helper2<T>(var item: T)
//将T转成R
fun <T, R> Helper2<T>.mappper2(action: T.() -> R) = Helper2(action(item))
fun <T> Helper2<T>.comsumer2(action: (T) -> Unit) = action(item)
//返回Help用来保存创建的数据
fun <R> create2(action: () -> R): Helper2<R> = Helper2(action())
2.内置高阶函数
1.Apply
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
可以看到apply中lamba是一个扩展函数,并且返回的是this,也就是调用对象的本身,我们通常使用它给某个 对象设置属性,方法等
data class User(var name: String, var email: String)
fun main() {
val user = User("Bob", "bob@example.com").apply {
name = "Alice"
email = "alice@example.com"
}
println("Name: ${user.name}, Email: ${user.email}")
}
2.also
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
also 函数会返回调用它的对象本身,这使得它适合用于在不改变对象本身的情况下,对对象执行一些额外的操作,比如日志记录、调试信息输出等。
@Test
fun testAlso() {
val numbers = mutableListOf(1, 2, 3, 4, 5)
val result = numbers
.also { println("原始列表: $it") }
.map { it * 2 }
.also { println("乘以 2 后的列表: $it") }
.filter { it > 5 }
.also { println("过滤后大于 5 的列表: $it") }
println("最终结果: $result")
}
3.Run
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
可以看到run有两种使用方式。一种是作为对象的扩展函数调用;另一种是作为独立函数使用。
- 当作为扩展函数时,它会将调用对象作为
this上下文,在代码块中可以直接访问该对象的属性和方法。 - 代码块的最后一行表达式的值会作为
run函数的返回值。
@Test
fun runTest() {
val person = Person("Alice", 25)
// 作为扩展函数使用
val result = person.run {
age++
"Name: $name, Age: $age"
}
println(result)
// 作为独立函数使用
val number = run {
val a = 10
val b = 20
a + b
}
println(number)
}
4.Let
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
let函数将调用对象作为参数传递给代码块,在代码块中使用it来引用该对象。- 代码块的最后一行表达式的值会作为函数的返回值。
- 常用于空安全处理,避免空指针异常。
@Test
fun testLet() {
var message: String? = "Hello, Kotlin!"
message?.let {
println(it.length)
}
}
5.with
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
with函数接收一个对象作为参数,在代码块中,该对象作为this上下文,可直接访问其属性和方法。- 代码块的最后一行表达式的值会作为函数的返回值。
data class Book(var title: String, var author: String)
@Test
fun testWith() {
val book = Book("Kotlin in Action", "Dmitry Jemerov")
val info = with(book) {
"Title: $title, Author: $author"
}
println(info)
}
| 函数名 | 上下文引用 | 返回值 | 常见使用场景 |
|---|---|---|---|
also | it | 调用对象本身 | 额外操作(如日志记录 ) |
apply | this | 调用对象本身 | 对象初始化和配置 |
with | this | 代码块最后一个表达式的值 | 对对象进行一系列操作并获取结果 |
let | it | 代码块最后一个表达式的值 | 空安全处理、链式调用中的转换 |
run | this(扩展函数),无(独立函数) | 代码块最后一个表达式的值 | 对象初始化后计算结果、独立代码块执行 |
四.委托机制
什么是委托?委托就是通过组合的形式,传入对象/属性,让其他类帮你实现对象或者属性的实例化。
1.类委托
假设我们定义一个Dao接口,它有两个实现,我们有个仓库需要调用它获取实例,我们可以通过组合属性的方式将其实例化,我们可以使用依赖注入的方式,同时我们也可以使用委托,在Kotlin中,我们的委托帮助我们省略了不少代码,我们看下 它是如何实现的:
interface Dao {
fun getUser(): String
}
/**
* rom数据库实现
*/
class DaoImplRom : Dao {
override fun getUser() =
"return Rom User"
}
/**
* litePal数据库实现
*/
class DaoImplLitePal : Dao {
override fun getUser() =
"return litePal User"
}
/**
* 委托实现
* 1.参数 使用接口
* 2.实现功能接口,但是当前类无需实现接口的方法
* 3.by + 功能
* 4.内部即可调用接口中的方法 节省代码
* 5.外部调用时指定传入实现即可
*
* @constructor
*/
class UserRepository(dao: Dao) :Dao by dao{
fun getDaoUser():String{
return getUser()
}
}
@Test
fun testDelegateClz() {
println(UserRepository(DaoImplRom()).getDaoUser())
}
可以看到,dao以组合形式和仓库组合,并且它实现了Dao接口,但是我们并没有重写它的方法就获得了它的功能,我们只需要在外部传入具体实现即可。
2.属性委托
1.一般属性委托:将类中的属性委托给另外一个类实现;
2.可观察属性委托:属性改变时委托给observable观察变化
3.vetoable委托:和可观察差不多,但是可以阻止其变化
4.属性委托存储在map中。
// 1.定义一个属性委托类
class NameDelegate {
private var value: String = ""
//1.操作符 对象持有
operator fun getValue(thisRef: Any?, property: kotlin.reflect.KProperty<*>): String {
println("getValue:$this,$property")
return value
}
operator fun setValue(thisRef: Any?, property: kotlin.reflect.KProperty<*>, newValue: String) {
println("setValue:$this,$property,$newValue")
value = newValue
}
}
// 2.定义一个属性委托类 实现Kotlin帮我们提供的接口
class AgeDelegate : ReadWriteProperty<UserDelegate, Int> {
private var age: Int = 0
override fun getValue(thisRef: UserDelegate, property: KProperty<*>): Int {
println("age getValue:$thisRef,$property")
return age
}
override fun setValue(thisRef: UserDelegate, property: KProperty<*>, value: Int) {
age = value
println("age setValue:$thisRef,$property,$age")
}
}
class UserDelegate {
//属性委托给UserNameDelegate实现
var name: String by NameDelegate()
var age: Int by AgeDelegate()
//3.可观察的属性委托:即属性变化时,我们在委托中会观察到它的变化
var pressTime: Long by Delegates.observable(System.currentTimeMillis()) { _, oldValue, newValue ->
println("pressTime oldValue:$oldValue,newValue:$newValue")
//2次的时间间隔小于2秒就退出了
if (newValue - oldValue < 2000) {
println("退出")
} else {
println("请再次按返回键退出")
}
}
//4.vetoable
var num: Int by Delegates.vetoable(1) { _, oldValue, newValue ->
println("newValue:$newValue")
newValue > 2 //大于2时才容许更改
}
}
//5. map属性委托构造方法中传入ma,然后属性可以对其赋值
class MapDelegate(map:Map<String,Any>){
val name:String by map
val age:Int by map
}
@Test
fun testDelegateClz() {
UserRepository(DaoImplRom()).getDaoUser().also {
println(it)
}
}
@Test
fun testDelegateProperty() {
UserDelegate().apply {
name = "zhangsan"
age = 14
println(name)
println(age)
}
}
@Test
fun testDelegateObservableProperty() {
UserDelegate().apply {
pressTime = System.currentTimeMillis()
}
}
@Test
fun testDelegateVoProperty() {
UserDelegate().apply {
num = 2 //不容许修改
println(num)
num = 0 //容许修改
println(num)
}
}
@Test
fun testDelegateMap() {
val map = mapOf("name" to "zhangsan", "age" to 15)
.onEach { (t, u) ->
println("key:$t")
println("value:$u")
}
MapDelegate(map)
.also {
println(it.name)
println(it.age)
}
}
五.协程
1.协程的定义
协程可以理解为轻量级的线程,它是一种可以暂停和恢复执行的程序组件。与线程相比,协程的创建和销毁开销极小,一个线程中可以同时存在成千上万个协程。可以帮助我们使用同步的写法写出异步的效果;可以合并执行结果等,代码示例如下:
//1.协程的好处,
// 1.可以使用同步的方式写异步的代码
lifecycleScope.launch {
delay(1000)//模拟网路请求在非主线程执行
//直接更新UI会切换到主线程
}
//2.合并两个请求的结果
//如果是Rx可以用到zip操作符 或者使用ContDownLatch来获取
lifecycleScope.launch {
delay(1000)//模拟网路请求在非主线程执行
//任务1
val ret1=async {
1
}
//任务2
val ret2=async { 2 }
//任务1执行+任务2执行 得到合并后结果
val i = ret1.await() + ret2.await()
}
2.协程的启动方式
1.runblocking
它会帮助我们开启一个协程,并且会在其执行完之后才结束当前线程,主要用于测试。
@Test
fun testRunBlocking() = runBlocking {
// 这是一种阻塞式的协程启动方式,会阻塞当前线程直到协程体执行完毕。
// 主要用于测试、main 函数中或桥接阻塞代码与非阻塞代码。禁止在 UI 线程中使用,
//否则会导致应用无响应(ANR)
println("run blocking test...")
}
2.launch
用于启动一个不需要返回结果的协程。它返回一个 Job 对象,可以用来控制协程的生命周期,例如取消协程( job.cancel() )或等待其完成( job.join() )。这种方式适用于“发后即忘”(fire-and-forget)的后台任务,如日志上传或缓存清理。 job的有几个点需要注意下:
launch执行流程 :
1.外层协程启动:当调用外层的 launch 函数时,外层协程开始执行。
2.遇到内层 launch :在外层协程的执行过程中,如果遇到内层的 launch 函数,会立即启动内 层协程,而外层协程不会等待内层协程执行完毕,会继续执行自身后续的代码。
3.内外层协程并发执行:内层协程和外层协程会并发执行(具体取决于调度器的配置),它们的执行顺序是不确定的,可能交替执行。
1. Main: runBlocking 开始
8. Main: 外层协程已启动,主线程继续执行
2. Parent: 外层协程开始
5. Parent: 内层协程已启动,外层协程继续执行
3. Child: 内层协程开始
6. Parent: 外层协程部分任务执行完毕
4. Child: 内层协程执行完毕
7. Parent: 显式等待内层协程完成,外层协程结束
9. Main: 外层协程执行完毕,runBlocking 结束
@Test
fun test2() = runBlocking {
println("1. Main: runBlocking 开始") // 主线程打印
val parentJob = launch {
println("2. Parent: 外层协程开始")
val childJob = launch {
println("3. Child: 内层协程开始")
delay(1000) // 模拟耗时操作,挂起内层协程
println("4. Child: 内层协程执行完毕")
} // childJob 创建并立即返回
println("5. Parent: 内层协程已启动,外层协程继续执行")
delay(500)
println("6. Parent: 外层协程部分任务执行完毕")
// 等待子协程完成 (可选,因为结构化并发已隐式等待)
childJob.join()
println("7. Parent: 显式等待内层协程完成,外层协程结束")
} // parentJob 创建并立即返回
println("8. Main: 外层协程已启动,主线程继续执行")
// 等待外层协程完成
parentJob.join()
println("9. Main: 外层协程执行完毕,runBlocking 结束")
}
job的取消流程
1.如果一个 Job 有子 Job ,取消父 Job 会递归地取消所有子 Job
2.取消一个 Job 不会影响其父 Job 或兄弟 Job
3.一旦 Job 被取消,它就处于“Cancelling”然后是“Cancelled”状态,不能再被用作新 Job 的父级
@Test
fun testLaunch()= runBlocking {
val job = launch { // 启动一个新协程并保持对它的引用
//子job 如果父job
launch {
while (true){
delay(1000) //如果被取消 可能会抛出异常
println("job running inner")
}
}
launch {
while (isActive/* 最好是根据isActive*/){
try {
delay(1000)
} catch (e: CancellationException) {
println("协程被取消,捕获到 CancellationException: ${e.message}")
}
println("job running inner2")
}
}
while (isActive){ //判断是否取消 如果一个 Job 有子 Job,取消父 Job 会递归地取消所有子 Job
delay(1000)
println("job running")
}
}
launch {
println("Kotlin ")
delay(3000)
println("cancel job")
job.cancel()//取消job协程 它是协作时的,所以在协程中可以使用isActive来判断它的运行状态
job.join()//挂起,直到取消操作完成
}
println("Hello,")
}
3.asyn
async 是一个用于启动协程的函数,它与 launch 类似,但主要区别在于 async 会返回一个 Deferred对象,该对象可以在协程执行完成后获取其结果。
启动流程:
1.启动协程:调用 async 函数时,协程立即开始执行,async 函数会立即返回一个 Deferred 对象,而 不会等待协程执行完成。
2.并发执行:协程在后台并发执行(具体取决于调度器的配置),主线程可以继续执行后续代码。
3.获取结果:当调用 deferred.await() 时,如果协程还未执行完成,当前协程会被挂起,直到协程执 行完成并返回结果。
4.继续执行:获取到结果后,当前协程会继续执行后续代码。
@Test
fun testAsyn() =
//用于启动一个需要返回结果的协程。它返回一个 Deferred<T> 对象(继承自 Job),通过调用 await()
// 方法可以获取协程的执行结果,该操作会挂起当前协程直到结果可用。async 常用于并行计算任务
runBlocking {
//1.启动协程:调用 async 函数时,协程立即开始执行,async 函数会立即返回一个 Deferred 对象,而不会等待协程执行完成。
//2.并发执行:协程在后台并发执行(具体取决于调度器的配置),主线程可以继续执行后续代码。
//3.获取结果:当调用 deferred.await() 时,如果协程还未执行完成,当前协程会被挂起,直到协程执行完成并返回结果。
//4.继续执行:获取到结果后,当前协程会继续执行后续代码。
val deferred1 = async {
delay(1000)
println("deferred1")
10
}
val deferred2 = async {
delay(1500)
println("deferred2")
20
}
launch {
while (isActive){
println("--->>>")
delay(500)
}
}
// 等待两个协程都执行完成并获取结果
val result1 = deferred1.await()
val result2 = deferred2.await()
println("结果1: $result1, 结果2: $result2")
}
4.suspend关键字
suspend 关键字的主要作用是标记一个函数为挂起函数,挂起函数可以在执行过程中暂停(挂起)当前协程的执行,将控制权交还给调用者,并且在合适的时候恢复执行。这种特性使得协程可以在不阻塞线程的情况下处理异步操作。挂起函数可以用于协程之间的协作,例如一个协程等待另一个协程的结果。 delay 函数就是一个典型的挂起函数,用于暂停协程的执行一段时间。它具有以下三个特点:
-
- **只能在协程或其他挂起函数中调用
**由于挂起函数的执行可能会挂起当前协程,因此它只能在协程或者其他挂起函数内部被调用。如果在普通函数中调用挂起函数,会导致编译错误。 - **挂起和恢复机制
**当调用挂起函数时,当前协程会被挂起,线程可以继续执行其他任务。当挂起函数的条件满足(如异步操作完成)时,协程会恢复执行。 - **异常处理
**挂起函数的异常处理和普通函数类似,可以使用 try-catch 块来捕获和处理异常。
- **只能在协程或其他挂起函数中调用
5.withcontext
在 Kotlin 协程里,执行上下文( CoroutineContext )定义了协程执行的环境,包含了调度器( CoroutineDispatcher )、异常处理器( CoroutineExceptionHandler )等元素。 withContext 函数的主要作用就是在不同的执行上下文之间进行切换,让协程可以在合适的线程环境中执行特定的任务。
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
可以看到,它的参数是一个协程的上下文和一个协程作用域扩展的挂起函数。
context:指定要切换到的目标执行上下文。常见的有Dispatchers.Main(用于 Android 主线程)、Dispatchers.IO(用于 I/O 操作)、Dispatchers.Default(用于 CPU 密集型操作)等。block:这是一个挂起函数类型的 lambda 表达式,代表要在新的执行上下文里执行的代码块。该代码块的返回值会作为withContext函数的返回值。
常见用法:
1.调度器的切换
2.线程的切换
@Test
fun testWith1() = runBlocking {
println("当前线程: ${Thread.currentThread().name}")
// 切换到 IO 调度器执行
val result = withContext(Dispatchers.IO) {
println("切换到 IO 线程: ${Thread.currentThread().name}")
// 模拟一个耗时的 I/O 操作
delay(1000)
"IO 操作结果"
}
println("回到原线程: ${Thread.currentThread().name},结果: $result")
}
@Test
fun testWith2() = runBlocking {
// 模拟一个异步任务
GlobalScope.launch(Dispatchers.IO) {
// 模拟耗时操作
delay(2000)
val data = "从网络获取的数据"
// 切换到主线程更新 UI
withContext(Dispatchers.Main) {
// 更新 UI 的操作
// 例如 textView.text = data
println("update UI $data ${Thread.currentThread().name}")
}
}
}
6.delay函数
delay 函数用于让当前协程挂起指定的时间,在挂起期间,它不会阻塞线程,而是会让出线程的控制权,使得其他协程或任务可以在该线程上继续执行。当指定的时间过去后,协程会恢复执行。
suspend fun delay(timeMillis: Long)
通过下面的例子可以看出,使用sleep会阻塞协程1,但是delay则不会。
@Test
fun testKDelay(): Unit =
runBlocking {
// 使用 delay 的协程
launch {
println("协程 1 开始执行")
delay(1000)
println("协程 1 暂停 1 秒后继续执行")
}
// 使用 Thread.sleep 的线程阻塞
launch {
println("协程 2 开始执行")
Thread.sleep(3000) //会阻塞其他协程
//delay(3000) //不会阻塞其他协程
println("协程 2 阻塞 3 秒后继续执行")
}
}
delay的可取消性:
当协程被取消时,如果正在执行 delay 函数, delay 会抛出 Cancellat ionException 来响应取 消操作。
val job=launch {
try {
println("协程开始执行")
delay(2 * 1000)
println("协程执行完毕")
} catch (call: CancellationException) {
println(call.message)
call.printStackTrace()
}
}
delay(100)
job.cancel()
3.协程的结构化编程
1.协程的作用域
Kotlin 协程的作用域( CoroutineScope )是管理协程生命周期的核心概念。它是一个轻量级的上下文,用于确保协程在特定的生命周期内运行,并在该生命周期结束时自动取消所有相关的协程,从而避免内存泄漏和资源浪费。它有以下三个核心功能。
-
- 结构化并发:作用域强制实现了结构化并发的原则。在一个作用域内启动的所有协程都会被这个作用域所管理。当作用域本身被取消(
scope.cancel()),其下所有的子协程也会被递归地取消。 - 生命周期关联:在 Android 开发等场景中,可以将协程作用域与组件的生命周期(如
Activity、Fragment、ViewModel)绑定。例如,使用viewModelScope可以确保在ViewModel被清除时,所有仍在运行的协程会自动被取消**** 。 - 提供执行上下文:每个作用域都包含一个
CoroutineContext,其中至少有一个Job和一个CoroutineDispatcher(调度器)。这决定了协程的执行环境和线程。
- 结构化并发:作用域强制实现了结构化并发的原则。在一个作用域内启动的所有协程都会被这个作用域所管理。当作用域本身被取消(
- 常见的作用域
1.GloableScope
GlobalScope 是一个全局作用域,它没有父级 Job ,因此不受任何外部生命周期的限制。在其内部启动的协程是“顶级”协程,必须手动管理它们的生命周期。强烈不推荐在有明确生命周期的环境中使用 GlobalScope ,因为它启动的协程不会随着组件的销毁而自动取消,极易导致内存泄漏。
// 不推荐的做法
GlobalScope.launch {
delay(1000)
println("This will run even if the Activity is destroyed")
}
2.ViewModelScope
这是 ViewModel 的扩展属性,提供了一个与 ViewModel 生命周期绑定的作用域。当 ViewModel 的 onCleared() 方法被调用时, viewModelScope 会自动取消其内的所有协程。这是在 ViewModel 中启动协程的推荐方式
class MyViewModel : ViewModel() {
fun fetchData() {
// 推荐做法
viewModelScope.launch {
try {
val result = repository.getData()
_uiState.value = UiState.Success(result)
} catch (e: Exception) {
_uiState.value = UiState.Error(e)
}
}
}
}
3.LifeCycleScope
这是一个 LifecycleOwner (如 Activity 或 Fragment )的扩展属性。它提供的作用域与 Lifecycle 绑定。当 Lifecycle 进入 DESTROYED 状态时,作用域会被取消
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// 在 Fragment 的 STARTED 状态下安全地观察数据流
flow.collect { data ->
textView.text = data
}
}
}
}
}
4.courtineScope
coroutineScope 是 Kotlin 协程中一个关键的挂起函数,它在实现结构化并发方面起着重要作用。
coroutineScope 函数的主要功能是创建一个新的协程作用域,该作用域会继承外部协程的上下文(如调度器、异常处理器等)。 它会等待其内部启动的所有子协程都执行完毕后,才会让当前协程继续往下执行,从而确保了协程执行的结构化和有序性
suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R
特点
- **结构化并发
**coroutineScope 严格遵循结构化并发原则。在其作用域内启动的所有子协程形成一个“工作单元”。如果其中一个子协程抛出未捕获的异常 ,整个 coroutineScope 作用域会被取消,其他子协程也会被终止, 这样可以避免出现“孤儿协程”,即那些在父协程结束后仍在运行的协程,从而提高代码的健壮性和可维护性 。 - **挂起特性
**由于 coroutineScope 是挂起函数,它不会阻塞线程,而是挂起当前协程的执行。在等待子协程完成的过程中,线程可以去执行其他任务,提高了线程的利用率。 - **上下文继承
**coroutineScope 会继 承其父协程的上下文 。例如,如果父协程使用 Dispatchers.IO 调度器,那么 coroutineScope 内的子协程默认也会在 Dispatchers.IO 调度器指定的线程上执行。不过,在 coroutineScope 内部可以使用 withContext 等方式切换上下文。
使用场景
- **分组执行任务
**当需要同时执行多个相关的协程任务,并确保所有任务都完成后再继续后续操作时,可以使用 coroutineScope。比如,从多个数据源获取数据,然后对这些数据进行汇总处理。 - **异常统一处理
**coroutineScope 可以将一组相关的协程任务作为一个整体进行异常处理。当其中一个子协程抛出异常时,可以统一捕获和处理,避免异常在代码中分散处理导致的复杂性。
import kotlinx.coroutines.*
suspend fun fetchData() = coroutineScope {
// 启动第一个子协程获取数据
val data1 = async {
delay(1000)
"Data from source 1"
}
// 启动第二个子协程获取数据
val data2 = async {
delay(1500)
"Data from source 2"
}
// 汇总数据
val combinedData = listOf(data1.await(), data2.await())
combinedData
}
fun main() = runBlocking {
val result = fetchData()
println("汇总数据: $result")
}
//子协程 2 抛出了一个异常,coroutineScope 会捕获这个异常并取消整个作用域,
//其他子协程也会被终止。通过 try-catch 块可以捕获并处理这个异常
import kotlinx.coroutines.*
suspend fun performTasks() = coroutineScope {
// 启动第一个子协程
val job1 = launch {
delay(1000)
println("子协程 1 执行完毕")
}
// 启动第二个子协程,抛出异常
val job2 = launch {
delay(500)
throw RuntimeException("子协程 2 抛出异常")
}
try {
// 等待所有子协程完成
job1.join()
job2.join()
} catch (e: Exception) {
println("捕获到异常: ${e.message}")
}
}
fun main() = runBlocking {
performTasks()
println("主协程继续执行")
}
5.自定义作用域
你可以创建自己的 CoroutineScope 实例,通常与类的实例绑定
lass MyManager {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
fun doWork() {
scope.launch {
// 执行工作
}
}
fun cleanup() {
scope.cancel() // 取消所有在该作用域内启动的协程
}
}
正确使用协程作用域是编写健壮、无内存泄漏代码的关键。基本原则是:
- 永远不要使用 ****
GlobalScope。 - 根据生命周期选择作用域 :在 ****
ViewModel****中用 ****viewModelScope,在 ****Activity/Fragment****中用 ****lifecycleScope。 - 自定义类应持有自己的
CoroutineScope,并在不再需要时显式取消它
2.协程的上下文
协程的上下文( CoroutineContext )是一个非常重要的概念,它定义了协程执行的环境和规则。下面为你详细介绍协程上下文的组成、作用、使用方式以及一些常见的上下文元素。
1.job
代表协程的生命周期,用于控制协程的启动、取消和等待完成。
Job的分类:
1.job接口
Job 是一个最基础的接口,它代表协程的生命周期,可以用来控制协程的启动、取消和等待完成。 Job 接口定义了协程的基本操作和状态,主要方法和属性包括:
-
start():启动协程,如果协程还未启动的话。cancel(cause: CancellationException? = null):取消协程,可以传入一个可选的取消原因。join():挂起当前协程,直到目标协程完成。isActive:判断协程是否处于活跃状态。isCompleted:判断协程是否已经完成。isCancelled:判断协程是否已经被取消。
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
delay(1000)
println("协程执行完毕")
}
println("协程是否活跃: ${job.isActive}")
job.cancel()
println("协程是否被取消: ${job.isCancelled}")
job.join()
}
2.Deferred<T> 接口
Deferred<T> ****是 ****Job ****的子接口,它在 ****Job ****的基础上增加了获取协程执行结果的功能。 Deferred<T> ****通常由 ****async ****函数返回,代表一个异步计算操作,最终会返回一个类型为 ****T ****的结果。主要方法是 ****await() :
await():这是一个挂起函数,用于等待异步计算完成并返回结果。如果计算过程中抛出异常,await()会重新抛出该异常。
import kotlinx.coroutines.*
fun main() = runBlocking {
val deferred: Deferred<Int> = async {
delay(1000)
42
}
val result = deferred.await()
println("协程执行结果: $result")
}
3.SupervisorJob
SupervisorJob 是 Job 的一种特殊实现,它与普通 Job 的主要区别在于异常处理和取消传播机制。在普通 Job 中,如果一个子协程抛出异常,会导致整个父协程及其所有子协程被取消;而在 SupervisorJob 中,一个子协程的异常不会影响其他子协程,只会取消抛出异常的子协程本身。
import kotlinx.coroutines.*
fun main() = runBlocking {
val supervisorJob = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + supervisorJob)
scope.launch {
try {
delay(1000)
throw RuntimeException("子协程 1 抛出异常")
} catch (e: Exception) {
println("捕获到子协程 1 的异常: ${e.message}")
}
}
scope.launch {
delay(2000)
println("子协程 2 正常执行完毕")
}
supervisorJob.join()
}
4.CompletableJob
CompletableJob ****也是 ****Job ****的一种特殊实现,它提供了额外的方法来手动完成或取消 ****Job 。主要方法包括:
complete():手动将 ****Job****标记为完成状态。completeExceptionally(exception: Throwable):手动将Job标记为因异常而完成
import kotlinx.coroutines.*
fun main() = runBlocking {
val completableJob = Job() as CompletableJob
launch {
delay(1000)
completableJob.complete()//通过 CompletableJob 的 complete()
//方法手动将 Job 标记为完成状态
}
completableJob.join()
println("Job 已完成")
}
2.CoroutineDispatcher
决定协程在哪个线程或线程池上执行。 常见的调度器有:
Dispatchers.Main:用于 Android 主线程,只能在 Android 环境中使用。Dispatchers.IO:用于处理阻塞式 I/O 操作,如文件读写、数据库查询、网络请求等。Dispatchers.Default:用于 CPU 密集型操作,背后是一个固定大小的线程池。Dispatchers.Unconfined:不指定执行线程,协程会在调用它的线程上开始执行,直到遇到第一个挂起函数,之后由恢复它的线程继续执行。
3.CoroutineName
为协程指定一个名称,方便调试和日志记录。
4.CoroutineExceptionHandler
可以捕获协程中未处理的异常,避免程序崩溃。
import kotlinx.coroutines.*
fun main() = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("捕获到异常: ${exception.message}")
}
val job = launch(exceptionHandler) {
throw RuntimeException("模拟异常")
}
job.join()
}
5.使用方式
1. 创建协程时指定上下文
import kotlinx.coroutines.*
fun main() = runBlocking {
// 指定协程的调度器为 Dispatchers.Default
val job = launch(Dispatchers.Default) {
println("协程在 Default 调度器上执行")
}
job.join()
}
2.合并上下文元素
可以使用 + 运算符将多个上下文元素合并成一个新的上下文。
import kotlinx.coroutines.*
fun main() = runBlocking {
val context = Dispatchers.Default + CoroutineName("myCoroutine")
val job = launch(context) {
println("协程名称: ${coroutineContext[CoroutineName]?.name}")
}
job.join()
}
3. 使用 withContext 切换上下文
withContext 是一个挂起函数,用于在协程执行过程中切换上下文。
import kotlinx.coroutines.*
fun main() = runBlocking {
println("当前线程: ${Thread.currentThread().name}")
// 切换到 IO 调度器执行
withContext(Dispatchers.IO) {
println("切换到 IO 线程: ${Thread.currentThread().name}")
}
println("回到原线程: ${Thread.currentThread().name}")
}
使用注意事项:
- 上下文合并规则:当使用
+运算符合并上下文元素时,如果有相同类型的元素,后面的元素会覆盖前面的元素。 - 异常处理 :不同的上下文元素对异常的处理方式不同,需要根据具体情况选择合适的异常处理机制。
- 线程安全:在切换上下文时,要注意线程安全问题,避免出现数据竞争和并发问题。
6.作用域和上下文的关系
K otlin 协程中,上下文( CoroutineContext )和作用域( CoroutineScope )是两个核心概念,它们紧密相关,共同协作来管理协程的执行和生命周期。
- 协程上下文(
CoroutineContext):它是一个元素的集合,这些元素定义了协程执行的环境和规则。常见的上下文元素包括Job(管理协程的生命周期)、CoroutineDispatcher(决定协程在哪个线程或线程池上执行)、CoroutineName(为协程指定名称)、CoroutineExceptionHandler(处理协程中未捕获的异常)等。 - 协程作用域(
CoroutineScope):它用于管理协程的生命周期,确保协程在特定的生命周期内运行,并在该生命周期结束时自动取消所有相关的协程,避免内存泄漏和资源浪费。每个协程都必须在一个作用域内启动。
- 作用域包含上下文
每个 CoroutineScope 都包含一个 CoroutineContext,作用域通过这个上下文来确定协程的执行环境和规则。当在作用域内启动协程时,协程会继承该作用域的上下文。
import kotlinx.coroutines.*
//customScope 包含了一个上下文,其中指定了调度器为 Dispatchers.Default
//和协程名称为 "CustomScope"。
//在 customScope 内启动的协程会继承这个上下文,从而在指定的调度器上执行,并拥有指定的协程名称
fun main() = runBlocking {
// 创建一个自定义作用域,指定上下文
val customScope = CoroutineScope(Dispatchers.Default + CoroutineName("CustomScope"))
customScope.launch {
println("协程名称: ${coroutineContext[CoroutineName]?.name}")
println("调度器: ${coroutineContext[CoroutineDispatcher]}")
}
delay(1000)
}
2.上下文影响作用域内协程的行为。
上下文的不同元素会影响作用域内协程的行为。例如, Job 元素决定了协程的生命周期管理方式, CoroutineDispatcher 决定了协程在哪个线程或线程池上执行。
import kotlinx.coroutines.*
//scope 的上下文包含了一个 Job,当取消这个 Job 时,作用域内的所有协程都会被取消
fun main() = runBlocking {
val job = Job()
val scope = CoroutineScope(job + Dispatchers.Default)
val childJob = scope.launch {
delay(1000)
println("子协程执行完毕")
}
// 取消作用域的 Job,会导致作用域内的协程被取消
job.cancel()
childJob.join()
}
3.作用域决定上下文的生命周期
作用域的生命周期会影响其包含的上下文的生命周期。当作用域被取消时,其上下文也会被取消,从而导致作用域内的所有协程被取消。例如,在 Android 中使用 viewModelScope ,当 ViewModel 被清除时, viewModelScope 会被取消,其上下文也会被取消,所有在 viewModelScope 内启动的协程都会被自动取消。
4.可以在作用域内修改上下文
在作用域内启动协程时,可以通过 withContext 等方式修改协程的上下文。
import kotlinx.coroutines.*
//scope 的默认上下文是 Dispatchers.Default,但在协程内部通过 withContext(Dispatchers.IO)
//切换到了 Dispatchers.IO 上下文执行任务
fun main() = runBlocking {
val scope = CoroutineScope(Dispatchers.Default)
scope.launch {
// 在协程内部切换上下文
withContext(Dispatchers.IO) {
println("在 IO 调度器上执行")
}
}
delay(1000)
}
上下文和作用域是 Kotlin 协程中相辅相成的两个概念。上下文为协程提供了执行的环境和规则,而作用域则负责管理协程的生命周期。它们共同确保了协程能够高效、安全地执行。
3.执行原理
基于状态机执行。