文章目录
- 一. 基本语法
- 二. 协程
- 1. 协程是什么
- 2. 协程的启动
- 3. 协程的挂起和阻塞概念
- 4. 理解Suspend关键字
- 5. Job和SupervisorJob
- 6. CoroutineDispatcher调度器
- 7. CoroutineContext 上下文组合规则与元素合并原理
- 8. 协程的启动模式
- 9. 协程的作用域
- 10. 讲讲 Continuation 接口与 suspend 挂起函数底层状态机实现
- 11. 协程非阻塞的底层本质,为什么线程做不到高并发
- 12. coroutineScope 与 supervisorScope 核心区别 & 源码行为
- 13. GlobalScope 为什么不推荐使用?会引发什么问题
- 14. viewModelScope 底层 Job 类型 & 设计原因
- 15. Dispatchers.IO 与 Default 线程池机制 & 选型规范
- 16. withContext 原理、和 async+await 对比 & 最佳实践
- 17. Job 完整生命周期 & cancel () 与 cancelChildren () 区别
- 18. launch 与 async 异常抛出差异 底层原因
- 19. CoroutineExceptionHandler 使用限制与生效条件
- 20. 深刻解释协程的实现原理
- 三. Flow
协程的资料可参考该文章第五节
一. 基本语法
1. 泛型的Out和In关键字
- Out:协变——生产者
协变:只能读取不能写入
Out T等价于 ? extends T
案例1:支持协变的List - In:逆变——消费者
逆变:只能写入不能读取
In T等价于 ? super T
案例2:支持逆变的Comparator
2. UnsafeVariance
UnsafeVariance 是一个特殊的注解(annotation),用于处理泛型类型参数的协变(covariance)和逆变(contravariance)问题。它通常用于在编译器无法自动推断出类型安全的情况下,强制告诉编译器忽略某些类型检查,从而允许协变或逆变的使用。
public interface List<out E> : Collection<E> {
override val size: Int
override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
public operator fun get(index: Int): E
}
3. lateinit和by lazy
- 二者都用于延迟初始化,差异如下:

- by lazy的具体使用场景
// VIEWMODEL 的懒加载初始化(推荐方式)
private val viewModel: MyViewModel by lazy {
ViewModelProvider(this).get(MyViewModel::class.java)
}
//单例数据库实例懒加载(ROOM 数据库)
val database: AppDatabase by lazy {
Room.databaseBuilder(App.instance, AppDatabase::class.java, "app_db").build()
}
4. 扩展方法及其原理
扩展方法(Extension Function) 是一种允许我们为已有类添加新方法的机制,而无需继承该类或使用装饰器等设计模式。
- 原理
Kotlin 编译器在编译时会将扩展函数转换为带有接收者参数的静态函数。
实际上并没有修改原有类的结构,只是在调用处将接收者作为第一个参数传入。
扩展函数不能访问类的私有成员,因为它不是类的内部成员。 - 使用场景
简化 View 操作
为 Context 或 Activity 添加常用功能
为数据类型添加格式化方法
5. 如何理解委托
把一个对象的职责委托给另外一个对象
- 属性委托:by lazy
- 类的委托:通过by关键字
- 使用场景:
使用 lazy 委托延迟初始化 View 或资源
使用委托简化 ViewModel 或 Repository 的获取
6. 内联
- 定义
编译时把调用代码插入到函数中去,避免方法调用的开销 - inline: 内联,将函数体插入调用处,提升性能
- noinline:避免参数被内联
- crossline:限制非局部返回
7. 高阶函数
高阶函数是指可以接收其他函数作为参数或者返回一个函数的函数
- 案例:
let,run,also这些关键字
8. 伴随对象
Kotin中没有静态成员或函数,伴随对象Companion object用于“没有类实例的情况下但需要访问类内部的函数”。
单例模式
如果需要与java代码集成,加上注解@JvmStatic
9. 泛型实化
inline + Reified实现泛型实化,用于在运行时直接获取泛型类型参数的具体类型的
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
fun <T> create(serviceClass: Class<T>):T = retrofit.create(serviceClass);
inline fun<reified T> create() :T = create(T::class.java) // 不加refiled inline T::class.java这里会报错
10. Unit与void的区别
Unit 是 Kotlin 中的一个 对象(object),表示没有有意义的返回值
public object Unit {
override fun toString() = "kotlin.Unit"
}
void 是 Java 的关键字,表示函数不返回任何值。它不是对象,也不是类型,不能作为参数或变量类型使用。
11. lambda
12. run,let,also,with,apply高阶函数总结

13. 如何理解by关键字
by 是 Kotlin 属性委托语法,意思是:这个属性的 get/set 逻辑,交给右边那个对象来接管。
- by lazy
by 含义:data 的 get 逻辑交给 Lazy 对象
行为:第一次访问 data 的时候,才执行 lambda 算出值;之后永远缓存这个结果,不再执行
val data by lazy { "hello" }
// 类似于
private val lazyObj = lazy { "hello" }
val data: String
get() = lazyObj.getValue(this, ::data)
- by viewModels ()
by 含义:vm 这个属性的 get,交给这个 ViewModelLazy 对象托管
当你第一次读取 vm → 委托对象去 ViewModelProvider 里面拿:
val vm: TestViewModel by viewModels()
// 脱糖理解
private val delegate = viewModels<TestViewModel>()
val vm: TestViewModel
get() = delegate.getValue(this, ::vm)
- var count by mutableStateOf(0)
MutableState 本身就实现了 getValue、setValue 两个函数,所以支持 var 可变委托。
行为:
读 count → 读 stateObj.value
写 count=xxx → 写 stateObj.value = xxx
写的时候,MutableState 自带通知,触发 Compose 重组
val stateObj = remember { mutableStateOf(0) }
var count by stateObj
// 脱糖
var count: Int
get() = stateObj.getValue(this, ::count)
set(newVal) = stateObj.setValue(this, ::count, newVal)
演示代码
class CustomLazy <T> (val init: () -> T){
private var cache: T? = null
/**
* Kotlin魔法约定:只要你的委托类里面,存在一个标记 operator、签名符合规则的 getValue,编译器就认为:这个类可以用来做 by 属性委托。
* 属性委托,必须用 operator 修饰 getValue / setValue,编译器才识别它能配合 by
* thisRef: Any?:这个属性属于哪个对象
* property: KProperty<*>: 当前这个属性本身的反射信息
*/
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
// 第一次访问,执行lambda,存起来;之后直接返回缓存
if (cache == null) {
cache = init()
println("第一次初始化:${property.name}")
}
return cache!!
}
}
fun <T> myLazy(init: () -> T) = CustomLazy(init)
// 使用
fun main() {
val msg by myLazy {
println("执行初始化逻辑")
"hello delegate"
}
println("还没访问 msg,不会执行初始化")
println(msg)
println(msg) // 第二次访问,直接拿缓存,不再走lambda
}
二. 协程
1. 协程是什么
官方回答:协程视为一种轻量级线程,可用于提高并发代码的性能
关键词:轻量级,并发
轻量级:它不映射到本机线程,因此不需要在处理器上进行上下文切换,因此协程速度更快(线程由操作系统管理,协程由用户管理)
结构化并发:Kotlin 协程支持结构化并发模型,通过 CoroutineScope 来管理协程的生命周期,确保所有协程在完成或取消时不会泄漏。
并发与并行的区别
一手画圆,一手画方,两只手同时操作,左右互搏,这个是并行;但是呢,我先左手画一笔,右手画一笔,同一时候只有一只手在操作,来回交替,直到完成图案,这个就是并发
支持以同步的方式编写异步代码
协程是更高效和更简单的方式管理并发的框架,其轻量级线程编写在实际线程框架之上,通过利用函数的协作性质来充分利用它
2. 协程的启动
launch / async / runBlocking / coroutineScope / supervisorScope

3. 协程的挂起和阻塞概念
-
挂起
挂起是协程中的概念,指协程暂停自身的执行,但并不会阻塞底层线程。
挂起的非阻塞式指的是它能用看起来阻塞的代码写出非阻塞的操作
协程在执行到有 suspend 标记的函数的时候,会被 suspend 也就是被挂起,而所谓的被挂起,就是切个线程;挂起函数在执行完成之后,协程会重新切回它原先的线程。 -
阻塞
阻塞是指线程在执行某个操作时被暂停,直到该操作完成,而不能执行其他任务。比如sleep方法
4. 理解Suspend关键字
-
定义
用于标记可挂起函数,使其能在不阻塞线程的前提下暂停和恢复执行 -
工作原理
当你在 Kotlin 中使用 suspend 修饰一个函数时,Kotlin 编译器会在编译阶段将该函数转换为一个状态机(一个实现了 Continuation 接口的类),这个状态机用于管理函数的挂起与恢复。 -
使用限制
其他挂起函数内部
协程作用域内(如 launch、async、runBlocking)
若挂起函数内部无实际挂起逻辑(如未调用 delay 或 withContext),编译器会提示 redundant suspend modifier(多余的 suspend 修饰符) -
suspend 仅标记函数可挂起,线程切换需主动调用调度器(如 withContext)
-
挂起函数定义
suspend 修饰的函数称为挂起函数,表示该函数内部可能存在耗时操作(如网络请求、文件读写) -
常见挂起函数

5. Job和SupervisorJob
- Job
Job 是协程中最基本的接口,用于表示一个可取消的工作单元
如果一个子协程异常失败,会取消其父 Job 及其他兄弟协程。
适用场景:任务之间有强依赖关系 - SupervisorJob
SupervisorJob 是 Job 的子接口,但行为不同:子协程的失败不会影响其他子协程或父协程。
使用场景:APP的首页,首页上展示的数据五花八门。如:广告,弹窗,未读状态
6. CoroutineDispatcher调度器
- Dispatchers.Main
用于 Android 的主线程(UI 线程),适合执行更新 UI 的操作。 - Dispatchers.IO
用于执行 I/O 密集型任务(如网络请求、文件读写等) - Dispatchers.Default
用于 CPU 密集型任务(如数据计算、图像处理等) - Dispatchers.Unconfined
不限制执行的线程,协程会在调用它的线程中运行,但恢复时可能在其他线程
7. CoroutineContext 上下文组合规则与元素合并原理
- CoroutineContext是不可变集合式接口,采用键值对结构存储各类元素:Job、Dispatcher、异常处理器、协程名称等;
- 支持+运算符做上下文叠加,同 Key 元素会后者覆盖前者,不同 Key 元素相互合并;
- 子协程默认继承父协程全部上下文;
- 主动指定Dispatcher/Job会覆盖父级对应元素,CoroutineExceptionHandler、协程名称等默认继承。
8. 协程的启动模式
- DEFAULT 默认启动模式
饿汉启动模式,协程创建后立即开始调度 - LAZY 懒汉启动模式
懒汉启动模式,启动后并不会有任何调度行为,直到我们需要它执行的时候才会产生调度(主动的调用Job的start、join或者await等函数) - ATOMIC
在协程创建后立即开始调度。执行到第一个挂起点之前是不响应cancel 取消操作的,ATOMIC一定要涉及到协程挂起后cancel 取消操作的时候才有意义 - UNDISPATCHED
直接开始在当前线程下执行,直到运行到第一个挂起点。不经过任何调度器就开始执行的。当然遇到挂起点之后的执行,将取决于挂起点本身的逻辑和协程上下文中的调度器
(没有遇到挂起点前,在当前线程下执行;遇到挂起点后,看挂起点具体的场景)
9. 协程的作用域
- 顶级作用域
没有父协程的协程所在的作用域称之为顶级作用域 - 协同作用域
在协程中启动一个协程,新协程为所在协程的子协程。子协程所在的作用域默认为协同作用域。此时子协程抛出未捕获的异常时,会将异常传递给父协程处理,如果父协程被取消,则所有子协程同时也会被取消。 - 主从作用域(监督作用域)
与协同作用域一致,区别在于该作用域下的协程取消操作的单向传播性,子协程的异常不会导致其它子协程取消。但是如果父协程被取消,则所有子协程同时也会被取消。
10. 讲讲 Continuation 接口与 suspend 挂起函数底层状态机实现
- Continuation 是协程挂起与恢复的核心回调接口,封装了协程后续执行逻辑、上下文、异常回调,是挂起函数的载体。
- 所有suspend挂起函数,编译期会被 Kotlin 编译器改写,自动新增Continuation类型入参,同时生成状态机代码,通过label标记代码执行位置。
- 协程挂起时:保存当前执行现场、局部变量、label 状态,交出线程使用权;
- 任务完成后:通过回调Continuation.resume/resumeWithException恢复状态机,从上次挂起点继续执行。
- 非协程环境无法调用挂起函数,本质是没有自动生成与传递 Continuation,缺少恢复回调。
11. 协程非阻塞的底层本质,为什么线程做不到高并发
- 线程由 OS 内核调度,上下文切换成本高、内存占用大,线程池数量有限;
- 协程由用户态调度,运行在线程之上,多个协程复用同一个线程,挂起仅保存少量栈信息,无内核态切换开销;
- 非阻塞核心:挂起不阻塞线程,线程可立刻复用执行其他协程任务;
- 单一线程可承载成千上万个挂起协程,因此协程支持海量并发,资源开销远低于线程
12. coroutineScope 与 supervisorScope 核心区别 & 源码行为
- coroutineScope:遵循标准结构化并发,子协程异常会向上传播,直接取消父作用域及所有兄弟协程,全部任务终止;
- supervisorScope:基于SupervisorJob,子协程异常隔离,单个子任务崩溃不影响其他子协程与父作用域;
- 两者都会阻塞当前协程,等待内部所有子协程执行完毕才结束;
- Android 中页面独立埋点、多模块并行请求、互不依赖的 UI 附属任务,统一使用supervisorScope
13. GlobalScope 为什么不推荐使用?会引发什么问题
- GlobalScope属于全局顶级作用域,无绑定生命周期,不受页面 / 组件生命周期管控
- 无结构化并发约束,页面销毁后协程仍持续运行,引发内存泄漏、空指针崩溃、无效 UI 更新
- 无法统一取消,批量任务管理困难,异常无法统一捕获
- 安卓规范:Activity 用lifecycleScope、ViewModel 用viewModelScope、仓库层自定义业务
Scope,彻底替代 GlobalScope
14. viewModelScope 底层 Job 类型 & 设计原因
- viewModelScope内部默认绑定SupervisorJob + Dispatchers.Main;
- 设计目的:页面部分接口失败不杀死整个 ViewModel 所有任务,避免单一请求崩溃导致整个页面功能瘫痪;
- ViewModel 销毁时自动触发 Job 取消,自动终止所有协程,杜绝泄漏;
- 适合 MVVM 架构下多接口并行请求、独立业务任务解耦。
15. Dispatchers.IO 与 Default 线程池机制 & 选型规范
- Dispatchers.Default:固定核心线程池,适配CPU 密集型任务,如数据解析、图片压缩、复杂计算;
- Dispatchers.IO:动态扩容线程池,按需创建回收线程,适配IO 密集型:网络请求、文件读写、数据库操作;
- 错误选型危害:CPU 任务用 IO 会造成线程泛滥、频繁切换;IO 任务用 Default 会堵塞计算线程池;
- 两者底层共享公共调度器核心,只是线程池扩容策略不同。
16. withContext 原理、和 async+await 对比 & 最佳实践
- withContext是挂起函数,作用为切换上下文 / 线程,无额外任务容器,执行完自动切回原线程;
- 无需手动管理 Job,自动跟随外层作用域取消,代码更简洁;
- 对比async+await:单任务线程切换优先用 withContext;多任务并行、需要获取多个返回值时使用 async;
- 项目规范:串行网络、数据库、文件操作统一使用 withContext (Dispatchers.IO)。
17. Job 完整生命周期 & cancel () 与 cancelChildren () 区别
- Job 生命周期:New→Active→Completing→Completed/Cancelled;
- cancel ():取消当前 Job+全部子 Job,整个协程树终止;
- cancelChildren ():仅取消所有子协程,当前父协程继续运行;
- 协程一旦进入 Cancelled 状态,不可重启,需重新创建协程实例。
18. launch 与 async 异常抛出差异 底层原因
- launch:独立任务型设计,异常立即向上传播,若无捕获直接触发崩溃;
- async:结果返回型设计,异常被封装在 Deferred 内部,只有调用 await () 才会抛出;
- 设计初衷:launch 用于单向执行任务,async 用于需要返回结果的异步任务,异常延迟抛出;
- 并行 async 任务建议统一 try-catch 包裹 await,避免隐性崩溃。
19. CoroutineExceptionHandler 使用限制与生效条件
- 只能捕获作用域内未被 try-catch 捕获、未被消费的顶级协程异常;
- 无法捕获子协程已捕获的异常、CancellationException 取消异常;
- 异常处理器遵循就近原则,子上下文 Handler 优先于父级;
- 仅对 launch 有效,async 异常不会经过该处理器,必须通过 await 捕获。
20. 深刻解释协程的实现原理
- 协程一定跑在线程上:挂起不代表线程被挂起,只是这段协程逻辑暂停,把线程让出来去执行别的任务。
- 挂起的本质:suspend 经编译器改写为“状态机 + Continuation”。遇到挂起点时保存现场并返回COROUTINE_SUSPENDED。
- 恢复的本质:挂起条件满足后(delay 到期/IO 回调返回),并不是“立刻抢回原线程A”,而是 resume 后交给Dispatcher.dispatch(…),把 continuation 包装成任务投递/入队。
- 线程A忙怎么办:不会抢占、不并发插入执行;恢复任务进入队列,等线程A空闲(当前任务执行完)再执行。
- 是否必须回到同一条物理线程A:一般不保证。
- Dispatchers.IO/Default:只保证回到同一线程池,可能换线程继续执行。
- Dispatchers.Main:保证回到主线程(同一条主线程),通过Looper/MessageQueue 排队执行。
- 若确实需要固定到同一条工作线程:用单线程 dispatcher(single-thread executor),恢复同样靠排队等待。
- 内核/Linux 的角色:主要参与定时/IO 等“等待事件”的底层能力;协程的挂起/恢复语义本身在用户态由 continuation +调度队列完成,而非内核去“挂起/恢复线程”。
三. Flow
1. Flow工作原理
Flow 的核心设计基于 响应式流 (默认冷流 )的概念,原理上是 kotlinx.coroutines 库的一部分。它有三个主要角色:
生产者(Producer):负责发送数据流(通过 flow { emit() } 实现)
中间处理(Intermediate):对数据进行转换、过滤等操作(如 map、filter)
消费者(Collector):接收并处理数据(通过 collect 启动)
2. Cold Flow冷流和Hot Flow热流
- 冷流
定义:冷流只有在有消费者(collect)开始收集数据时才会开始执行。
典型实现类:flow (Flow) // Observable (Rxjava)
使用场景:适合一次性任务,如网络请求、数据库查询等。 - 热流
定义:热流无论是否有消费者收集数据,都会持续发射数据。
典型实现类:SharedFlow、StateFlow // Subject, livedata
使用场景:适合广播事件、UI 状态共享、传感器数据等需要实时共享的场景。
3. StateFlow和SharedFlow
- StateFlow
StateFlow 是一种始终有值的热流,它保存当前状态,并将最新值发送给所有活跃的收集者。它的行为类似于 LiveData,但基于协程。
应用场景:持有并共享 UI 状态(ViewModel 状态管理) - SharedFlow
SharedFlow 是一个 广播式事件流,可以发送多个值给多个收集者,是一种更通用的热流。
应用场景:分发一次性事件(Toast、导航)
4. Flow 背压产生原因 & buffer/conflate/collectLatest 区别
- 背压:上游数据发送速度 > 下游消费速度,数据堆积导致内存占用过高;
- buffer:设置缓存池,缓存上游数据,异步消费,避免阻塞生产者;
- conflate:保留最新数据,丢弃老旧积压数据,适合实时状态刷新;
- collectLatest:新数据到来时,取消上一次未完成的消费逻辑,适合搜索、列表刷新等场景。
5. flowOn 操作符线程切换规则 为什么只影响上游
- flowOn 仅改变上游生产者、中间操作符的执行线程;
- 下游 collect 收集代码始终运行在当前协程上下文线程;
- 原理:Flow 是冷流懒加载设计,上下游执行环境隔离,各自绑定独立调度器;
- 多段 flowOn 叠加时,以最近的 flowOn为准覆盖上游线程。
6. StateFlow 数据倒灌原因 & 解决方案
- 倒灌根源:StateFlow 默认保存当前最新值,新订阅者连接后会立刻回放历史值;
- 劣势:一次性事件(弹窗、Toast、路由)重复触发;
- 解决方案:一次性事件改用 SharedFlow、添加事件消费标记、使用distinctUntilChanged过滤重复状态;
- 架构规范:UI 状态用 StateFlow,事件型通知强制用 SharedFlow。
5028



被折叠的 条评论
为什么被折叠?



