首先要了解的就是CPS转换。
CPS转换 在Kotlin协程中,挂起函数的执行是通过 Continuation Passing Style (CPS)转换 来实现的。CPS转换是一种将函数式编程中的函数调用转换为可传递的 Continuation 对象的过程。这里的转换是Kotlin编译器实现的,在跨平台属性上,也保证了流程的一致性。
CPS转换调用的过程如下:
当一个函数被调用时,它的参数和返回值会被封装在一个Continuation对象中。 函数的执行过程中,遇到挂起操作时,会将当前的Continuation对象传递给挂起函数。 挂起函数执行完毕后,会将结果封装在一个新的Continuation对象中,并将其传递给原始的Continuation对象。 原始的Continuation对象会继续执行,直到所有的挂起操作都完成。 假设我们有一个简单的suspend函数,它模拟了一个异步操作:
suspend fun fetchData (): String {
delay ( 1000 ) // 模拟耗时操作
return "Data fetched"
}
在CPS转换后,这个函数可能会被转换为类似以下的形式:
fun fetchData ( continuation : Continuation < String >) {
delay ( 1000 , object : Continuation < Unit > {
override val context : CoroutineContext = continuation . context
override fun resumeWith ( result : Result < Unit >) {
continuation . resume ( "Data fetched" )
}
})
}
在这个转换后的函数中,fetchData不再直接返回结果,而是通过continuation.resume方法将结果传递给调用者。简单来说,CPS其实就是函数通过回调传递结果的一种方式。
Kotlin协程通过将异步流程拆解为一系列 挂起点 ,对含有 suspend 关键字的函数进行了 CPS转换 ,即Continuation Passing Style转换,使其能够 接收Continuation对象 作为参数,并在异步操作完成后通过调用 Continuation 的恢复方法来继续执行协程。
在编译后的字节码中,协程的状态会被转换为状态机的形式,每个挂起点对应状态机的一个状态。当协程挂起时,它的执行状态会被保存在Continuation对象中,包括局部变量上下文和执行位置。
Continuation Continuation (续体)是一个保存协程状态的对象,它记录了协程挂起的位置以及局部变量上下文,使得协程可以在任何时候从上次挂起的地方继续执行。Continuation是一个接口,它定义了 resumeWith 方法,用于恢复协程的执行。
interface Continuation < in T > {
val context : CoroutineContext
fun resumeWith ( result : Result < T >) //result 为返回的结果
}
续体是一个较为抽象的概念,简单来说它包装了协程在挂起之后应该继续执行的代码;在编译的过程中,一个完整的协程被分割切块成一个又一个续体。 在suspend函数或者 await 函数的挂起结束以后,它会调用 continuation 参数的 resumeWith 函数,来恢复执行suspend函数或者await 函数后面的代码。 CPS转换 使得协程能够在不阻塞线程的情况下执行异步操作。当协程挂起时,线程可以被释放去执行其他任务,从而提高了系统的并发性能。此外,CPS转换使得协程的挂起和恢复操作对开发者来说是透明的,开发者可以像编写同步代码一样编写异步代码。
发生 CPS 变换的函数,返回值类型变成了 Any?,这是因为这个函数在发生变换后,除了要返回它本身的返回值,还要返回一个标记CoroutineSingletons.COROUTINE_SUSPENDED,为了适配各种可能性,CPS 转换后的函数返回值类型就只能是 Any?了。
协程的启动 下面跟随启动,挂起,恢复的流程,从源码层面看看协程的核心原理。
测试代码入口:
object CoroutineExample {
private val TAG : String = "CoroutineExample"
fun main (){
// 启动协程,分析入口
GlobalScope . launch ( Dispatchers . Main ) {
request ()
}
}
private suspend fun request (): String {
delay ( 2000 )
Log . e ( TAG , "request complete" )
return "result from request"
}
}
从 CoroutineScope.launch 开始:
public fun CoroutineScope . launch (
context : CoroutineContext = EmptyCoroutineContext ,
start : CoroutineStart = CoroutineStart . DEFAULT ,
block : suspend CoroutineScope .() -> Unit
): Job {
val newContext = newCoroutineContext ( context )
val coroutine = if ( start . isLazy ){
LazyStandaloneCoroutine ( newContext , block )
} else {
StandaloneCoroutine ( newContext , active = true )
}
coroutine . start ( start , coroutine , block )
return coroutine
}
参数一context:协程上下文,并不是我们平时理解的Android中的上下文,它是一种key-value数据结构。可以传入Main用于主线程调度。 参数二start:启动模式,此处我们没有传值则为默认值(DEFAULT),共有三种启动模式。DEFAULT:默认模式,创建即启动协程,可随时取消; ATOMIC:自动模式,创建即启动协程,启动前不可取消; LAZY:延迟启动模式,只有当调用start方法时才能启动。 参数三block:协程真正执行的代码块,即上面例子中launch{}闭包内的代码块。 SuspendLambda CoroutineScope.launch中第三个参数类型为suspend CoroutineScope.() -> Unit函数,这是怎么来的呢?我们编写代码的时候并没有这个东西,其实它由编译器生成的,我们的 block代码块 经过编译器编译后会生成一个 继承Continuation 的 类SuspendLambda 。一起看下反编译的java代码,为了关注主要逻辑方便理解,去掉了一些无关代码大概代码如下:
public final void main () {
BuildersKt . launch $default (( CoroutineScope ) GlobalScope . INSTANCE , ( CoroutineContext ) Dispatchers . getMain (), ( CoroutineStart ) null , ( Function2 )( new Function2 (( Continuation ) null ) {
int label ;
@Nullable
public final Object invokeSuspend ( @NotNull Object $result ) {
Object var2 = IntrinsicsKt . getCOROUTINE_SUSPENDED ();
switch ( this . label ) {
case 0 :
ResultKt . throwOnFailure ( $result );
CoroutineExample var10000 = CoroutineExample . this ;
this . label = 1 ;
if ( var10000 . request ( this ) == var2 ) {
return var2 ;
}
break ;
case 1 :
ResultKt . throwOnFailure ( $result );
break ;
default :
throw new IllegalStateException ( "call to 'resume' before 'invoke' with coroutine" );
}
return Unit . INSTANCE ;
}
···
}
从上面反编译的java代码中好像并不能很好的看出来协程中的block代码块具体编译长什么样子,但可以确定他是编译成了 Continuation类 ,因为我们可以看到实现的 invokeSuspend 方法实际是来自BaseContinuationImpl,而BaseContinuationImpl的父类就是Continuation。这个继承关系我们后面再说。既然从反编译的java代码中看的不明显,我们直接看上面例子的字节码文件,其中可以很明显的看到这样一段代码:
final class com/imile/pda/CoroutineExample$main$1 extends kotlin/coroutines/jvm/internal/SuspendLambda implements kotlin/jvm/functions/Function2
这下恍然大悟,launch函数的第三个参数,即协程中的 block代码块 是一个编译后 继承了SuspendLambda并且实现了Function2的实例 。
SuspendLambda 本质上是一个 Continuation ,前面我们已经说过 Continuation 是一个有着恢复操作的接口,其 resume 方法可以恢复协程的执行。
SuspendLambda继承机构如下:
- Continuation: 续体,恢复协程的执行
- BaseContinuationImpl: 实现 resumeWith(Result) 方法,控制状态机的执行,定义了 invokeSuspend 抽象方法
- ContinuationImpl: 增加 intercepted 拦截器,实现线程调度等
- SuspendLambda: 封装协程体代码块
- 协程体代码块生成的子类: 实现 invokeSuspend 方法,其内实现状态机流转逻辑
每一层封装都对应添加了不同的功能,我们先忽略掉这些功能细节,着眼于我们的主线,继续跟进 launch 函数执行过程,由于第二个参数是默认值(DEFAULT),所以创建的是 StandaloneCoroutine , 最后启动协程:
// 启动协程
coroutine.start(start, coroutine, block)
// 启动协程
public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
start(block, receiver, this)
}
上面 coroutine.start 的调用涉及到运算符重载,实际上会调到 CoroutineStart.invoke() 方法:
public operator fun < R , T > invoke ( block: suspend R .() -> T , receiver: R , completion: Continuation < T >): Unit =
when ( this ) {
DEFAULT -> block . startCoroutineCancellable ( receiver , completion )
ATOMIC -> block . startCoroutine ( receiver , completion )
UNDISPATCHED -> block . startCoroutineUndispatched ( receiver , completion )
LAZY -> Unit // will start lazily
}
这里启动方式为默认的 DEFAULT ,所以接着往下看:
internal fun < R , T > ( suspend ( R ) -> T ). startCoroutineCancellable (
receiver : R , completion : Continuation < T >,
onCancellation : (( cause : Throwable ) -> Unit )? = null
) = runSafely ( completion ) {
createCoroutineUnintercepted ( receiver , completion )
. intercepted ()
. resumeCancellableWith ( Result . success ( Unit ), onCancellation )
}
整理下调用链如下:
coroutine.start(start, coroutine, block)
-> CoroutineStart.start(block, receiver, this)
-> CoroutineStart.invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>)
-> block.startCoroutineCancellable(receiver, completion)
->
createCoroutineUnintercepted(receiver,completion).intercepted().resumeCancellableWith(Result.success(Unit), onCancellation)
最后走到 createCoroutineUnintercepted(receiver,completion).intercepted().resumeCancellableWith(Result.success(Unit), onCancellation) ,这里创建了一个协程,并链式调用 intercepted、resumeCancellable 方法,利用协程上下文中的 ContinuationInterceptor 对协程的执行进行拦截,intercepted 实际上调用的是 ContinuationImpl 的 intercepted 方法:
internal abstract class ContinuationImpl (
completion : Continuation < Any ?>?,
private val _context : CoroutineContext ?
) : BaseContinuationImpl ( completion ) {
.. .
public fun intercepted (): Continuation < Any ?> =
intercepted
?: ( context [ ContinuationInterceptor ] ?. interceptContinuation ( this ) ?: this )
. also { intercepted = it }
.. .
}
context[ContinuationInterceptor]?.interceptContinuation调用的是 CoroutineDispatcher 的 interceptContinuation 方法:
public final override fun < T > interceptContinuation ( continuation : Continuation < T >): Continuation < T > =
DispatchedContinuation ( this , continuation )
内部创建了一个 DispatchedContinuation 可分发的协程实例,我们继续进到看resumeCancellableWith 方法:
internal class DispatchedContinuation < in T >(
@JvmField val dispatcher : CoroutineDispatcher ,
@JvmField val continuation : Continuation < T >
) : DispatchedTask < T >( MODE_UNINITIALIZED ), CoroutineStackFrame , Continuation < T > by continuation {
.. .
public fun < T > Continuation < T >. resumeCancellableWith (
result : Result < T >,
onCancellation : (( cause : Throwable ) -> Unit )? = null
): Unit = when ( this ) {
// 判断是否是DispatchedContinuation 根据我们前面的代码追踪 这里是DispatchedContinuation
is DispatchedContinuation -> resumeCancellableWith ( result , onCancellation )
else -> resumeWith ( result )
}
inline fun resumeCancellableWith (
result : Result < T >,
noinline onCancellation : (( cause : Throwable ) -> Unit )?
) {
val state = result . toState ( onCancellation )
// 判断是否需要线程调度
// 由于我们之前使用的是 `GlobalScope.launch(Main)` Android主线程调度器所以这里为true
if ( dispatcher . isDispatchNeeded ( context )) {
_state = state
resumeMode = MODE_CANCELLABLE
dispatcher . dispatch ( context , this )
} else {
executeUnconfined ( state , MODE_CANCELLABLE ) {
if (! resumeCancelled ( state )) {
resumeUndispatchedWith ( result )
}
}
}
}
.. .
}
最终走到 dispatcher.dispatch(context, this) 而这里的 dispatcher 就是通过工厂方法创建的 HandlerDispatcher ,dispatch() 函数第二个参数this是一个runnable这里为 DispatchedTask
HandlerDispatcher internal class HandlerContext private constructor (
private val handler : Handler ,
private val name : String ?,
private val invokeImmediately : Boolean
) : HandlerDispatcher (), Delay {
.. .
// 最终执行这里的 dispatch方法 而handler则是android中的 MainHandler
override fun dispatch ( context : CoroutineContext , block : Runnable ) {
if (! handler . post ( block )) {
cancelOnRejection ( context , block )
}
}
.. .
}
这里借用 Android 的主线程消息队列来在主线程中执行 block Runnable而这个 Runnable 即为 DispatchedTask:
internal abstract class DispatchedTask < in T >(
@JvmField public var resumeMode : Int
) : SchedulerTask () {
.. .
public final override fun run () {
.. .
withContinuationContext ( continuation , delegate . countOrElement ) {
.. .
if ( job != null && ! job . isActive ) {
val cause = job . getCancellationException ()
cancelCompletedResult ( state , cause )
// 异常情况下
continuation . resumeWithStackTrace ( cause )
} else {
if ( exception != null ) {
// 异常情况下
continuation . resumeWithException ( exception )
} else {
// step1:正常情况下走到这一步
continuation . resume ( getSuccessfulResult ( state ))
}
}
}
.. .
}
}
//step2:这是Continuation的扩展函数,内部调用了resumeWith()
@InlineOnly public inline fun < T > Continuation < T >. resume ( value : T ): Unit =
resumeWith ( Result . success ( value ))
//step3:最终会调用到BaseContinuationImpl的resumeWith()方法中
internal abstract class BaseContinuationImpl ( .. .) {
// 实现 Continuation 的 resumeWith,并且是 final 的,不可被重写
public final override fun resumeWith ( result : Result < Any ?>) {
.. .
val outcome = invokeSuspend ( param )
.. .
}
// 由编译生成的协程相关类来实现,例如 CoroutineExample$main$1
protected abstract fun invokeSuspend ( result : Result < Any ?>): Any ?
}
最终调用到 continuation.resumeWith() 而 resumeWith() 中会调用 invokeSuspend,即之前编译器生成的 SuspendLambda 中的 invokeSuspend 方法:
@Nullable
public final Object invokeSuspend ( @NotNull Object $result ) {
Object var2 = IntrinsicsKt . getCOROUTINE_SUSPENDED ();
switch ( this . label ) {
case 0 :
ResultKt . throwOnFailure ( $result );
CoroutineExample var10000 = CoroutineExample . this ;
this . label = 1 ;
if ( var10000 . request ( this ) == var2 ) {
return var2 ;
}
break ;
case 1 :
ResultKt . throwOnFailure ( $result );
break ;
}
}
这段代码是一个状态机机制,每一个挂起点都是一种状态,协程恢复只是跳转到下一个状态,挂起点将执行过程分割成多个片段,利用状态机的机制保证各个片段按顺序执行。
可以看到 协程非阻塞的异步底层实现其实就是一种Callback回调 (这一点我们在介绍Continuation时有提到过),只不过有多个挂起点时就会有多个Callback回调,这里协程把多个Callback回调封装成了一个状态机。
以上就是协程的启动过程,下面我们再来看下协程中的重点挂起和恢复。
协程的挂起与恢复 协程的挂起和恢复有两个关键方法 : invokeSuspend() 和 resumeWith(Result)。我们以上一节中的例子,反编译后逆向剖析协程的挂起和恢复,先整体看下是怎样的一个过程。
suspend fun reqeust (): String {
delay ( 2000 )
return "result from request"
}
反编译后的代码如下(为了方便理解,代码有删减和修改):
//1.函数返回值由String变成Object,入参也增加了Continuation参数
public final Object reqeust ( @NotNull Continuation completion ) {
//2.通过completion创建一个ContinuationImpl,并且复写了invokeSuspend()
Object continuation ;
if ( completion instanceof < undefinedtype >){
continuation = < undefinedtype > completion
} else {
continuation = new ContinuationImpl ( completion ) {
Object result ;
int label ; //初始值为0
@Nullable
public final Object invokeSuspend ( @NotNull Object $ result ) {
this . result = $ result ;
this . label |= Integer . MIN_VALUE ;
return request ( this ); //又调用了request()方法
}
};
}
Object $ result = ( continuation ). result ;
Object var4 = IntrinsicsKt . getCOROUTINE_SUSPENDED ();
//状态机
//3.方法被恢复的时候又会走到这里,第一次进入case 0分支,label的值从0变为1,第二次进入就会走case 1分支
switch ( continuation . label ) {
case 0 :
ResultKt . throwOnFailure ( $ result );
continuation . label = 1 ;
//4.delay()方法被suspend修饰,传入一个continuation回调,返回一个object结果。这个结果要么是`COROUTINE_SUSPENDED`,否则就是真实结果。
Object delay = DelayKt . delay ( 2000L , continuation )
if ( delay == var4 ) {
//如果是 COROUTINE_SUSPENDED 则直接return,就不会往下执行了,request()被暂停了。
// 如果不是COROUTINE_SUSPENDED,则说明不需要挂起,就会break跳出switch语句。正常返回继续往下执行后续外部代码。
return var4 ;
}
break ;
case 1 :
ResultKt . throwOnFailure ( $ result );
break ;
default :
throw new IllegalStateException ( "call to 'resume' before 'invoke' with coroutine" );
}
return "result from request" ;
}
ResultKt.throwOnFailure($result) 是 Kotlin 协程中的一个重要方法,主要用于处理协程的异常情况。它的主要作用是: (1)检查异常:它会检查传入的 $result 对象,如果这个对象是一个异常(即协程执行过程中抛出的异常),它会立即抛出这个异常。 (2)确保正常执行:如果 $result 不是异常,则继续正常执行后续代码。
挂起过程 函数返回值由 String 变成 Object,编译器自动增加了Continuation参数,相当于帮我们添加Callback。
根据 completion 创建了一个 ContinuationImpl(如果已经创建就直接用,避免重复创建),复写了 invokeSuspend() 方法,在这个方法里面它又调用了 request() 方法,这里又调用了一次自己(是不是很神奇),并且把 continuation 传递进去。
在 switch 语句中,label 的默认初始值为 0,第一次会进入 case 0 分支,delay() 是一个挂起函数,传入上面的 continuation 参数,会有一个 Object 类型的返回值。这个结果要么是COROUTINE_SUSPENDED,否则就是真实结果。
DelayKt.delay(2000, continuation)的返回结果如果是 COROUTINE_SUSPENDED , 则直接 return ,那么方法执行就被结束了,方法就被挂起了。
函数即便被 suspend 修饰了,但是也未必会挂起。需要里面的代码编译后有返回值为 COROUTINE_SUSPENDED 这样的标记位才可以。
协程的挂起实际是方法的挂起,本质是return。
恢复过程 因为 delay() 是 IO操作,在2000ms后就会通过传递给它的 continuation 回调回来。
回调到 ContinuationImpl 类的 resumeWith() 方法,会再次调用 invokeSuspend() 方法,进而再次调用 request() 方法。
即反编译代码中的这一段:
Object continuation ;
if ( completion instanceof < undefinedtype >){
continuation = < undefinedtype > completion
} else {
continuation = new ContinuationImpl ( completion ) {
Object result ;
int label ; //初始值为0
@Nullable
public final Object invokeSuspend ( @NotNull Object $result ) {
this . result = $result ;
this . label |= Integer . MIN_VALUE ;
return request ( this ); //又调用了request()方法
}
};
}
程序会再次进入switch语句,由于第一次在 case 0 时把 label = 1 赋值为1,所以这次会进入 case 1 分支,检查无异常之后,再次 break ,并且返回了结果result from request。
并且 request() 的返回值作为 invokeSuspend() 的返回值返回。重新被执行的时候就代表着方法被恢复了。
看到大家一定会疑问, 步骤2中 invokeSuspend() 是如何被再次调用呢? 我们都知道 ContinuationImpl 的父类是 BaseContinuationImpl,实际上ContinuationImpl中调用的resumeWith()是来自父类。
BaseContinuationImpl internal abstract class BaseContinuationImpl (
public val completion : Continuation < Any ?>?
) : Continuation < Any ?>, CoroutineStackFrame , Serializable {
//这个实现是最终的,用于展开 resumeWith 递归。
public final override fun resumeWith ( result : Result < Any ?>) {
var current = this
var param = result
while ( true ) {
with ( current ) {
val completion = completion !!
val outcome : Result < Any ?> =
try {
// 1.调用 invokeSuspend()方法执行,执行协程的真正运算逻辑,拿到返回值
val outcome = invokeSuspend ( param )
// 2.如果返回的还是COROUTINE_SUSPENDED则提前结束
if ( outcome == COROUTINE_SUSPENDED ) return
Result . success ( outcome )
} catch ( exception : Throwable ) {
Result . failure ( exception )
}
if ( completion is BaseContinuationImpl ) {
//3.如果 completion 是 BaseContinuationImpl,内部还有suspend方法,则会进入循环递归,继续执行和恢复
current = completion
param = outcome
} else {
//4.否则是最顶层的completion,则会调用resumeWith恢复上一层并且return
// 这里实际调用的是其父类 AbstractCoroutine 的 resumeWith 方法
completion . resumeWith ( outcome )
return
}
}
}
}
}
实际上任何一个挂起函数它在恢复的时候都会调到 BaseContinuationImpl 的 resumeWith() 方法里面。
一但 invokeSuspend() 方法被执行,那么 request() 又会再次被调用, invokeSuspend() 就会拿到 request() 的返回值,在 ContinuationImpl 里面根据 val outcome = invokeSuspend() 的返回值来判断我们的 request() 方法恢复了之后的操作。
如果 outcome 是 COROUTINE_SUSPENDED 常量(可能挂起函数中又返回了一个挂起函数),说明你即使被恢复了,执行了一下, if (outcome == COROUTINE_SUSPENDED) return但是立马又被挂起了,所以又 return 了。
如果本次恢复 outcome 是一个正常的结果,就会走到 completion.resumeWith(outcome),当前被挂起的方法已经被执行完了,实际调用的是其父类 AbstractCoroutine 的 resumeWith 方法,那么协程就恢复了。
我们知道 request() 肯定是会被协程调用的(从上面反编译代码知道会传递一个Continuation completion参数),request() 方法恢复完了就会让协程completion.resumeWith()去恢复,所以说协程的恢复是方法的恢复,本质其实是 callback(resumeWith) 回调。
一张图总结一下:
协程的核心是挂起——恢复,挂起——恢复的本质是return & callback回调
协程挂起 我们说过协程启动后会调用到上面这个 resumeWith() 方法,接着调用其 invokeSuspend() 方法:
当 invokeSuspend() 返回 COROUTINE_SUSPENDED 后,就直接 return 终止执行了,此时协程被挂起。 当 invokeSuspend() 返回非 COROUTINE_SUSPENDED 后,说明协程体执行完毕了,对于 launch 启动的协程体,传入的 completion 是 AbstractCoroutine 子类对象,最终会调用其 AbstractCoroutine.resumeWith() 方法做一些状态改变之类的收尾逻辑。至此协程便执行完毕了。
协程恢复 这里我们接着看上面第一条:协程执行到挂起函数被挂起后,当这个挂起函数执行完毕后是怎么恢复协程的,以下面挂起函数为例:
private suspend fun login () = withContext ( Dispatchers . IO ) {
Thread . sleep ( 2000 )
return @withContext true
}
通过反编译可以看到上面挂起函数中的函数体也被编译成了 SuspendLambda 的子类,创建其实例时也需要传入 Continuation 续体参数(调用该挂起函数的协程所在续体)。贴下 withContext 的源码:
public suspend fun < T > withContext (
context : CoroutineContext ,
block : suspend CoroutineScope .() -> T
): T {
return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
// 创建 new context
val oldContext = uCont . context
val newContext = oldContext + context
// 检查新上下文是否作废
newContext . ensureActive ()
// 新上下文与旧上下文相同
if ( newContext === oldContext ) {
val coroutine = ScopeCoroutine ( newContext , uCont )
return @sc coroutine . startUndispatchedOrReturn ( coroutine , block )
}
// 新调度程序与旧调度程序相同
if ( newContext [ ContinuationInterceptor ] == oldContext [ ContinuationInterceptor ]) {
val coroutine = UndispatchedCoroutine ( newContext , uCont )
// 上下文有变化,所以这个线程需要更新
withCoroutineContext ( newContext , null ) {
return @sc coroutine . startUndispatchedOrReturn ( coroutine , block )
}
}
// 使用新的调度程序
val coroutine = DispatchedCoroutine ( newContext , uCont )
block . startCoroutineCancellable ( coroutine , coroutine )
coroutine . getResult ()
}
}
首先调用了 suspendCoroutineUninterceptedOrReturn 方法,看注释知道可以通过它来获取到当前的续体对象 uCont, 接着有几条分支调用,但最终都是会通过续体对象来创建挂起函数体对应的 SuspendLambda 对象,并执行其 invokeSuspend() 方法,在其执行完毕后调用 uCont.resume() 来恢复协程,具体逻辑大家感兴趣可以自己跟代码,与前面大同小异。
至于其他的顶层挂起函数如 await(), suspendCoroutine(), suspendCancellableCoroutine() 等,其内部也是通过 suspendCoroutineUninterceptedOrReturn() 来获取到当前的续体对象,以便在挂起函数体执行完毕后,能通过这个续体对象恢复协程执行。
Desktop平台举例分析挂起恢复(Kotlin 2.1.0) Kotlin测试代码如下:
class MySimpleTest {
suspend fun stephenTest (): String {
delay ( 500L )
return "result From stephenTest"
}
}
fun callFromOutside () {
CoroutineScope ( Dispatchers . IO ). launch {
val result = MySimpleTest (). stephenTest ()
println ( result )
}
}
在callFromOutside函数中,我们创建了一个协程作用域,并在其中启动了一个协程。该协程将调用stephenTest函数。而stephenTest函数是一个挂起函数,它会暂停该协程的执行,直到delay函数返回。
将这个片段反编译成java代码,删掉导包和元数据注解信息等,分析过程见注释流程号:
public final class MySimpleTest {
public static final int $stable ;
@Nullable
// (8)stephenTest函数本来是无参的,现在有一个Continuation类型的参数
// 这个就是外部调用代码块封装成的实例,stephenTest方法执行完毕,需要继续往下执行的代码都在这个对象里面
public final Object stephenTest ( @NotNull Continuation $completion ) {
Continuation $continuation ;
// (9)label20: 是一个Java中的标签(label),主要用于控制流程跳转。在这里它被用来实现协程的挂起和恢复机制
label20: {
if ( $completion instanceof < undefinedtype >) {
$continuation = (< undefinedtype >) $completion ;
// (10)用于检查当前协程是否处于挂起状态。Integer.MIN_VALUE 是一个特殊的标志位,用于标记协程是否被挂起。
// 它的值是10000000 00000000 00000000 00000000
// label首次传进来是1,即00000000 00000000 00000000 00000001,和Integer.MIN_VALUE按位与的结果为0,表示需要挂起,会走到11步,基于外部传入的 completion 对象创建一个新的ContinuationImpl对象
//=======================分割线====================
// (16)这里的label在15步被赋值成了10000000 00000000 00000000 00000001,按位与的结果是Integer.MIN_VALUE,即条件检查结果为真(即 != 0)
// 10000000 00000000 00000000 00000001减去Integer.MIN_VALUE,结果是1,即00000000 00000000 00000000 00000001
// 并将label20标签跳出循环,继续往下执行stephenTest的switch状态判断
if (( $continuation . label & Integer . MIN_VALUE ) != 0 ) {
$continuation . label -= Integer . MIN_VALUE ;
break label20 ;
}
}
// (11)开始创建关于stephenTest代码块的ContinuationImpl对象,用于传递给下一个suspend函数
$continuation = new ContinuationImpl ( $completion ) {
// $FF: synthetic field
Object result ;
// 初始值为0
int label ;
@Nullable
// (13)delay执行完,调用resumeWith,触发这个invokeSuspend方法
public final Object invokeSuspend ( @NotNull Object $result ) {
this . result = $result ;
//(14)将label = 1和Integer.MIN_VALUE按位或,
// 运算的结果是 10000000 00000000 00000000 00000001(即 -2147483647)
this . label |= Integer . MIN_VALUE ;
//(15)重入调用stephenTest函数,这次是传入 $continuation 自己作为参数。
return MySimpleTest . this . stephenTest (( Continuation ) this );
}
};
}
Object $result = $continuation . result ;
Object var4 = IntrinsicsKt . getCOROUTINE_SUSPENDED ();
// (17) 本轮调用中,label值为1,检查无异常后,就会返回这个字符串
// "result From stephenTest"
switch ( $continuation . label ) {
case 0 :
ResultKt . throwOnFailure ( $result );
$continuation . label = 1 ;
// (12)调用delay函数,之后就和外部调用的(3)-(7)步流程一样.
// 传入ContinuationImpl对象,delay函数内部会判断是否需要挂起,如果需要挂起,就return掉本轮stephenTest方法的调用
// 进入了delay内部执行,等500ms过后,调用外部传进来的ContinuationImpl对象的 resumeWith 函数回调
// 而resumeWith方法,必然会调用到这个ContinuationImpl 对象自己的invokeSuspend方法,就跳转到第13步了
if ( DelayKt . delay ( 500L , $continuation ) == var4 ) {
return var4 ;
}
break ;
case 1 :
ResultKt . throwOnFailure ( $result );
break ;
default :
throw new IllegalStateException ( "call to 'resume' before 'invoke' with coroutine" );
}
return "result From stephenTest" ;
}
}
// CoroutineTestKt.java
public final class CoroutineTestKt {
public static final void callFromOutside () {
// (1)分析入口,从最外部的调用开始
BuildersKt . launch $default ( CoroutineScopeKt . CoroutineScope (( CoroutineContext ) Dispatchers . getIO ()), ( CoroutineContext ) null , ( CoroutineStart ) null , new Function2 (( Continuation ) null ) {
// (2)函数代码块里的任务,被封装在了继承自Continuation的一个匿名内部类对象中
// launch开始后,进入就会调用其invoke方法,并首次执行invokeSuspend方法,这时候label为0
int label ;
// (18) 17步返回后,标志着 stephenTest 方法中 $continuation实例的invokeSuspend方法调用完毕
// 将调用completion的invokeSuspend方法
// (19)这时候外部的这个label值也已经为1了,就是继续往下执行了
public final Object invokeSuspend ( Object $result ) {
Object var3 = IntrinsicsKt . getCOROUTINE_SUSPENDED ();
Object var10000 ;
// (3)通过label来判断当前是到了哪一个状态
switch ( this . label ) {
case 0 :
// (4)首先检查异常
ResultKt . throwOnFailure ( $result );
// (5)创建一个MySimpleTest对象,并调用其stephenTest方法
var10000 = new MySimpleTest ();
// (6)将这个匿名内部类自己传进去,作为参数
Continuation var10001 = ( Continuation ) this ;
// 将label状态设置为1,等下次再次调用invokeSuspend就会走switch的1的分支
this . label = 1 ;
var10000 = ( MySimpleTest ) var10000 . stephenTest ( var10001 );
// (7) 如果 stephenTest 这个方法的返回值是COROUTINE_SUSPENDED,则表示该函数已暂停,我们也返回COROUTINE_SUSPENDED给调用者
// 通知这个函数是挂起函数,暂时不往下执行了
if ( var10000 == var3 ) {
return var3 ;
}
// 转到MySimpleTest这个类分析 ->(8)
break ;
case 1 :
ResultKt . throwOnFailure ( $result );
var10000 = ( MySimpleTest ) $result ;
break ;
default :
throw new IllegalStateException ( "call to 'resume' before 'invoke' with coroutine" );
}
// (20)挂起和恢复流程执行完毕,打印结果
String result = ( String ) var10000 ;
System . out . println ( result );
return Unit . INSTANCE ;
}
public final Continuation create ( Object value , Continuation $completion ) {
return ( Continuation )( new < anonymous constructor >( $completion ));
}
public final Object invoke ( CoroutineScope p1 , Continuation p2 ) {
return ((< undefinedtype >) this . create ( p1 , p2 )). invokeSuspend ( Unit . INSTANCE );
}
// $FF: synthetic method
// $FF: bridge method
public Object invoke ( Object p1 , Object p2 ) {
return this . invoke (( CoroutineScope ) p1 , ( Continuation ) p2 );
}
}, 3 , ( Object ) null );
}
}
以上的分析流程就是协程的挂起恢复过程。
自动的线程切换 引例 在Android上使用协程,从本地读取一个字符串,或其他耗时逻辑,可能会写下这样的代码:
// MainViewModel.kt
suspend fun getLocalString () = withContext ( Dispatchers . IO ) {
// 模拟IO操作
Thread . sleep ( 2000 )
"result from local"
}
// MainActivity.kt
class MainActivity : AppCompatActivity () {
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity_main )
// 开启协程
lifecycleScope . launch {
val result = getLocalString ()
tv_result . text = result
}
}
}
getLocalString() 方法,我们设置了上下文为IO,很明显直觉上会在 IO 线程中执行。
在 MainActivity 中,我们通过 lifecycleScope.launch 开启了一个协程,协程中调用 getLocalString() 方法,最初为主线程环境,是怎么从主线程切换到IO线程来运行这个方法的呢?
ContinuationInterceptor Kotlin协程实现自动线程切换的核心在于其调度器(Dispatcher) 机制,而调度器 Dispatcher 就是 ContinuationInterceptor 的实现。
ContinuationInterceptor 接口(简化):
public interface ContinuationInterceptor : CoroutineContext . Element {
// 拦截 Continuation 的恢复
fun < T > interceptContinuation ( continuation : Continuation < T >): Continuation < T >
}
AbstractCoroutine (例如 StandaloneCoroutine) 的 resumeWith 方法:
当协程需要恢复时,通常会调用 Continuation 的 resumeWith 方法。在协程的底层实现中,例如 AbstractCoroutine (所有 Job 的子类,如 StandaloneCoroutine 继承的基类),其 resumeWith 方法会检查 CoroutineContext 中是否存在 ContinuationInterceptor。
// AbstractCoroutine.kt (简化)
override fun resumeWith ( result : Result < T >) {
val context = this . context
val dispatcher = context [ ContinuationInterceptor ] as ? ContinuationInterceptor
if ( dispatcher == null ) {
// 没有调度器,直接在当前线程执行
dispatchResume ( result )
} else {
// 有调度器,通过调度器来分发恢复操作
dispatcher . dispatch ( this , result ) // 最终会调用 dispatchResume
}
}
Dispatcher 的 dispatch 方法Dispatcher 的 dispatch 方法是实际执行线程切换的地方。不同的调度器有不同的实现。
Dispatchers.Default (例如 DefaultScheduler.kt):
Dispatchers.Default 通常使用一个共享的线程池来执行协程。
// DefaultScheduler.kt (简化)
internal object DefaultScheduler : CoroutineDispatcher (), Executor {
override fun dispatch ( context : CoroutineContext , block : Runnable ) {
// 将 block (即恢复协程的Runnable) 提交到默认的线程池
DefaultExecutor . enqueue ( block ) // DefaultExecutor 是一个线程池
}
// ... 其他方法
}
当 dispatch 被调用时,它会将协程的恢复逻辑封装在一个 Runnable 中,然后提交给调度器底层的 线程池 。这个 Runnable 最终会在线程池中的某个线程上执行,从而实现了线程的切换。
Dispatchers.IO 通常会使用一个独立的、容量更大的线程池,用于处理 IO 密集型任务。其 dispatch 逻辑与 Default 类似,只是提交给不同的线程池。Dispatchers.Main 在 Android 上通常会与主线程的 Looper 绑定。// AndroidMainDispatcherFactory.kt (简化)
internal class AndroidMainDispatcherFactory : MainDispatcherFactory {
override fun createDispatcher ( allFactories : List < MainDispatcherFactory >): MainCoroutineDispatcher {
// ...
return HandlerContext ( Looper . getMainLooper (), "Main" ) // 包装了 Looper
}
}
// HandlerDispatcher.kt (简化)
class HandlerContext ( .. .) : MainCoroutineDispatcher () {
override fun dispatch ( context : CoroutineContext , block : Runnable ) {
// 将 block 提交到 Looper 关联的 Handler
handler . post ( block )
}
// ...
}
上面的代码可以看出, Dispatchers.Main 会将协程的恢复操作通过 Android Handler 的 post 方法发送到主线程的消息队列,从而确保协程在主线程上恢复执行。
suspendCoroutine / suspendCancellableCoroutine除了编译器自动生成的挂起点,我们也可以手动创建挂起点,这通常通过 suspendCoroutine 或 suspendCancellableCoroutine 函数实现。
suspend fun manualSuspendExample (): String = suspendCancellableCoroutine { continuation ->
// 可以在这里执行一些异步操作
Thread {
Thread . sleep ( 1000 )
continuation . resume ( "Resumed from another thread" ) // 在另一个线程调用 resume
}. start ()
}
这里 continuation.resume(...) 的调用是关键。当这个 resume 被调用时,它会触发之前提到的 ContinuationInterceptor 机制,如果 CoroutineContext 中有调度器,就会通过调度器进行线程切换。
总结挂起和线程切换流程 协程挂起: 当协程遇到 suspend 函数(如 delay,或自定义的挂起函数),如果该函数需要等待某个异步操作完成,它会返回 COROUTINE_SUSPENDED,并将当前的执行上下文(Continuation)保存起来。异步操作完成: 当异步操作完成时(例如网络请求返回数据,或 delay 时间到),会调用之前保存的 Continuation 对象的 resumeWith 方法。调度器介入: resumeWith 方法会检查协程的 CoroutineContext 中是否存在 ContinuationInterceptor (即 Dispatcher)。分发恢复: 如果存在调度器,resumeWith 会调用调度器的 dispatch 方法。线程切换: 调度器的 dispatch 方法会将协程的恢复逻辑(一个 Runnable)提交到其管理的线程(如线程池中的线程,或 Android 主线程)。协程恢复: Runnable 在目标线程上执行,调用实际的恢复逻辑,协程从挂起的地方继续执行。通过这种 CPS转换 + 回调resume + 调度器拦截 的机制,Kotlin 协程得以在不阻塞线程的情况下,根据需要自动在不同的线程之间切换执行,从而实现高效的并发编程。
在 Kotlin 中,集合类(Collections)是非常重要的一部分,它提供了丰富且功能强大的 API 来操作数据集合。Kotlin 的集合类在很大程度上与 Java 的集合框架兼容,但 Kotlin 在其基础上进行了扩展和增强,提供了更简洁、更安全、更富表达力的 API。
一个重要的概念是,Kotlin 明确区分了只读 (read-only) 集合和可变 (mutable) 集合。
只读集合接口: 它们只提供读取数据的方法,不能添加、删除或修改元素。例如:List<T>、Set<T>、Map<K, V>。 可变集合接口: 它们在只读接口的基础上,提供了修改集合的方法。例如:MutableList<T>、MutableSet<T>、MutableMap<K, V>。 Kotlin 的集合类主要分为三大类:列表 (List)、集合 (Set) 和 映射 (Map)。这些集合类都继承自 kotlin.collections 包中的接口。
List<T>有序集合,保持元素的插入顺序,其内部的元素可以重复。
只读列表 :通常通过 listOf() 或 mutableListOf().toList() 创建。可变列表 :通常通过 mutableListOf() 创建。除了 List 的功能外,还支持添加、删除、更新元素。除了上面两种创建方式,还可以使用 arrayListOf() 返回一个 ArrayList ,其实就是Java 的 ArrayList。
Set<T>Set是一种无序的集合,不包含重复的元素。
只读集合 通常通过 setOf() 或 mutableSetOf().toSet() 创建。不保证元素的顺序,不允许有重复元素。可变集合 通常通过 mutableSetOf() 创建。支持添加、删除元素。其他的创建方式还有 hashSetOf(),返回一个 HashSet (Java 的 HashSet)。
linkedSetOf(): 返回一个 LinkedHashSet (Java 的 LinkedHashSet)。
Map<K, V>映射 (也称为字典或关联数组) 存储键值对,其中每个键都是唯一的,并且映射到一个值。
Map<K, V> (只读): 通常通过 mapOf() 或 mutableMapOf().toMap() 创建。用来存储键值对,键是唯一的,不保证元素的顺序 (除非使用特定实现如 LinkedHashMap)。MutableMap<K, V> (可变): 通常通过 mutableMapOf() 创建。支持添加、删除、更新键值对。还可以使用 hashMapOf() 创建一个 HashMap (Java 的 HashMap)。
linkedMapOf(): 返回一个 LinkedHashMap (Java 的 LinkedHashMap)。
Kotlin 集合与 Java 集合对比 Kotlin 的集合在很大程度上是基于 Java 集合框架的,但进行了优化和扩展,提供了更安全、更简洁的 API。
1. 只读与可变分离 (最主要区别) Kotlin: 明确区分了只读接口 (List, Set, Map) 和可变接口 (MutableList, MutableSet, MutableMap)。
这在编译时强制执行了不变性,有助于避免运行时错误和并发问题。当你只需要读取集合时,声明为只读类型可以更好地表达意图,并防止意外修改。
val readOnlyList : List < String > = listOf ( "A" , "B" , "C" )
// readOnlyList.add("D") // 编译错误!
val mutableList : MutableList < String > = mutableListOf ( "X" , "Y" , "Z" )
mutableList . add ( "W" ) // 可以修改
而使用 Java的集合接口 (如 List, Set, Map) 本身就包含了修改方法。虽然可以通过 Collections.unmodifiableList() 等方法创建不可修改的视图,但这只是一个运行时检查,如果你仍然持有原始的可变集合引用,它仍然可以被修改。
List < String > javaList = new ArrayList <>( Arrays . asList ( "A" , "B" , "C" ));
// javaList.add("D"); // 可以直接修改
List < String > unmodifiableJavaList = Collections . unmodifiableList ( javaList );
// unmodifiableJavaList.add("E"); // 运行时抛出 UnsupportedOperationException
// 但是,如果修改 javaList,unmodifiableJavaList 也会随之改变
javaList . add ( "F" ); // unmodifiableJavaList 现在也包含 "F"
在Java中也可以直接创建不可变集合,使用 Java 9 以后引入的 List.of(), Set.of(), Map.of() 方法。
List < Integer > numbers = Arrays . asList ( 1 , 2 , 3 ); // 返回一个固定大小的List
// 或者
List < Integer > numbersJava9 = List . of ( 1 , 2 , 3 ); // 不可变List
Map < String , Integer > users = new HashMap <>();
users . put ( "Alice" , 30 );
2. 可空性支持 Kotlin 的类型系统原生支持可空性。这意味着你可以明确指定集合是否可以包含 null 元素,以及集合本身是否可以为 null。
val nullableStrings : List < String ?> = listOf ( "A" , null , "B" ) // 列表中可以有 null
val nonNullableList : List < String > = listOf ( "C" , "D" ) // 列表中不能有 null
// 如果一个List本身可能为null
var maybeList : List < Int >? = null
maybeList = listOf ( 1 , 2 )
而 Java 在语言层面没有原生支持可空性,null 是一种常见的运行时错误源 (NullPointerException)。通常通过 @Nullable 和 @NonNull 注解来提示,但这些只是编译器或工具的提示,不能像 Kotlin 那样在编译时强制执行。
3. 集合扩展函数 Kotlin提供了大量的 扩展函数 (Extension Functions) 来操作集合,这使得集合操作变得非常简洁和富有表现力。例如:filter, map, forEach, firstOrNull, count, groupBy 等。这些函数通常链式调用,形成了非常强大的函数式编程风格。
val nums = listOf ( 1 , 2 , 3 , 4 , 5 )
val evenSquared = nums . filter { it % 2 == 0 }. map { it * it } // 过滤偶数并平方
println ( evenSquared ) // 输出: [4, 16]
以下是一些常用的扩展函数:
map:对集合中的每个元素进行转换,返回新的集合。filter:过滤出符合条件的元素,返回新的集合。flatMap:先对每个元素进行转换,然后将结果扁平化为一个新的集合。reduce 和 fold:对集合中的元素进行累积操作。forEach:遍历集合中的每个元素。any 和 all:判断集合中是否存在或所有元素满足某个条件。find 和 first:查找符合条件的元素。示例:
val numbers = listOf ( 1 , 2 , 3 , 4 , 5 )
val doubled = numbers . map { it * 2 } // [2, 4, 6, 8, 10]
val even = numbers . filter { it % 2 == 0 } // [2, 4]
val sum = numbers . reduce { acc , i -> acc + i } // 15
这些扩展函数让 Kotlin 的集合操作更加直观和简洁,提高了开发效率。
4. 与 Java 集合的互操作性 Kotlin 集合与 Java 集合是完全兼容的,并且可以无缝互操作。
在 Kotlin 代码中,你可以直接使用 Java 的 ArrayList, HashSet, HashMap 等。当你在 Kotlin 中使用这些 Java 集合时,它们会自动被视为可变集合。 当 Kotlin 的只读集合传递给 Java 方法时,它们会被转换为相应的 Java 接口,但仍然是“只读视图”。修改这些视图会导致运行时异常,而修改原始 Kotlin 可变集合则会反映在 Java 视图中。 当你从 Java 方法接收集合时,Kotlin 会将其视为可变集合,但在 Kotlin 中你可以轻松地将其转换为只读视图(例如 someJavaList.toList())。 Kotlin 的 Sequence(序列) Sequence 是 Kotlin 提供的一种惰性集合操作机制,类似于 Java 的 Stream API。它的主要特点是:
惰性计算 :Sequence 中的操作不会立即执行,而是按需计算,只有在终端操作(如 toList()、forEach())被调用时才会执行。适合大数据集 :由于是惰性计算,Sequence 在处理大量数据时更高效,因为它避免了创建中间集合。链式操作 :支持链式调用多个操作,代码更简洁。示例:
val numbers = sequenceOf ( 1 , 2 , 3 , 4 , 5 )
val result = numbers
. map { it * 2 } // 不会立即执行
. filter { it % 3 == 0 } // 不会立即执行
. toList () // 触发实际计算,返回 [6]
println ( result ) // 输出 [6]
与 Java 的 Stream 相比,Kotlin 的 Sequence 在语法上更简洁,且与 Kotlin 的集合体系无缝集成。
协变(Covariance)、逆变(Contravariance)和 reified 关键字是 Kotlin 泛型系统中比较高级和强大的特性。它们能帮助你编写更健壮、更灵活、更类型安全的泛型代码,尤其是在处理集合、高阶函数以及需要运行时类型检查的场景。
类型擦除 在深入协变和逆变之前,先简单回顾一下 Java/Kotlin 泛型的类型擦除(Type Erasure) 。
在 JVM 上,泛型信息只在编译时存在,运行时会被擦除。这意味着 List<String> 和 List<Int> 在运行时都会变成 List<Object>(或 List<Any?>)。
这就导致了两个主要限制:
你不能在运行时直接获取泛型参数的具体类型(比如 T::class.java)。 你不能直接创建泛型数组(比如 Array<T>())。 Kotlin 中通过 reified 关键字 解决了第一个限制,在内联函数中使用,可以在编译期就确定泛型参数的实际类型。
而 协变和逆变 则解决了在使用泛型时如何安全地处理子类型关系的问题。
Java中的协变和逆变 首先回顾下Java中是怎么做的, Java 泛型中的 super 和 extends 通配符,与 Kotlin 的协变 (out) 和逆变 (in) 概念密切相关。
Java 泛型通配符:extends 和 super 在 Java 中,泛型默认是 不变的 (invariant) ,这意味着 List<String> 并不是 List<Object> 的子类型,也就是说,子类的泛型(List<String>)不属于泛型(List<Object>)的子类,反之亦然。
为了在需要时放宽这种限制,Java 引入了泛型通配符 :? extends T 和 ? super T。
它们允许你在泛型类型参数上定义上限或下限,从而实现 协变(Covariance) 和 逆变(Contravariance) 的效果。
1. ? extends T (上界通配符) 简介:
含义 : ? extends T 表示“类型是 T 或 T 的某个子类型 ”。用途 : 主要用于从泛型结构中读取数据 。你可以从一个 List<? extends T> 中获取 T 类型的对象,但不能安全地向其中添加任何 T 类型的对象(除了 null)。角色 : 充当生产者 (Producer) 。如果你要从集合中取 东西,那么这个集合应该使用 extends。与 Kotlin 的 out 对应 : ? extends T 在 Java 中实现了协变 。如果 Sub 是 Super 的子类型,那么 Generic<Sub> 也是 Generic<Super> 的子类型,就称为协变。即 子类型关系在泛型中得以保留。
List < Button > buttons = new ArrayList < Button >();
List <? extends TextView > textViews = buttons ; // 合法
TextView textView = textViews . get ( 0 ); // 合法
// 下面的描述都是成立的
List <? extends TextView > textViews = new ArrayList < TextView >(); // 👈 本身
List <? extends TextView > textViews = new ArrayList < Button >(); // 👈 直接子类
List <? extends TextView > textViews = new ArrayList < RadioButton >(); // 👈 间接子类
前面说到 List<? extends TextView> 的泛型类型是个未知类型 ?,编译器也不确定它是啥类型,只是有个限制条件。
由于它满足 ? extends TextView 的限制条件 ,所以 get 出来的对象,肯定是 TextView 的子类型,根据多态的特性,能够赋值给 TextView ,啰嗦一句,赋值给 View 也是没问题的。
List <? extends TextView > textViews = new ArrayList < Button >();
TextView textView = textViews . get ( 0 ); // 合法
View view = textViews . get ( 0 ); // 合法
// 下面的添加元素的代码是不合法的
textViews . add ( new Button ()); // 不合法
textViews . add ( new TextView ()); // 不合法
到了 add 操作的时候,我们可以这么理解:
List<? extends TextView> 由于类型未知,它可能是 List<Button>,也可能是 List<TextView> 。对于前者,显然我们要添加 TextView 是不可以的。 实际情况是编译器无法确定到底属于哪一种,无法继续执行下去,就报错了。 2. ? super T (下界通配符) 简介:
含义 : ? super T 表示“类型是 T 或 T 的某个父类型 ”。用途 : 主要用于向泛型结构中写入数据 。你可以向一个 List<? super T> 中添加 T 类型的对象或其任何子类型,但从其中获取元素时,你只能确定它们是 Object 类型。角色 : 充当消费者 (Consumer) 。如果你要向集合中放 东西,那么这个集合应该使用 super。与 Kotlin 的 in 对应 : ? super T 在 Java 中实现了逆变 。如果 Sub 是 Super 的子类型,那么 Generic<Super> 是 Generic<Sub> 的子类型,就称为逆变。即 子类型关系在泛型中被反转。
先看一下它的写法:
List <? super Button > buttons = new ArrayList < TextView >();
这个 ? super 叫做「下界通配符」,可以使 Java 泛型具有「逆变性 Contravariance」。
与上界通配符对应,这里 super 限制了通配符 ? 的子类型,所以称之为下界。
它也有两层意思:
通配符 ? 表示 List 的泛型类型是一个未知类型。 super 限制了这个未知类型的下界,也就是泛型类型必须满足这个 super 的限制条件。super 我们在类的方法里面经常用到,这里的范围不仅包括 Button 的直接和间接父类,也包括下界 Button 本身。super 同样支持 interface 。上面的例子中, TextView 是 Button 的父类型 ,也就能够满足 super 的限制条件,就可以成功赋值了。
其他示例:
List <? super Button > buttons = new ArrayList < Button >(); // 👈 本身
List <? super Button > buttons = new ArrayList < TextView >(); // 👈 直接父类
List <? super Button > buttons = new ArrayList < Object >(); // 👈 间接父类
在涉及到拿取和添加元素的情景时,编译器可以确定你 添加进去的元素是 Button 的父类 ,Button 对象一定是这个未知类型的子类型,根据多态的特性,这里通过 add 添加 Button 对象是合法的。
但你不能通过 get 方法拿到这个元素,因为编译器只知道它是个未知类型,是 Button 的父类,但是你拿什么类型的对象来接收呢(除非Object)。
使用下界通配符 ? super 的泛型 List,只能读取到 Object 对象,一般没有什么实际的使用场景,通常也只拿它来添加数据,也就是消费已有的 List<? super Button>,往里面添加 Button,因此这种泛型类型声明称之为「消费者 Consumer」。
Kotlin中的 协变:out 关键字 以上为java中实现逆变和协变的方法,在Kotlin中的写法如何呢?在 Kotlin 中,当泛型类型参数被标记为 out 时,它表示该类型参数只能被生产 (作为返回值输出),而不能被消费 (作为参数输入)。(这个很形象,一个out,一个in)
如果一个类 Producer<T> 的类型参数 T 被声明为 out:Producer<T> 的成员函数只能返回 T 类型的值 。Producer<T> 的成员函数不能接受 T 类型的值作为参数 (因为你无法保证传入的 T 是特定子类型)。 这意味着,如果 A 是 B 的子类型,那么 Producer<A> 就是 Producer<B> 的子类型。 // 声明一个协变接口:只能生产 T 类型
interface Producer < out T > {
fun produce (): T // T 只能作为返回类型(生产)
// fun consume(item: T) // 编译错误!T 不能作为参数类型(消费)
}
open class Animal
class Cat : Animal ()
class Dog : Animal ()
// 实现生产 Animal 的生产者
class AnimalProducer : Producer < Animal > {
override fun produce (): Animal = Cat () // 可以生产 Cat (是 Animal 的子类)
}
// 实现生产 Cat 的生产者
class CatProducer : Producer < Cat > {
override fun produce (): Cat = Cat ()
}
fun main () {
val animalProducer : Producer < Animal > = CatProducer () // 协变:CatProducer 可以被赋值给 Producer<Animal>
val animal : Animal = animalProducer . produce () // produce() 返回 Animal
println ( "Produced: $animal" )
// AnimalProducer producerCat = CatProducer() // 这样也是可以的
}
何时使用 out? 当你的泛型类型只作为输出 (例如,函数返回值、只读属性)时,使用 out。这通常用于表示“提供者”或“源头”。Kotlin 的 List<out E> 就是一个很好的例子:你只能从 List 中获取元素,不能添加特定类型的元素(尽管 MutableList<E> 不会使用 out,因为它可以添加)。
Kotlin中的逆变:in 关键字 在 Kotlin 中,当泛型类型参数被标记为 in 时,它表示该类型参数只能被消费 (作为参数输入),而不能被生产 (作为返回值输出)。
如果一个类 Consumer<T> 的类型参数 T 被声明为 in:Consumer<T> 的成员函数只能接受 T 类型的值作为参数 。Consumer<T> 的成员函数不能返回 T 类型的值 (因为你无法保证返回的 T 是特定父类型)。 这意味着,如果 A 是 B 的子类型,那么 Consumer<B> 就是 Consumer<A> 的子类型。 // 声明一个逆变接口:只能消费 T 类型
interface Consumer < in T > {
fun consume ( item : T ) // T 只能作为参数类型(消费)
// fun produce(): T // 编译错误!T 不能作为返回类型(生产)
}
open class Animal
class Cat : Animal ()
class Dog : Animal ()
// 实现消费 Animal 的消费者
class AnimalConsumer : Consumer < Animal > {
override fun consume ( item : Animal ) {
println ( "Consuming an animal: $item" )
}
}
// 实现消费 Cat 的消费者
class CatConsumer : Consumer < Cat > {
override fun consume ( item : Cat ) {
println ( "Consuming a cat: $item" )
}
}
fun main () {
val catConsumer : Consumer < Cat > = AnimalConsumer () // 逆变:AnimalConsumer 可以被赋值给 Consumer<Cat>
catConsumer . consume ( Cat ()) // 可以消费 Cat
// catConsumer.consume(Animal()) // 编译错误!因为 catConsumer 期望的是 Cat 或其子类型
}
何时使用 in? 当你的泛型类型只作为输入 (例如,函数参数、只写属性)时,使用 in。这通常用于表示“消费者”或“汇集点”。Kotlin 的 Comparator<in T> 就是一个很好的例子:它可以通过比较任何 T 或其超类型来比较 T。
reified 关键字最后介绍一下Kotlin中的reifeid关键字,reified 关键字用于 内联函数 (inline functions) 的泛型类型参数。它解决了 Java/Kotlin 泛型类型擦除的问题,允许你 在运行时访问泛型类型信息 。
由于类型擦除,你不能像下面的示例一样写,直接在运行时检查一个泛型类型:
// 这是不允许的,因为 T 在运行时是 Any/Object
fun < T > checkIfString ( value : Any ) {
// if (value is T) { // 编译错误!Cannot check for instance of erased type: T
// println("It's a T")
// }
}
// 也不允许获取 T 的 Class 对象
// fun <T> createInstance(): T {
// return T::class.java.newInstance() // 编译错误!Cannot use T as reified type parameter
// }
reified 的作用当一个泛型类型参数被标记为 reified 时,Kotlin 编译器会在编译时将该类型参数的具体类型信息内联到调用点 。这意味着在运行时,该泛型类型不再被擦除,你可以像访问普通类型一样访问它。
reified 只能用于 inline 函数 的类型参数。因为内联函数会将其代码复制到调用点,所以编译器有机会“知道”实际的类型参数。有了 reified,你就可以在函数体内使用 is 运算符、as 运算符以及 T::class.java。 // 使用 reified 关键字检查类型
inline fun < reified T > T . checkClassType () {
// 类型 T 内联解析
when ( this ) {
is Int -> {
// 检查 this 是否为 Int 类型
println ( "this is a Int: $this" )
}
is String -> {
// 检查 this 是否为 String 类型
println ( "this is a String: $this" )
}
else -> {
// 检查 this 是否为其他类型
println ( "this is a other type: $this" )
}
}
}
/**
this is a Int: 2
this is a String: Kotlin
this is a other type: 2.0
*/
// 使用reified创建类实例
class Fish {
fun swim () {
println ( "Fish is swimming" )
}
}
inline fun < reified T > createInstance (){
try {
// 1. 获取 ClassLoader
val classLoader = Thread . currentThread (). contextClassLoader
// 2. 加载类
val className = T :: class . java . name
val loadedClass = classLoader ?. loadClass ( className )
// 3. 创建实例
val instance = loadedClass ?. getDeclaredConstructor () ?. newInstance ()
instance ?. let {
// 4. 调用方法
val method = loadedClass . getDeclaredMethod ( "swim" )
method . invoke ( instance )
}
} catch ( e : Exception ) {
e . printStackTrace ()
}
}
/**
Fish is swimming
*/
reified 在Android中的应用场景JSON 解析库 : 许多 JSON 解析库(如 Gson, Moshi, kotlinx.serialization)的扩展函数使用 reified 来简化类型指定,无需传递 Class<T> 参数。// 假设你有这样一个扩展函数
inline fun < reified T > String . fromJson (): T {
// 内部使用 T::class.java 进行类型反序列化
// ...
throw NotImplementedError ()
}
// val user = jsonString.fromJson<User>() // 比 jsonString.fromJson(User::class.java) 更简洁
启动 Activity : 简化 Activity 的启动,无需在 Intent 中指定 Class。inline fun < reified T : Activity > Context . startActivity () {
startActivity ( Intent ( this , T :: class . java ))
}
// 使用:context.startActivity<DetailActivity>()
获取 Service : 简化获取系统服务。安卓热门网络请求库Retrofit也是使用了这个方法来示例化定义好的api服务的。inline fun < reified T > Context . getSystemService (): T ? {
return getSystemService ( T :: class . java ) as ? T
}
// val locationManager = context.getSystemService<LocationManager>()
查找视图 : 在一些旧的视图查找框架中,可以使用 reified 简化类型转换。注意事项 :
reified 只能用于 inline 函数。由于内联的特性,过度使用 reified 可能会导致生成的字节码文件变大。应合理使用。 本文基于公司内部我写的一篇关于Kotlin的推广文,呼吁在日常开发中更多地使用Kotlin,而不是Java。
Kotlin在Android平台上,最吸引人的一点,就是它在简洁优雅的同时,完全兼容Java,可以与Java的方法,类等无缝地进行互调用。第一章节,先介绍一下对比Java的写法优化。
第二节是Kotlin的一些高级特性,像协程,密封类,内联,noinline等。
对比Java有哪些写法优化 lambda Java 中也有lambda,在Kotlin中的lambda表达式,是一种更简洁的函数表示方式,它可以代替匿名内部类的使用。lambda表达式的语法如下:
// 无参lambda表达式
val printName = { println ( "Kotlin" ) }
// 带参lambda表达式
val sum = { a : Int , b : Int -> a + b }
在这个例子中,printName 是一个无参的lambda表达式,它的函数体只有一行代码。sum 是一个带参的lambda表达式,它接收两个 Int 类型的参数,并返回它们的和。
与匿名内部类相比,lambda表达式的代码更简洁,可读性更好。
其他的用法,例如声明Runnable和线程的时候,可以直接使用lambda写成下面这样:
val runnable = Runnable {
println ( "Kotlin" )
}
val thread = Thread ( runnable )
thread . start ()
fun startThread () {
Thread {
println ( "Thread name is ${Thread.currentThread().name}" )
}. start ()
}
实现原理 Kotlin和Java的Lambda语法实现均是基于函数式接口(内部只有一个方法的接口)。
函数式接口 是指只包含一个抽象方法(Single Abstract Method,简称 SAM),但是可以有多个默认方法或静态方法的接口。这样的接口可以被 Lambda 表达式 或 方法引用 所实现(或替代)。
Java中没有原生的接口类,而Kotlin则原生定义了很多接口类,归类叫做FunctionN,其中N代表参数的数量,最多支持带22个参数。
public interface Function0 < out R > : Function < R > {
/** Invokes the function. */
public operator fun invoke (): R
}
/** A function that takes 1 argument. */
public interface Function1 < in P1 , out R > : Function < R > {
/** Invokes the function with the specified argument. */
public operator fun invoke ( p1 : P1 ): R
}
.. .
同时Kotlin还支持带接收者的lambda,可以说是和函数式编程和扩展函数的结合,可以在lambda的域中访问该对象的变量和方法。
在定义使用Lambda时,会默认将lambda参数继承实现 FunctionN 接口,传递到方法中,在方法中调用 invoke方法。
// HighOrderFunction.kt 文件的顶层函数
fun printSomething ( print : () -> Unit ) {
print ()
}
反编译之后:
public final class HighOrderFunctionKt {
public static final void printSomething ( @NotNull Function0 print ) {
Intrinsics . checkNotNullParameter ( print , "print" );
print . invoke ();
}
}
循环中使用lambda的坑 上面的分析可以得知,每一个lambda的调用,不像一般的方法使用指针来调用,而是都会创建出一个匿名内部类,如果在循环中使用的话,会导致性能问题。
这时候一般会在循环中使用inline关键字修饰的内联函数,或者使用crossinline关键字修饰的内联函数,这样在循环中使用lambda时,就不会创建出多个匿名内部类了。
class LambdaTest {
inline fun testInline ( lambdaParams :()-> Unit ) {
lambdaParams ()
}
}
fun main () {
val lambdaTest = LambdaTest ()
for ( i in 0 .. 100000 ) {
lambdaTest . testInline {
println ( "hello world" )
}
}
}
反编译之后:
public final class LambdaTest {
public final void testInline ( @NotNull Function0 lambdaParams ) {
Intrinsics . checkNotNullParameter ( lambdaParams , "lambdaParams" );
lambdaParams . invoke ();
}
}
public final class MainKt {
public static final void main () {
LambdaTest lambdaTest = new LambdaTest ();
int $i$iv = 0 ;
int var3 ;
for ( var3 = 100000 ; $i$iv <= var3 ; ++ $i$iv ) {
System . out . println ( "hello world" );
}
}
}
默认函数参数 Kotlin中函数的参数可以有默认值,这样在调用函数时如果没有为该参数传入值,就会使用默认值。
fun printName ( name : String = "Unknown" ) {
println ( "My name is $name" )
}
printName () // 输出: My name is Unknown
printName ( "Kotlin" ) // 输出: My name is Kotlin
自动类型推断 Kotlin 编译器会根据上下文推断变量的类型,这意味着你通常不需要显式地声明变量的类型。
val name = "Kotlin" // 编译器推断 name 为 String 类型
除了变量,函数也可以自动推断参数类型和返回值类型。
fun sum ( a : Int , b : Int ): Int {
return a + b
}
在这个例子中,sum 函数的参数 a 和 b 类型都是 Int,返回值类型也是 Int。Kotlin 编译器可以根据函数体推断出这一点,所以你可以省略函数声明中的类型。
fun sum ( a : Int , b : Int ) = a + b
if else直接返回结果 Java中,对一个变量进行分支判断赋值,往往写成下面这样:
String name ;
if ( isMale ) {
name = "Mike" ;
} else {
name = "Marry" ;
}
而Kotlin中,使用if else表达式,在一行代码里完成对变量的赋值:
val name = if ( isMale ) "Mike" else "Marry"
实际上反编译成Java之后,可以看出这段代码仍然使用的是上面Java的那种写法,或者一个三元判断运算符来实现,不过在面向程序员时的写法更优雅了。
when 关键字 Kotlin 中的 when 关键字 ,它是一个非常强大和灵活的控制流结构,是 Java 中 switch 语句的增强版。 when 在处理多种条件分支时,比传统的 if-else if-else 链更加简洁和表达性强。
when 可以作为一个表达式 (有返回值)或一个语句 (没有返回值)使用,这使得它比 Java 的 switch 更具通用性。它的基本作用是根据某个值或条件,执行对应的代码块。
1. when 作为表达式 当 when 作为表达式使用时,它会评估每个分支的条件,然后返回第一个满足条件的分支的结果。所有可能的分支都必须被覆盖(或者有一个 else 分支),以确保 when 总是能返回一个值。
fun getColorName ( colorCode : Int ): String {
return when ( colorCode ) {
0xFF0000 -> "Red"
0x00FF00 -> "Green"
0x0000FF -> "Blue"
else -> "Unknown Color" // else 分支是必需的,因为 when 是表达式
}
}
val color1 = getColorName ( 0xFF0000 ) // color1 = "Red"
val color2 = getColorName ( 0x00FFFF ) // color2 = "Unknown Color"
println ( color1 )
println ( color2 )
2. when 作为语句 当 when 作为语句使用时,它会执行第一个满足条件的分支的代码,但不会返回任何值。在这种情况下,else 分支是可选的,除非编译器无法确定所有可能的情况都已覆盖(例如处理 sealed 类时)。
fun printColorInfo ( colorCode : Int ) {
when ( colorCode ) {
0xFF0000 -> println ( "This is the color Red." )
0x00FF00 -> println ( "This is the color Green." )
0x0000FF -> println ( "This is the color Blue." )
// else 分支在这里是可选的
}
}
printColorInfo ( 0x00FF00 ) // 输出: This is the color Green.
匹配多个值 (逗号分隔) 如果多个分支需要执行相同的操作,可以将它们用逗号 , 分隔开。
val character = 'a'
when ( character ) {
'a' , 'e' , 'i' , 'o' , 'u' -> println ( "It's a vowel." )
in 'b' .. 'z' -> println ( "It's a consonant." ) // 后面会介绍范围匹配
else -> println ( "Not a letter." )
}
范围 (Ranges) 匹配 (in 或 !in) 可以使用 in 运算符检查值是否在一个范围内,或使用 !in 检查是否不在一个范围内。
val age = 25
val category = when ( age ) {
in 0 .. 12 -> "Child"
in 13 .. 19 -> "Teenager"
in 20 .. 64 -> "Adult"
else -> "Senior"
}
println ( "Age $age is a $category." ) // 输出: Age 25 is a Adult.
类型检查 (is 或 !is) 可以使用 is 运算符检查一个值是否是某种类型,或使用 !is 检查是否不是某种类型。这在处理多态性或检查未知对象类型时非常有用。
fun describe ( obj : Any ) {
when ( obj ) {
1 -> println ( "One" )
"Hello" -> println ( "Greeting" )
is Long -> println ( "Long type value: $obj" ) // obj 会被智能转换为 Long
! is String -> println ( "Not a String" )
else -> println ( "Unknown type or value" )
}
}
describe ( 1 ) // 输出: One
describe ( "Hello" ) // 输出: Greeting
describe ( 1000L ) // 输出: Long type value: 1000
describe ( 2.5 ) // 输出: Not a String
describe ( "Kotlin" ) // 输出: Unknown type or value
when 无参数when 也可以在没有参数的情况下使用。在这种情况下,它会评估每个分支的布尔表达式,然后执行第一个为 true 的分支。这类似于一个更可读的 if-else if-else 链。
val temperature = 28
val isRaining = true
when {
temperature > 30 -> println ( "It's very hot!" )
temperature > 20 && ! isRaining -> println ( "It's warm and sunny." )
isRaining -> println ( "It's raining." )
else -> println ( "Normal weather." )
}
// 输出: It's warm and sunny.
处理密封类 (Sealed Classes) 或枚举 (Enums) when 在处理密封类 (Sealed Classes) 和枚举 (Enums) 时特别有用。如果 when 表达式覆盖了密封类或枚举的所有可能子类/值,那么不需要 else 分支 ,因为编译器可以验证所有情况都已处理。
// 定义一个密封类
sealed class Result {
data class Success ( val data : String ) : Result ()
data class Error ( val message : String ) : Result ()
object Loading : Result () // 单例对象
}
fun handleResult ( result : Result ) {
when ( result ) {
is Result . Success -> println ( "Success: ${result.data}" )
is Result . Error -> println ( "Error: ${result.message}" )
Result . Loading -> println ( "Loading data..." ) // 注意这里直接引用单例对象
}
}
handleResult ( Result . Success ( "Data fetched!" )) // 输出: Success: Data fetched!
handleResult ( Result . Error ( "Network failed." )) // 输出: Error: Network failed.
handleResult ( Result . Loading ) // 输出: Loading data...
这种用法在 Android 中处理网络请求状态、UI 事件或不同的视图状态时非常常见和强大,因为它提供了编译时安全,确保你不会遗漏任何一种情况。
范围限制coerceIn coerceIn 是一个扩展函数,作用是将接收者对象的值“强制”限定在一个指定的范围内。如果原始值在这个范围内,就返回原始值;如果原始值小于范围的最小值,就返回最小值;如果原始值大于范围的最大值,就返回最大值。
这个函数通常用于任何实现了 Comparable 接口的类型,比如数字(Int, Double, Float, Long 等)、字符串,甚至自定义的可比较对象。
fun main () {
// 1. 限定整数范围
val num1 = 5 . coerceIn ( 1 , 10 ) // 5 在 [1, 10] 之间,返回 5
val num2 = 0 . coerceIn ( 1 , 10 ) // 0 小于 1,返回 1
val num3 = 12 . coerceIn ( 1 , 10 ) // 12 大于 10,返回 10
println ( "Int coercing: $num1, $num2, $num3" ) // 输出: Int coercing: 5, 1, 10
// 2. 限定浮点数范围
val float1 = 3.5f . coerceIn ( 1.0f , 5.0f ) // 3.5f 在 [1.0f, 5.0f] 之间,返回 3.5f
val float2 = 0.5f . coerceIn ( 1.0f , 5.0f ) // 0.5f 小于 1.0f,返回 1.0f
println ( "Float coercing: $float1, $float2" ) // 输出: Float coercing: 3.5, 1.0
// 3. 限定字符串范围 (按字典顺序)
val str1 = "banana" . coerceIn ( "apple" , "orange" ) // banana 在 apple 和 orange 之间,返回 banana
val str2 = "cat" . coerceIn ( "apple" , "banana" ) // cat 大于 banana,返回 banana
val str3 = "zoo" . coerceIn ( "apple" , "orange" ) // zoo 大于 orange,返回 orange
println ( "String coercing: $str1, $str2, $str3" ) // 输出: String coercing: banana, banana, orange
// 4. 处理负数或范围倒置(注意:如果 min > max,会抛出 IllegalArgumentException)
// val invalidRange = 5.coerceIn(10, 1) // 这会抛出 IllegalArgumentException
}
Android 应用 coerceIn 在 Android 开发中,coerceIn 在很多场景下都能派上用场:
UI 元素的滑动或拖拽限制 : 当用户拖拽一个视图时,你可能需要限制其位置在屏幕的某个特定区域内。val newX = event . rawX . coerceIn ( 0f , screenWidth - viewWidth )
view . x = newX
进度条或评分 : 确保进度值或评分值始终在有效的 0 到 100(或 1 到 5)范围内。val progress = ( rawProgressValue * 100 ). toInt (). coerceIn ( 0 , 100 )
progressBar . progress = progress
动画插值 : 限制动画的起始或结束值,防止超出预期。游戏开发 : 限制玩家角色的移动范围,或限制敌人 AI 的行为范围。数据验证 : 在处理用户输入或从外部来源获取数据时,确保数值符合预期的业务规则。val quantity = inputString . toIntOrNull () ?. coerceIn ( 1 , 99 ) ?: 1 // 如果解析失败或超出范围,默认为1
数值计算 : 避免计算结果超出合理的物理或逻辑限制。coerceAtLeast 和 coerceAtMostKotlin 还提供了两个更细粒度的“强制”函数:
value.coerceAtLeast(minimumValue) : 返回 value 和 minimumValue 中较大的那个 。它只设定下限。val score = 80 . coerceAtLeast ( 90 ) // 返回 90 (因为它不能低于 90)
val score2 = 95 . coerceAtLeast ( 90 ) // 返回 95
value.coerceAtMost(maximumValue) : 返回 value 和 maximumValue 中较小的那个 。它只设定上限。val speed = 120 . coerceAtMost ( 100 ) // 返回 100 (因为它不能高于 100)
val speed2 = 90 . coerceAtMost ( 100 ) // 返回 90
这两个函数在你只需要限制单边范围(只有上限或只有下限)时非常方便。
空安全 这一点是Kotlin的核心设计,也是它的一大卖点。Kotlin的空安全设计对于开发者来说是一种福利,它可以在编译阶段就发现很多空指针异常,而不是在运行时才发现。
作为一名 Android 开发者,你肯定深知 NullPointerException (NPE) 是 Java 开发中常见且令人头疼的问题。Kotlin 的设计目标之一就是消除这种运行时错误,通过在编译时强制进行空安全检查来解决这个问题。
1. 可空类型与非空类型 Java里的空指针(NPE)报错,Kotlin 中也有类似的,就是使用 lateinit var 的不可空变量时,需要注意初始化和使用时机不对的情况下,有可能会报 UnInitializedPropertyAccessException 异常。所以在一些不确定是否在初始化完毕之后调用的方法里,使用变量时,最好加一层初始化判断。
// 延迟初始化变量
lateinit var str : String
// 使用时进行初始化判断
if ( :: str . isInitialized ) {
print ( str . length )
}
2. 安全调用操作符 (?.) 当处理可空类型的变量时,你不能直接访问它的成员(例如调用方法或访问属性)。你需要使用安全调用操作符 ?.。
如果 ?. 左边的表达式不为 null,则会正常执行右边的操作。 如果 ?. 左边的表达式为 null,则整个表达式的结果为 null,并且不会执行右边的操作,从而避免了 NPE。val name : String ? = null
val length : Int ? = name ?. length // 如果 name 为 null,则 length 也为 null
println ( length ) // 输出: null
val name2 : String ? = "Hello"
val length2 : Int ? = name2 ?. length
println ( length2 ) // 输出: 5
3. Elvis 操作符 (?:) Elvis 操作符 ?: 提供了一种简洁的方式来处理可空值,当左边的表达式为 null 时,提供一个默认值。
如果 ?: 左边的表达式不为 null,则返回左边的值。 如果 ?: 左边的表达式为 null,则返回 ?: 右边的默认值。val name : String ? = null
val length : Int = name ?. length ?: 0 // 如果 name?.length 为 null,则 length 为 0
println ( length ) // 输出: 0
val name2 : String ? = "World"
val length2 : Int = name2 ?. length ?: 0
println ( length2 ) // 输出: 5
4. 非空断言操作符 (!!) 非空断言操作符 !! 允许你将任何可空类型的值转换为非空类型。然而,如果 !! 左边的表达式为 null,它会抛出一个 NullPointerException。
val name : String ? = null
// val length: Int = name!!.length // 运行时会抛出 NullPointerException
这个操作符应该慎用,只有当你非常确定某个值在特定时刻不可能为 null 时才使用。它的作用是告诉编译器“我保证这里不会是 null,如果错了,就让它崩溃吧”。
5. let 函数 let 是一个作用域函数,常用于对非空对象执行操作。如果接收者对象不为 null,let 函数会执行给定的 lambda 表达式,并将接收者作为 it 参数传入。
val name : String ? = "Kotlin"
name ?. let {
// 只有当 name 不为 null 时才执行这里的代码
println ( "The name is ${it.toUpperCase()}" )
}
val name2 : String ? = null
name2 ?. let {
// 这段代码不会执行
println ( "This will not be printed if name2 is null" )
}
6. 安全类型转换 (as?) 安全类型转换 as? 尝试将一个值转换为指定的类型,如果转换失败,则返回 null,而不是抛出 ClassCastException。
val obj : Any = "Hello"
val str : String ? = obj as ? String // str 为 "Hello"
val num : Any = 123
val str2 : String ? = num as ? String // str2 为 null
Kotlin 的空安全设计非常严格,但当它需要与 Java 代码交互时,就面临一个挑战。Java 不像 Kotlin 那样在类型系统中强制空安全,Java 的引用可以是 null,也可以是非 null,这在编译时是无法确定的。
为了解决这个问题,Kotlin 引入了平台类型 (Platform Types) 。
平台类型是指 Kotlin 编译器 无法确定其空性 的类型,通常是来自 Java 代码的类型。当你从 Java 代码中调用方法或访问字段时,Kotlin 编译器无法知道这些值是否可能为 null。
平台类型在 Kotlin 中用 T! 的形式表示(例如 String!),但你不能在代码中显式地声明一个平台类型。它只会在编译器推断出类型时出现。
例如,如果你有一个 Java 类:
// JavaClass.java
public class JavaClass {
public String getName () {
return null ; // Java 中可以返回 null
}
public void printValue ( String value ) {
System . out . println ( value . length ()); // 如果 value 为 null,这里会抛出 NPE
}
}
在 Kotlin 中使用 JavaClass:
// Kotlin code
val javaClass = JavaClass ()
val name = javaClass . name // name 的类型会被推断为 String! (平台类型)
平台类型的特点和处理 当你操作一个平台类型的值时,Kotlin 编译器不会强制进行空安全检查。这意味着你可以像在 Java 中那样使用它,但这也意味着你可能会遇到 NullPointerException,因为它可能在运行时为 null。
对于平台类型,Kotlin 将空性的责任交给了开发者。你可以选择将其视为可空类型 (String?) 或非空类型 (String)。
如果你确定它不会是 null,可以将其赋值给非空类型。如果运行时是 null,就会抛出 NPE。 如果你不确定它是否为 null,最好将其赋值给可空类型,并使用安全调用操作符或其他空处理机制。 在Java代码中,为了帮助 Kotlin 编译器更好地理解 Java 代码的空性,Java 库可以使用空性注解(如 @Nullable, @NotNull,来自 JetBrains、AndroidX、JSR-305 等)。如果 Java 代码使用了这些注解,Kotlin 编译器可以根据注解信息 将 Java 类型映射为 Kotlin 的可空或非空类型 ,从而避免平台类型带来的不确定性。
总结来说,平台类型是 Kotlin 和 Java 互操作性中的一个“妥协点”,它允许你在 Kotlin 中使用 Java 代码,但同时也提醒你,在这些特定情况下,Kotlin 的编译时空安全保护可能会失效,你需要更加小心地处理潜在的 null 值。
单例类 Java中比较通用的单例类写法一般为static关键字声明的懒加载同步方法。
public class Singleton {
private static Singleton instance ;
private Singleton () {
// 私有构造函数,防止外部实例化
}
public static synchronized Singleton getInstance () {
if ( instance == null ) {
instance = new Singleton ();
}
return instance ;
}
}
在Kotlin中,想要定义一类为全局单例模式,只需要使用 object 关键字来声明类就可以了。
object Singleton {
fun doSomething () {
// 单例对象的方法
}
}
// 使用
Singleton . doSomething ()
这个写法等同于Java中的饿汉单例模式,对于开发者写起来更简洁,反编译之后的java代码如下:
public final class Singleton {
@NotNull
public static final Singleton INSTANCE = new Singleton ();
private Singleton () {
}
}
伴生对象 在Kotlin中,每个类都可以有一个伴生对象 。伴生对象的成员可以直接通过类名调用,而不需要实例化类。
class MyClass {
companion object {
fun doSomething () {
// 伴生对象的方法
}
}
}
// 使用
MyClass . doSomething ()
与Java中的静态方法类似,Kotlin中的伴生对象方法在反编译后的Java代码中也会被转换为静态方法。
在Android中,可以把类的TAG,和这个类强相关的一些常量,都定义在这个类的伴生对象中。
字符串模板 Java中,字符串和变量的结合需要使用加号+,而Kotlin中可以使用字符串模板来简化这个过程。在Kotlin中,可以使用字符串模板来动态构建字符串。字符串模板以$开头,在其中可以嵌入变量或表达式。
val name = "Kotlin"
val message = "Hello, $name!" // 字符串模板,结果为 "Hello, Kotlin!"
如果是和常量拼接,在编译器就会直接内联优化为字符串。如果是变量拼接,最后运行时实际上还是使用StringBuilder来拼接字符串。
扩展函数 在Kotlin中,可以为现有的类添加新的函数,而不需要修改类的源代码。这些新的函数被称为扩展函数。扩展函数允许你在不继承类的情况下,向类添加新的行为。
例如,String 类并没有一个内置的 isPalindrome() 方法来检查一个字符串是否是回文 ,但你可以通过扩展函数为它添加这个功能:
fun String . isPalindrome (): Boolean {
val cleanedString = this . lowercase (). replace ( Regex ( "[^a-z0-9]" ), "" )
return cleanedString == cleanedString . reversed ()
}
fun main () {
val word = "madam"
println ( word . isPalindrome ()) // 输出: true
}
在这个例子中:
fun String.isPalindrome(): Boolean 定义了一个扩展函数。String. 表示这个函数是 String 类的扩展。在函数内部,this 关键字引用了调用该函数的 String 实例。 扩展函数让代码看起来更自然。比如 string.isPalindrome() 比 StringUtils.isPalindrome(string) 更直观。扩展函数可以把这些“工具”方法直接挂载到它们所操作的类上,减少了Utils工具类的数量,使得代码结构更清晰。
实现原理 Kotlin 的扩展函数实际上是一个静态函数 。当 Kotlin 编译器处理扩展函数时,它会将其转换为一个普通的静态方法,这个静态方法会将接收者对象作为第一个参数。
例如,上面的 String.isPalindrome() 扩展函数在编译后,大致等价于一个 Java 中的静态方法:
// 编译后的伪 Java 代码
public final class StringExtensionsKt { // 自动生成的文件名,通常是文件名 + Kt
public static final boolean isPalindrome ( @NotNull String $receiver ) {
// 函数体内部的 this 对应于这里的 $receiver 参数
String cleanedString = $receiver . toLowerCase (). replaceAll ( "[^a-z0-9]" , "" );
return cleanedString . equals ( new StringBuilder ( cleanedString ). reverse (). toString ());
}
}
然后,当你调用 word.isPalindrome() 时,编译器会将其转换为对这个静态方法的调用:
// 编译后的伪 Java 代码
StringExtensionsKt . isPalindrome ( word );
这就是为什么扩展函数不能访问其接收者的 private 或 protected 成员——因为它并不是真正意义上的成员函数,它只是一个方便的语法糖。
Android 开发中的常见应用 在 Android 开发中,扩展函数无处不在,极大地简化了代码:
View 扩展 : 为 View 添加方便的函数,比如 View.show()、View.hide()、View.gone()。fun View . show () {
this . visibility = View . VISIBLE
}
fun View . hide () {
this . visibility = View . INVISIBLE
}
fun View . gone () {
this . visibility = View . GONE
}
Context 扩展 : 简化 Toast 显示、资源获取等操作。fun Context . toast ( message : String , duration : Int = Toast . LENGTH_SHORT ) {
Toast . makeText ( this , message , duration ). show ()
}
// 使用: context.toast("Hello!")
Fragment/Activity 扩展 : 简化 FragmentTransaction 或 Intent 的使用。数据类型转换 : 比如为 Int 或 Long 添加 toPx() 或 toDp() 转换函数。高阶函数 let、with、apply、run、also 也叫操作域函数,它们是 Kotlin 标准库中非常强大且常用的高阶函数。作为 Android 开发者,你肯定会在日常工作中频繁遇到和使用它们,因为它们能让你的代码更简洁、更易读,尤其是处理对象的配置、转换或安全调用时。
作用域函数是一种特殊的函数,它们的主要目的是在你提供的 lambda 表达式内部创建一个 临时作用域 。在这个作用域内,你可以直接访问(或引用)你所操作的对象,从而避免重复写对象名,让代码更紧凑。
Kotlin 提供了五种主要的作用域函数:let、run、with、apply 和 also。它们之间的主要区别在于:
引用上下文对象的方式:使用 this 还是 it。 返回值:返回上下文对象本身还是 lambda 表达式的结果。 apply apply 函数,它的 lambda 表达式的 最后一行代码会自动作为返回值 返回。不同之处在于 apply 函数始终返回 上下文对象本身 。
val result = "Kotlin" . apply {
println ( "Length: $length" ) // 可以直接访问 String 的属性
}
// 输出: Length: 6
// 因为 apply 始终返回上下文对象本身,所以可以直接链式调用
"Kotlin" . apply {
println ( "Length: $length" )
}. also {
println ( "Also: $it" ) // 输出: Also: Kotlin
}
在Android中,apply通常用于对一个对象进行初始化或设置属性。例如,在RecyclerView的初始化过程中。
recyclerView . apply {
layoutManager = LinearLayoutManager ( context )
addItemDecoration ( MyDecoration ( context )) // 添加分隔线装饰器
setHasFixedSize ( true ) // 固定大小,提高性能
adapter = myAdapter // 设置适配器
}
let 上面的空安全有提到一次,最常用于判空场景,非空后执行let中的代码。返回值 lambda 表达式的最后一行结果。还可以很方便地在链式调用中对结果进行操作或转换。
val name : String ? = "Alice"
// 传统空检查
if ( name != null ) {
println ( name . length )
}
// 使用 let 进行空安全操作
name ?. let {
// 这里的 it 就是非空的 name
println ( it . length )
}
// 链式调用和转换
val result = "Hello Kotlin"
. length
. let { it * 2 } // 将长度乘以2
. let { "Double length: $it" } // 转换为字符串
println ( result ) // 输出: Double length: 24
run 内部引用方式为 this ,返回值为 lambda 表达式的最后一行结果。
run 主要有两种应用形式:
作为扩展函数调用 (在对象上调用)
val user = User ( "Bob" , 30 )
val userDescription = user . run {
// 这里的 this 就是 user 对象
"Name: ${this.name}, Age: ${this.age}" // 返回这个字符串
}
println ( userDescription ) // 输出: Name: Bob, Age: 30
// 结合空安全 (类似 let)
val greeting : String ? = "Hello"
val finalMessage = greeting ?. run {
// 这里的 this 就是非空的 "Hello"
toUpperCase () + "!" // 返回 "HELLO!"
} ?: "No greeting" // 如果 greeting 为 null,则返回 "No greeting"
println ( finalMessage ) // 输出: HELLO!
独立调用
val message = run {
val x = 10
val y = 20
"Sum: ${x + y}" // 返回这个字符串
}
println ( message ) // 输出: Sum: 30
with 引用方式为 this ,返回值 lambda 表达式的最后一行结果。 已知非空的对象执行一系列操作,而不需要链式调用。与 run 作为扩展函数类似,但 with 不是扩展函数写法,它将对象作为第一个参数传入。
val configuration = Configuration ( "Debug" , 1024 )
val configDetails = with ( configuration ) {
// 这里的 this 就是 configuration 对象
println ( "Configuring system..." )
"Mode: ${mode}, Size: ${maxSize}MB" // 返回这个字符串
}
println ( configDetails ) // 输出: Mode: Debug, Size: 1024MB
also 内部引用方式为 it 。返回 上下文对象本身 。主要用于 执行对象的附加操作,不影响对象本身,通常用于副作用 (side-effects) 。例如,日志记录、调试输出或在对象准备好后执行一些不影响其状态的操作。
val numbers = mutableListOf ( 1 , 2 , 3 )
val processedNumbers = numbers . also {
// 这里的 it 就是 numbers 列表
println ( "Before adding: $it" ) // 打印当前列表状态
it . add ( 4 )
}. also {
println ( "After adding: $it" ) // 再次打印列表状态
}
// also 返回 numbers 列表本身,所以 processedNumbers 仍然是 numbers
println ( processedNumbers ) // 输出: [1, 2, 3, 4]
即also 适用于你想在不改变原始对象的情况下,对其执行一些额外操作的场景。
操作域函数小结 let : 如果你想在代码块中对一个可空对象执行操作 ,或者想对结果进行转换 ,并返回转换后的值。run :作为扩展函数 : 如果你想配置一个对象并计算一个结果 ,或者结合空安全和 this 引用。作为非扩展函数 : 如果你想封装一段语句 ,并返回其结果。with : 如果你有一个非空对象 ,并且想在其作用域内执行一系列操作并返回一个结果 。apply : 如果你想配置一个对象并返回该对象本身 。非常适合链式设置多个属性。also : 如果你想在不改变对象的情况下,对它执行一些额外操作或副作用 (例如日志记录、调试打印),并返回该对象本身。val originSting = "Kotlin"
val letString = originSting . let {
it . uppercase ()
}
val applyString = originSting . apply {
uppercase ()
}
val withString = with ( originSting ) {
uppercase ()
}
val runString = originSting . run {
uppercase ()
}
val alsoString = originSting . also {
it . uppercase ()
}
/**
letString: KOTLIN
applyString: Kotlin
withString: KOTLIN
runString: KOTLIN
alsoString: Kotlin
*/
根据运行结果可以看出,apply和also都是返回操作的对象本身的,另外的三个,都是返回最后一行表达式的结果。在使用操作域函数时,需要注意这一点以免拿到不符合预期的数据。
map扩展函数 map 可以对List,Map,Set等集合对象中的元素进行转换,生成一个新的集合。
例如:
val numbers = listOf ( 1 , 2 , 3 , 4 )
val doubled = numbers . map { it * 2 }
// 结果: [2, 4, 6, 8]
// 或显式命名参数
val squared = numbers . map { number -> number * number }
// 结果: [1, 4, 9, 16]
在Android中,也可以用在初始化要显示的数据集上,语法更简洁:
private val functionList = listOf (
"壁纸取色测试" to {
startActivity ( Intent ( this , WallpaperTestActivity :: class . java ))
},
"弹一个Toast" to {
Toast . makeText ( this , "一个普通的Toast" , Toast . LENGTH_SHORT ). show ()
},
"设备Root状态" to {
startActivity ( Intent ( this , RootInfoActivity :: class . java ))
},
"CPU信息" to {
startActivity ( Intent ( this , CpuInfoActivity :: class . java ))
},
). map { ( name , task ) -> FunctionItem ( name , task ) }
还可以和 Flow 数据流一起作用,在数据发送之前使用 map 预处理一遍:
fun mapTest () {
CoroutineScope ( Dispatchers . IO ). launch {
flowOf ( 1 , 2 , 3 , 4 , 5 ). map {
it + 1
}. collectLatest {
Log . i ( TAG , "mapTest collect $it" )
}
}
}
use扩展函数 use 函数是 Kotlin 标准库为实现了 Closeable 或 AutoCloseable 接口的类(如 FileInputStream、BufferedReader 等)提供的扩展函数。
它主要用于资源管理(如文件、网络连接、数据库连接等),它可以确保资源在使用完毕后被正确关闭,即使发生异常也能保证资源释放,防止内存泄露。其底层实现实际上也是对try-catch-finally的封装。
用法举例,独取一个文件的内容:
fun readFile () {
val file = File ( "example.txt" )
FileInputStream ( file ). use { inputStream ->
val bytes = inputStream . readBytes ()
println ( String ( bytes ))
} // inputStream 自动关闭
}
数据库连接:
fun queryDatabase () {
val connection : Connection = DriverManager . getConnection ( "jdbc:mysql://localhost:3306/db" )
connection . use { conn ->
// 执行 SQL 查询
conn . createStatement (). use { statement ->
statement . executeQuery ( "SELECT * FROM users" ). use { resultSet ->
while ( resultSet . next ()) {
println ( resultSet . getString ( "name" ))
}
}
}
} // conn 自动关闭
}
Kotlin集合 对于Kotlin和Java中的集合简要对比,专门提取来一篇来记录:
Kotlin中的集合
data class数据类 data class 是 Kotlin 中的一个重要概念,通常用于表示数据对象。
数据类是专门为存储数据而设计的类。 Kotlin 编译器会自动为数据类生成许多有用的成员函数,从而省去了你手动编写这些函数的麻烦。这使得你的代码更简洁、更安全、更易读。
在 Java 中,为了实现一个简单的数据持有类,你需要写大量的模板代码(构造函数、getter/setter、equals()、hashCode()、toString() 等),而 Kotlin 的数据类用一个关键字就搞定了这一切。
data class User ( val name : String , val age : Int )
所有属性的 Getters (以及 var 属性的 Setters) : 尽管在 Kotlin 中我们通常直接访问属性,但底层它们依然是存在的。
val user = User ( "Alice" , 30 )
println ( user . name ) // 访问 name 属性
equals(other: Any?) : 用于比较两个数据类对象是否相等。当且仅当两个对象的类型相同,并且主构造函数中声明的所有属性的值都相等时,它们才被认为是相等的。
val user1 = User ( "Alice" , 30 )
val user2 = User ( "Alice" , 30 )
val user3 = User ( "Bob" , 25 )
println ( user1 == user2 ) // 输出: true (因为属性值相同)
println ( user1 == user3 ) // 输出: false
注意: 传统的类比较的是内存地址(引用相等),而数据类比较的是内容(结构相等)。
hashCode() : 返回一个基于主构造函数中所有属性的哈希码。这在将数据类对象存储在哈希集合(如 HashSet 或 HashMap)中时至关重要。equals() 和 hashCode() 必须保持一致性(如果两个对象 equals 返回 true,它们的 hashCode 也必须相同)。
val userSet = hashSetOf ( user1 )
println ( userSet . contains ( user2 )) // 输出: true (因为 user2 的 equals 和 hashCode 与 user1 相同)
toString() : 返回一个包含类名和所有属性及其值的字符串表示。这对于日志记录和调试非常有用。
val user = User ( "Alice" , 30 )
println ( user ) // 输出: User(name=Alice, age=30)
componentN() 函数 : 为每个在主构造函数中声明的属性生成一个 componentN() 函数,其中 N 是属性在声明时的顺序(component1() 对应第一个属性,component2() 对应第二个,以此类推)。这些函数使得数据类可以支持解构声明 (Destructuring Declarations) 。
val ( name , age ) = User ( "Alice" , 30 ) // 解构声明
println ( "Name: $name, Age: $age" ) // 输出: Name: Alice, Age: 30
copy() : 创建一个新对象,复制现有对象的所有属性,同时允许你选择性地修改某些属性的值。这对于创建对象的一个副本但需要轻微修改时非常有用,因为数据类通常是不可变的(尽管也可以有 var 属性)。
val originalUser = User ( "Alice" , 30 )
val copiedUser = originalUser . copy ( age = 31 ) // 复制 originalUser,只改变 age 属性
val anotherCopiedUser = originalUser . copy ( name = "Bob" ) // 改变 name 属性
println ( originalUser ) // 输出: User(name=Alice, age=30)
println ( copiedUser ) // 输出: User(name=Alice, age=31)
println ( anotherCopiedUser ) // 输出: User(name=Bob, age=30)
数据类使用注意事项 主构造函数必须至少有一个参数 :所有自动生成的函数都是基于主构造函数中声明的属性。主构造函数的所有参数都必须标记为 val 或 var :这是为了确保它们是类中的属性,而不是仅仅是构造函数参数。不能是 abstract、open、sealed 或 inner 类 :数据类通常是最终的,不适合继承层次结构。可以有其他成员 : 除了自动生成的函数,你也可以在数据类中定义自己的函数、属性或伴生对象。
data class Product ( val id : String , val name : String , var price : Double ) {
// 自定义函数
fun displayInfo () {
println ( "Product ID: $id, Name: $name, Price: $price" )
}
// 伴生对象
companion object {
const val DEFAULT_CURRENCY = "USD"
}
}
属性的默认值 : 你可以为数据类的主构造函数属性提供默认值。
data class Settings ( val theme : String = "dark" , val notificationsEnabled : Boolean = true )
val defaultSettings = Settings () // 使用默认值
val customSettings = Settings ( theme = "light" ) // 覆盖默认值
Android中常用场景 在 Android 开发中,数据类无处不在:
API 响应模型 : 当你从 RESTful API 获取数据时,通常会定义数据类来映射 JSON 或 XML 结构。data class Post ( val userId : Int , val id : Int , val title : String , val body : String )
数据库实体 : 当使用 Room Persistence Library 或其他 ORM 框架时,数据类可以很好地表示数据库表中的一行数据。@Entity ( tableName = "users" )
data class UserEntity ( @PrimaryKey val id : Long , val name : String , val email : String )
UI 状态 : 在 MVVM 或 MVI 架构中,数据类常用于表示 UI 的当前状态,方便进行状态的更新和比较。data class UserProfileState (
val isLoading : Boolean = false ,
val user : User ? = null ,
val errorMessage : String ? = null
)
事件 (Events) : 在事件驱动的架构中,数据类可以很好地表示各种事件。sealed class LoginEvent {
data class Success ( val userId : String ) : LoginEvent ()
data class Error ( val message : String ) : LoginEvent ()
object Loading : LoginEvent ()
}
Kotlin高级特性 sealed class和sealed interface Kotlin 的密封类 (Sealed Class) 是一个非常棒的特性,尤其是在处理有限的、受限的类继承结构时。它能让你的代码更安全、更具表达力,并且在与 when 表达式结合使用时,能提供强大的编译时检查。
密封类是一种限制类继承层次结构的特殊抽象类。 它的主要目的是声明一个受限的类层次结构 ,其中所有可能的子类都必须在同一文件内 声明(Kotlin 1.5 之后可以在同一个模块内的任何文件中声明,但通常仍推荐在同一文件内以保持紧凑性)。
这就意味着,编译器在编译时就知道了这个密封类的所有可能直接子类。这种“已知子类”的特性是密封类最有价值的地方。
// 定义一个密封类来表示网络请求的结果
sealed class NetworkResult {
data class Success ( val data : String ) : NetworkResult () // 子类可以是数据类
data class Error ( val message : String ) : NetworkResult () // 子类可以是数据类
object Loading : NetworkResult () // 子类可以是单例对象
class Idle : NetworkResult () // 子类也可以是普通类
}
在这个例子中:
NetworkResult 是一个密封类。Success、Error、Loading 和 Idle 是 NetworkResult 的直接子类。重要 : 所有的这些子类都必须在定义 NetworkResult 的同一文件内 (或者在 Kotlin 1.5+ 中,在同一模块内),这样编译器才能“知道”它们。1. 确保穷举性检查 (Exhaustiveness Checking) 与 when 表达式 这是密封类最强大的特性。当你在 when 表达式中使用密封类的实例时,如果 when 覆盖了所有可能的子类型,Kotlin 编译器会强制你处理所有可能的子类,并且不需要 else 分支 。如果遗漏了某个子类,编译器会报错 ,从而防止运行时错误。
fun handleNetworkResult ( result : NetworkResult ) {
when ( result ) {
is NetworkResult . Success -> {
println ( "数据加载成功: ${result.data}" )
}
is NetworkResult . Error -> {
println ( "加载失败: ${result.message}" )
}
NetworkResult . Loading -> { // 注意:对于 object,直接引用即可
println ( "正在加载中..." )
}
is NetworkResult . Idle -> {
println ( "网络请求处于空闲状态。" )
}
// 不需要 else 分支,因为编译器知道所有可能的子类型都被处理了
}
}
这对于构建健壮的应用程序至关重要,特别是在处理 UI 状态、事件或网络响应时。
2. 更好的类型安全和代码可读性 密封类提供了一种清晰的方式来建模有限的状态。例如,一个 UI 组件的状态可能只有“加载中”、“显示数据”或“显示错误”几种。使用密封类可以明确地表示这些状态,使得代码的意图一目了然,并减少了引入无效状态的可能性。
3. 作为枚举的替代(更强大) 虽然枚举 (enum class) 也能表示一组有限的值,但枚举的每个成员都是一个简单的实例,不能携带额外的状态 。而密封类的每个子类可以是独立的类,可以拥有自己的属性和行为,这使得它比枚举更加灵活和强大。
// 枚举无法携带额外数据
enum class Color { RED , GREEN , BLUE }
// 密封类可以携带额外数据
sealed class Shape {
data class Circle ( val radius : Double ) : Shape ()
class Square ( val side : Double ) : Shape ()
object Triangle : Shape () // 也可以是无状态的单例
}
密封类与枚举 (Enum Class) 的场景选择
使用枚举 : 如果你只需要表示一组固定且不携带额外数据 的常量集(例如方向:上、下、左、右;或简单的状态:开启、关闭)。使用密封类 : 如果你需要表示一组有限的、可携带不同数据或具有不同行为的子类型 (例如网络请求结果、UI 状态、事件)。密封类在 Android 开发中的常见应用 在 Android 开发中,密封类几乎无处不在,是管理复杂状态和事件的利器:
网络请求结果 : 如上面示例所示,表示 API 调用的不同状态(成功、失败、加载中)。sealed class Resource < out T > { // 可以是泛型
data class Success < out T >( val data : T ) : Resource < T >()
data class Error ( val message : String , val errorCode : Int ) : Resource < Nothing >()
object Loading : Resource < Nothing >()
}
UI 状态 : 定义一个屏幕可能拥有的所有状态。sealed class UserViewState {
object Loading : UserViewState ()
data class Loaded ( val user : User ) : UserViewState ()
data class Error ( val errorMessage : String ) : UserViewState ()
object Empty : UserViewState ()
}
用户交互事件 : 表示用户在界面上的各种操作。sealed class ProfileEvent {
object LoadProfile : ProfileEvent ()
data class UpdateName ( val newName : String ) : ProfileEvent ()
object Logout : ProfileEvent ()
}
RecyclerView 列表项 : 如果一个 RecyclerView 可以显示不同类型的列表项(Header, Item, Footer),可以用密封类来建模。协程 Kotlin的协程也是广为开发者讨论的一个异步框架,在Android应用开发过程中,几乎可以完全替代线程的使用,并且以同步方式写异步代码看起来也比较优雅。
详细的有多篇文章介绍过:
Kotlin协程的基础使用
Kotlin协程浅谈
Kotlin协程的取消与异常处理
Kotlin协程挂起恢复源码解析
内联函数 (Inline Functions) 与交叉内联 (Crossinline)/无内联 (Noinline) 关于这几个内联相关的关键字,由另一篇文章也记录过:
Kotlin的inline&crossinline&noinline关键字
泛型的 in out 和 Reified 关键字 Kotlin协变和逆变
委托 在 Kotlin 中,委托(Delegation) 是一种强大的设计模式,它允许对象将部分功能委托给另一个辅助对象来实现。Kotlin 原生支持多种委托方式,主要分为以下几种:
类委托(Class Delegation) 通过 by 关键字,将类的接口实现委托给另一个对象,常用于 “装饰器模式” 或 “代理模式”。
示例:委托接口实现
interface Printer {
fun print ( message : String )
}
class DefaultPrinter : Printer {
override fun print ( message : String ) {
println ( "Default Printer: $message" )
}
}
// 委托给 printer 对象
class CustomPrinter ( private val printer : Printer ) : Printer by printer {
// 可以覆盖部分方法
override fun print ( message : String ) {
println ( "Before Printing..." )
printer . print ( message ) // 调用委托对象的方法
println ( "After Printing..." )
}
}
fun main () {
val defaultPrinter = DefaultPrinter ()
val customPrinter = CustomPrinter ( defaultPrinter )
customPrinter . print ( "Hello, Kotlin!" )
}
输出:
Before Printing… Default Printer: Hello, Kotlin! After Printing…
适用场景:
增强或修改现有类的行为(如日志、缓存、权限控制)。 避免继承,使用组合代替。 属性委托(Property Delegation) Kotlin 提供标准库委托(如 lazy、observable),也可以自定义委托。
(1) lazy 延迟初始化
val lazyValue : String by lazy {
println ( "Computed only once!" )
"Hello"
}
fun main () {
println ( lazyValue ) // 第一次访问时计算
println ( lazyValue ) // 直接返回缓存值
}
输出:
Computed only once! Hello Hello
(2) observable 监听属性变化
import kotlin.properties.Delegates
var observedValue : Int by Delegates . observable ( 0 ) { _ , old , new ->
println ( "Value changed from $old to $new" )
}
fun main () {
observedValue = 10 // 触发回调
observedValue = 20 // 再次触发
}
输出:
Value changed from 0 to 10 Value changed from 10 to 20
(3) vetoable 可拦截修改
var positiveNumber : Int by Delegates . vetoable ( 0 ) { _ , old , new ->
new > 0 // 只有 new > 0 时才允许修改
}
fun main () {
positiveNumber = 10 // 允许
println ( positiveNumber ) // 10
positiveNumber = - 5 // 拒绝修改
println ( positiveNumber ) // 仍然是 10
}
(4) 自定义属性委托
class StringDelegate ( private var initValue : String ) {
operator fun getValue ( thisRef : Any ?, property : KProperty < * >): String {
println ( "Getting value: $initValue" )
return initValue
}
operator fun setValue ( thisRef : Any ?, property : KProperty < * >, value : String ) {
println ( "Setting value: $value" )
initValue = value
}
}
fun main () {
var text by StringDelegate ( "Default" )
println ( text ) // 调用 getValue
text = "New Value" // 调用 setValue
}
输出:
Getting value: Default Default Setting value: New Value
文章源码和介绍来自Kotlin官方网站
协程的取消 在一个长时间运行的应用程序中,你也许需要对你的后台协程进行细粒度的控制。 比如说,一个用户也许关闭了一个启动了协程的界面,那么现在协程的执行结果已经不再被需要了,这时,它应该是可以被取消的。
该 launch 函数返回了一个可以被用来取消运行中的协程的 Job:
import kotlinx.coroutines.*
fun main () = runBlocking {
//sampleStart
val job = launch {
repeat ( 1000 ) { i ->
println ( "job: I'm sleeping $i ..." )
delay ( 500L )
}
}
delay ( 1300L ) // 延迟一段时间
println ( "main: I'm tired of waiting!" )
job . cancel () // 取消该作业
job . join () // 等待作业执行结束
println ( "main: Now I can quit." )
//sampleEnd
}
程序执行后的输出如下:
job: I’m sleeping 0 … job: I’m sleeping 1 … job: I’m sleeping 2 … main: I’m tired of waiting! main: Now I can quit.
一旦 main 函数调用了 job.cancel,我们在其它的协程中就看不到任何输出,因为它被取消了。 这里也有一个可以使 Job 挂起的函数 cancelAndJoin 它合并了对 cancel 以及 join 的调用。
取消是协作的 协程的取消是 协作 的。一段协程代码必须协作才能被取消。 所有 kotlinx.coroutines 中的挂起函数都是 可被取消的 。它们检查协程的取消, 并在取消时抛出 CancellationException。 然而,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的,就如如下示例代码所示:
import kotlinx.coroutines.*
fun main () = runBlocking {
//sampleStart
val startTime = System . currentTimeMillis ()
val job = launch ( Dispatchers . Default ) {
var nextPrintTime = startTime
var i = 0
while ( i < 5 ) { // 一个执行计算的循环,只是为了占用 CPU
// 每秒打印消息两次
if ( System . currentTimeMillis () >= nextPrintTime ) {
println ( "job: I'm sleeping ${i++} ..." )
nextPrintTime += 500L
}
}
}
delay ( 1300L ) // 等待一段时间
println ( "main: I'm tired of waiting!" )
job . cancelAndJoin () // 取消一个作业并且等待它结束
println ( "main: Now I can quit." )
//sampleEnd
}
运行示例代码,并且我们可以看到它连续打印出了“I’m sleeping”,甚至在调用取消后, 作业仍然执行了五次循环迭代并运行到了它结束为止。
The same problem can be observed by catching a CancellationException and not rethrowing it:
import kotlinx.coroutines.*
fun main () = runBlocking {
//sampleStart
val job = launch ( Dispatchers . Default ) {
repeat ( 5 ) { i ->
try {
// print a message twice a second
println ( "job: I'm sleeping $i ..." )
delay ( 500 )
} catch ( e : Exception ) {
// log the exception
println ( e )
}
}
}
delay ( 1300L ) // delay a bit
println ( "main: I'm tired of waiting!" )
job . cancelAndJoin () // cancels the job and waits for its completion
println ( "main: Now I can quit." )
//sampleEnd
}
While catching Exception is an anti-pattern, this issue may surface in more subtle ways, like when using the runCatching function, which does not rethrow CancellationException.
使计算代码可取消 我们有两种方法来使执行计算的代码可以被取消。第一种方法是定期调用挂起函数来检查取消。对于这种目的 yield 是一个好的选择。 另一种方法是显式的检查取消状态。让我们试试第二种方法。
将前一个示例中的 while (i < 5) 替换为 while (isActive) 并重新运行它。
import kotlinx.coroutines.*
fun main () = runBlocking {
//sampleStart
val startTime = System . currentTimeMillis ()
val job = launch ( Dispatchers . Default ) {
var nextPrintTime = startTime
var i = 0
while ( isActive ) { // 可以被取消的计算循环
// 每秒打印消息两次
if ( System . currentTimeMillis () >= nextPrintTime ) {
println ( "job: I'm sleeping ${i++} ..." )
nextPrintTime += 500L
}
}
}
delay ( 1300L ) // 等待一段时间
println ( "main: I'm tired of waiting!" )
job . cancelAndJoin () // 取消该作业并等待它结束
println ( "main: Now I can quit." )
//sampleEnd
}
你可以看到,现在循环被取消了。isActive 是一个可以被使用在 CoroutineScope 中的扩展属性。
在 finally 中释放资源 我们通常使用如下的方法处理在被取消时抛出 CancellationException 的可被取消的挂起函数。比如说,try {……} finally {……} 表达式以及 Kotlin 的 use 函数一般在协程被取消的时候执行它们的终结动作:
import kotlinx.coroutines.*
fun main () = runBlocking {
//sampleStart
val job = launch {
try {
repeat ( 1000 ) { i ->
println ( "job: I'm sleeping $i ..." )
delay ( 500L )
}
} finally {
println ( "job: I'm running finally" )
}
}
delay ( 1300L ) // 延迟一段时间
println ( "main: I'm tired of waiting!" )
job . cancelAndJoin () // 取消该作业并且等待它结束
println ( "main: Now I can quit." )
//sampleEnd
}
join 和 cancelAndJoin 等待了所有的终结动作执行完毕, 所以运行示例得到了下面的输出:
job: I’m sleeping 0 … job: I’m sleeping 1 … job: I’m sleeping 2 … main: I’m tired of waiting! job: I’m running finally main: Now I can quit.
运行不能取消的代码块 在前一个例子中任何尝试在 finally 块中调用挂起函数的行为都会抛出 CancellationException,因为这里持续运行的代码是可以被取消的。通常,这并不是一个问题,所有良好的关闭操作(关闭一个文件、取消一个作业、或是关闭任何一种通信通道)通常都是非阻塞的,并且不会调用任何挂起函数。然而,在真实的案例中,当你需要挂起一个被取消的协程,你可以将相应的代码包装在 withContext(NonCancellable) {……} 中,并使用 withContext 函数以及 NonCancellable 上下文,见如下示例所示:
import kotlinx.coroutines.*
fun main () = runBlocking {
//sampleStart
val job = launch {
try {
repeat ( 1000 ) { i ->
println ( "job: I'm sleeping $i ..." )
delay ( 500L )
}
} finally {
withContext ( NonCancellable ) {
println ( "job: I'm running finally" )
delay ( 1000L )
println ( "job: And I've just delayed for 1 sec because I'm non-cancellable" )
}
}
}
delay ( 1300L ) // 延迟一段时间
println ( "main: I'm tired of waiting!" )
job . cancelAndJoin () // 取消该作业并等待它结束
println ( "main: Now I can quit." )
//sampleEnd
}
超时 在实践中绝大多数取消一个协程的理由是它有可能超时。 当你手动追踪一个相关 Job 的引用并启动了一个单独的协程在延迟后取消追踪,这里已经准备好使用 withTimeout 函数来做这件事。 来看看示例代码:
import kotlinx.coroutines.*
fun main () = runBlocking {
//sampleStart
withTimeout ( 1300L ) {
repeat ( 1000 ) { i ->
println ( "I'm sleeping $i ..." )
delay ( 500L )
}
}
//sampleEnd
}
运行后得到如下输出:
I’m sleeping 0 … I’m sleeping 1 … I’m sleeping 2 … Exception in thread “main” kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
withTimeout 抛出了 TimeoutCancellationException,它是 CancellationException 的子类。 我们之前没有在控制台上看到堆栈跟踪信息的打印。这是因为在被取消的协程中 CancellationException 被认为是协程执行结束的正常原因。 然而,在这个示例中我们在 main 函数中正确地使用了 withTimeout。
由于取消只是一个例外,所有的资源都使用常用的方法来关闭。 如果你需要做一些各类使用超时的特别的额外操作,可以使用类似 withTimeout 的 withTimeoutOrNull 函数,并把这些会超时的代码包装在 try {…} catch (e: TimeoutCancellationException) {…} 代码块中,而 withTimeoutOrNull 通过返回 null 来进行超时操作,从而替代抛出一个异常:
import kotlinx.coroutines.*
fun main () = runBlocking {
//sampleStart
val result = withTimeoutOrNull ( 1300L ) {
repeat ( 1000 ) { i ->
println ( "I'm sleeping $i ..." )
delay ( 500L )
}
"Done" // 在它运行得到结果之前取消它
}
println ( "Result is $result" )
//sampleEnd
}
运行这段代码时不再抛出异常:
I’m sleeping 0 … I’m sleeping 1 … I’m sleeping 2 … Result is null
异步超时和资源 withTimeout中的超时事件对于在其代码块中运行的代码来说是异步的,可以在任何时间发生,甚至可以在从超时代码块内部返回之前发生。如果您在代码块内打开或获取了某些资源,而这些资源需要在代码块外关闭或释放,请记住这一点。
例如,在这里我们用 Resource 类模仿了一个可关闭的资源,该类只需通过在其关闭函数中递增获取计数器和递减计数器来记录创建次数。现在,让我们创建大量的例行程序,每个例行程序都在 withTimeout 代码块末尾创建一个资源,并在代码块外释放该资源。我们添加了一个小延迟,这样就更有可能在 withTimeout 代码块已经完成时发生超时,从而导致资源泄漏。
import kotlinx.coroutines.*
//sampleStart
var acquired = 0
class Resource {
init { acquired ++ } // Acquire the resource
fun close () { acquired-- } // Release the resource
}
fun main () {
runBlocking {
repeat ( 10_000 ) { // Launch 10K coroutines
launch {
val resource = withTimeout ( 60 ) { // Timeout of 60 ms
delay ( 50 ) // Delay for 50 ms
Resource () // Acquire a resource and return it from withTimeout block
}
resource . close () // Release the resource
}
}
}
// Outside of runBlocking all coroutines have completed
println ( acquired ) // Print the number of resources still acquired
}
//sampleEnd
如果运行上述代码,你会发现它并不总是打印零值,不过这可能取决于你的机器的定时。你可能需要调整本示例中的超时时间,才能真正看到非零值。
需要注意的是,在这里通过 10K 例程对获取的计数器进行递增和递减是完全线程安全的,因为它总是在同一个线程(即 runBlocking 所使用的线程)中进行。关于这一点的更多解释,将在 “例程上下文 ”一章中进行。
要解决这个问题,可以在变量中存储对资源的引用,而不是从 withTimeout 代码块中返回。
import kotlinx.coroutines.*
var acquired = 0
class Resource {
init { acquired ++ } // Acquire the resource
fun close () { acquired-- } // Release the resource
}
fun main () {
//sampleStart
runBlocking {
repeat ( 10_000 ) { // Launch 10K coroutines
launch {
var resource : Resource ? = null // Not acquired yet
try {
withTimeout ( 60 ) { // Timeout of 60 ms
delay ( 50 ) // Delay for 50 ms
resource = Resource () // Store a resource to the variable if acquired
}
// We can do something else with the resource here
} finally {
resource ?. close () // Release the resource if it was acquired
}
}
}
}
// Outside of runBlocking all coroutines have completed
println ( acquired ) // Print the number of resources still acquired
//sampleEnd
}
本例始终打印 0。资源不会泄漏。
协程的异常处理 本节内容涵盖了异常处理与在异常上取消。 我们已经知道被取消的协程会在挂起点抛出 CancellationException 并且它会被协程的机制所忽略。在这里我们会看看在取消过程中抛出异常或同一个协程的多个子协程抛出异常时会发生什么。
异常的传播 协程构建器有两种形式:自动传播异常(launch)或向用户暴露异常(async 与 produce)。 当这些构建器用于创建一个根协程时,即该协程不是另一个协程的子协程, 前者这类构建器将异常视为未捕获异常,类似 Java 的 Thread.uncaughtExceptionHandler, 而后者则依赖用户来最终消费异常,例如通过 await 或 receive(produce 与 receive 的相关内容包含于通道章节)。
可以通过一个使用 GlobalScope 创建根协程的简单示例来进行演示:
GlobalScope 是一种微妙的应用程序接口,可能会产生非同小可的反作用。为整个应用程序创建根例行程序是 GlobalScope 罕见的合法用途之一,因此您必须通过 @OptIn(DelicateCoroutinesApi::class)明确选择使用 GlobalScope。
import kotlinx.coroutines.*
//sampleStart
@OptIn ( DelicateCoroutinesApi :: class )
fun main () = runBlocking {
val job = GlobalScope . launch { // launch 根协程
println ( "Throwing exception from launch" )
throw IndexOutOfBoundsException () // 我们将在控制台打印 Thread.defaultUncaughtExceptionHandler
}
job . join ()
println ( "Joined failed job" )
val deferred = GlobalScope . async { // async 根协程
println ( "Throwing exception from async" )
throw ArithmeticException () // 没有打印任何东西,依赖用户去调用等待
}
try {
deferred . await ()
println ( "Unreached" )
} catch ( e : ArithmeticException ) {
println ( "Caught ArithmeticException" )
}
}
//sampleEnd
这段代码的输出如下(调试):
Throwing exception from launch Exception in thread “DefaultDispatcher-worker-1 @coroutine#2” java.lang.IndexOutOfBoundsException Joined failed job Throwing exception from async Caught ArithmeticException
CoroutineExceptionHandler将未捕获异常打印到控制台的默认行为是可自定义的。
根协程中的 CoroutineExceptionHandler 上下文元素可以被用于这个根协程通用的 catch 块,及其所有可能自定义了异常处理的子协程。
它类似于 Thread.uncaughtExceptionHandler 。
你无法从 CoroutineExceptionHandler 的异常中恢复。当调用处理者的时候,协程已经完成并带有相应的异常。通常,该处理者用于记录异常,显示某种错误消息,终止和(或)重新启动应用程序。
CoroutineExceptionHandler 仅在未捕获的异常上调用 — 没有以其他任何方式处理的异常。 特别是,所有子协程(在另一个 Job 上下文中创建的协程)委托它们的父协程处理它们的异常,然后它们也委托给其父协程,以此类推直到根协程, 因此永远不会使用在其上下文中设置的 CoroutineExceptionHandler。 除此之外,async 构建器始终会捕获所有异常并将其表示在结果 Deferred 对象中, 因此它的 CoroutineExceptionHandler 也无效。
在监督作用域内运行的协程不会将异常传播到其父协程,并且会从此规则中排除。本文档的另一个小节——监督提供了更多细节。
import kotlinx.coroutines.*
@OptIn ( DelicateCoroutinesApi :: class )
fun main () = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _ , exception ->
println ( "CoroutineExceptionHandler got $exception" )
}
val job = GlobalScope . launch ( handler ) { // 根协程,运行在 GlobalScope 中
throw AssertionError ()
}
val deferred = GlobalScope . async ( handler ) { // 同样是根协程,但使用 async 代替了 launch
throw ArithmeticException () // 没有打印任何东西,依赖用户去调用 deferred.await()
}
joinAll ( job , deferred )
//sampleEnd
}
这段代码的输出如下:
CoroutineExceptionHandler got java.lang.AssertionError
取消与异常 取消与异常紧密相关。协程内部使用 CancellationException 来进行取消,这个异常会被所有的处理者忽略,所以那些可以被 catch 代码块捕获的异常仅仅应该被用来作为额外调试信息的资源。 当一个协程使用 Job.cancel 取消的时候,它会被终止,但是它不会取消它的父协程。
import kotlinx.coroutines.*
fun main () = runBlocking {
//sampleStart
val job = launch {
val child = launch {
try {
delay ( Long . MAX_VALUE )
} finally {
println ( "Child is cancelled" )
}
}
yield ()
println ( "Cancelling child" )
child . cancel ()
child . join ()
yield ()
println ( "Parent is not cancelled" )
}
job . join ()
//sampleEnd
}
这段代码的输出如下:
Cancelling child Child is cancelled Parent is not cancelled
如果一个协程遇到了 CancellationException 以外的异常,它将使用该异常取消它的父协程。 这个行为无法被覆盖,并且用于为结构化的并发(structured concurrency) 提供稳定的协程层级结构。 CoroutineExceptionHandler 的实现并不是用于子协程。
在这些示例中,CoroutineExceptionHandler 总是被设置在由 GlobalScope 启动的协程中。将异常处理者设置在 runBlocking 主作用域内启动的协程中是没有意义的,尽管子协程已经设置了异常处理者, 但是主协程也总是会被取消的。
当父协程的所有子协程都结束后,原始的异常才会被父协程处理, 见下面这个例子。
import kotlinx.coroutines.*
@OptIn ( DelicateCoroutinesApi :: class )
fun main () = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _ , exception ->
println ( "CoroutineExceptionHandler got $exception" )
}
val job = GlobalScope . launch ( handler ) {
launch { // 第一个子协程
try {
delay ( Long . MAX_VALUE )
} finally {
withContext ( NonCancellable ) {
println ( "Children are cancelled, but exception is not handled until all children terminate" )
delay ( 100 )
println ( "The first child finished its non cancellable block" )
}
}
}
launch { // 第二个子协程
delay ( 10 )
println ( "Second child throws an exception" )
throw ArithmeticException ()
}
}
job . join ()
//sampleEnd
}
这段代码的输出如下:
Second child throws an exception Children are cancelled, but exception is not handled until all children terminate The first child finished its non cancellable block CoroutineExceptionHandler got java.lang.ArithmeticException
异常聚合 当协程的多个子协程因异常而失败时, 一般规则是“取第一个异常”,因此将处理第一个异常。 在第一个异常之后发生的所有其他异常都作为被抑制的异常绑定至第一个异常。
import kotlinx.coroutines.*
import java.io.*
@OptIn ( DelicateCoroutinesApi :: class )
fun main () = runBlocking {
val handler = CoroutineExceptionHandler { _ , exception ->
println ( "CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}" )
}
val job = GlobalScope . launch ( handler ) {
launch {
try {
delay ( Long . MAX_VALUE ) // 当另一个同级的协程因 IOException 失败时,它将被取消
} finally {
throw ArithmeticException () // 第二个异常
}
}
launch {
delay ( 100 )
throw IOException () // 首个异常
}
delay ( Long . MAX_VALUE )
}
job . join ()
}
这段代码的输出如下:
CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]
注意,这个机制当前只能在 Java 1.7 以上的版本中使用。 在 JS 和原生环境下暂时会受到限制,但将来会取消。
取消异常是透明的,默认情况下是未包装的:
import kotlinx.coroutines.*
import java.io.*
@OptIn ( DelicateCoroutinesApi :: class )
fun main () = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _ , exception ->
println ( "CoroutineExceptionHandler got $exception" )
}
val job = GlobalScope . launch ( handler ) {
val innerJob = launch { // 该栈内的协程都将被取消
launch {
launch {
throw IOException () // 原始异常
}
}
}
try {
innerJob . join ()
} catch ( e : CancellationException ) {
println ( "Rethrowing CancellationException with original cause" )
throw e // 取消异常被重新抛出,但原始 IOException 得到了处理
}
}
job . join ()
//sampleEnd
}
这段代码的输出如下:
Rethrowing CancellationException with original cause CoroutineExceptionHandler got java.io.IOException
监督 正如我们之前研究的那样,取消是在协程的整个层次结构中传播的双向关系。让我们看一下需要单向取消的情况。
此类需求的一个良好示例是在其作用域内定义作业的 UI 组件。如果任何一个 UI 的子作业执行失败了,它并不总是有必要取消(有效地杀死)整个 UI 组件, 但是如果 UI 组件被销毁了(并且它的作业也被取消了),由于其结果不再需要了,因此有必要取消所有子作业。
另一个例子是服务进程孵化了一些子作业并且需要 监督 它们的执行,追踪它们的故障并在这些子作业执行失败的时候重启。
SupervisorJob SupervisorJob 可以用于这些目的。 它类似于常规的 Job,唯一的不同是:SupervisorJob 的取消只会向下传播。这是很容易用以下示例演示:
import kotlinx.coroutines.*
fun main () = runBlocking {
//sampleStart
val supervisor = SupervisorJob ()
with ( CoroutineScope ( coroutineContext + supervisor )) {
// 启动第一个子作业——这个示例将会忽略它的异常(不要在实践中这么做!)
val firstChild = launch ( CoroutineExceptionHandler { _ , _ -> }) {
println ( "The first child is failing" )
throw AssertionError ( "The first child is cancelled" )
}
// 启动第二个子作业
val secondChild = launch {
firstChild . join ()
// 取消了第一个子作业且没有传播给第二个子作业
println ( "The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active" )
try {
delay ( Long . MAX_VALUE )
} finally {
// 但是取消了监督的传播
println ( "The second child is cancelled because the supervisor was cancelled" )
}
}
// 等待直到第一个子作业失败且执行完成
firstChild . join ()
println ( "Cancelling the supervisor" )
supervisor . cancel ()
secondChild . join ()
}
//sampleEnd
}
这段代码的输出如下:
The first child is failing The first child is cancelled: true, but the second one is still active Cancelling the supervisor The second child is cancelled because the supervisor was cancelled
监督作用域 对于作用域的并发,可以用 supervisorScope 来替代 coroutineScope 来实现相同的目的。它只会单向的传播并且当作业自身执行失败的时候将所有子作业全部取消。作业自身也会在所有的子作业执行结束前等待, 就像 coroutineScope 所做的那样。
import kotlin.coroutines.*
import kotlinx.coroutines.*
fun main () = runBlocking {
//sampleStart
try {
supervisorScope {
val child = launch {
try {
println ( "The child is sleeping" )
delay ( Long . MAX_VALUE )
} finally {
println ( "The child is cancelled" )
}
}
// 使用 yield 来给我们的子作业一个机会来执行打印
yield ()
println ( "Throwing an exception from the scope" )
throw AssertionError ()
}
} catch ( e : AssertionError ) {
println ( "Caught an assertion error" )
}
//sampleEnd
}
这段代码的输出如下:
The child is sleeping Throwing an exception from the scope The child is cancelled Caught an assertion error
监督协程中的异常 常规的作业和监督作业之间的另一个重要区别是异常处理。 监督协程中的每一个子作业应该通过异常处理机制处理自身的异常。 这种差异来自于子作业的执行失败不会传播给它的父作业的事实。 这意味着在 supervisorScope 内部直接启动的协程确实使用了设置在它们作用域内的 CoroutineExceptionHandler,与父协程的方式相同 (参见 CoroutineExceptionHandler 小节以获知更多细节)。
import kotlin.coroutines.*
import kotlinx.coroutines.*
fun main () = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _ , exception ->
println ( "CoroutineExceptionHandler got $exception" )
}
supervisorScope {
val child = launch ( handler ) {
println ( "The child throws an exception" )
throw AssertionError ()
}
println ( "The scope is completing" )
}
println ( "The scope is completed" )
//sampleEnd
}
这段代码的输出如下:
The scope is completing The child throws an exception CoroutineExceptionHandler got java.lang.AssertionError The scope is completed
来自扔物线朱凯大佬的博客学习笔记
JVM常量编译时优化 Kotlin中,使用了 const val 关键字修饰的变量,在编译时会被视为常量,并且在编译时进行了优化。直接将其值复制到调用处,而不是像普通变量一样在运行时进行变量访问。这可以提高代码的执行效率,因为避免了变量调用的开销。
const val CONST_VAL = 10
fun main () {
println ( CONST_VAL )
}
// 编译后
fun main () {
println ( 10 )
}
inline 内联函数 编译时同样被提前处理的还有内联函数,即使用了 inline 关键字修饰的函数。
JVM在编译时,会将inline函数内的代码直接复制到调用处,而不是像普通函数一样在运行时进行函数调用。听起来可能会对性能有优化,实际上少一层函数调用栈的优化是非常微小的。
而同时, 函数内联 不同于 常量内联 的地方在于,函数体通常比常量复杂多了,而函数内联会导致函数体被拷贝到每个调用处,如果函数体比较大而被调用处又比较多,就会导致编译出的字节码变大很多。
lambda参数实现方式 在Kotlin中,lambda参数的实现方式是使用了 匿名内部类 ,而不是使用了 函数指针 。
在编译之后,可以看到lambda参数调用的地方,实际上是Kotlin帮我们生成了一个匿名内部类,然后在调用处调用这个匿名内部类的方法。
class LambdaTest {
fun testInline ( lambdaParams :()-> Unit ) {
lambdaParams ()
}
}
经过反编译成Java代码之后:
public final class LambdaTest {
@NotNull
public final LambdaTest testInline ( @NotNull Function0 lambdaParams ) {
Intrinsics . checkNotNullParameter ( lambdaParams , "lambdaParams" );
lambdaParams . invoke ();
return this ;
}
}
可以看到,lambdaParams的类型是 Function0 ,这是一个接口。在运行过程中,就会生成一个匿名内部类,然后在调用处调用这个匿名内部类的方法。
inline对lambda的优化 如果上述的testinline方法,在外部被高频循环调用。
fun main () {
val lambdaTest = LambdaTest ()
for ( i in 0 .. 100000 ) {
lambdaTest . testInline {
println ( "hello world" )
}
}
}
内存占用会蹭的一下涨上来。
如果使用了这个接收lambda参数的方法使用了 inline 关键字修饰,就不会生成匿名内部类,而是直接将lambda的代码块里面的代码复制到调用处。
inline 关键字不止可以内联自己的内部代码,还可以内联自己内部的内部的代码,意思是什么呢,就是你的函数在被加了 inline 关键字之后,编译器在编译时不仅会把函数内联过来,而且会把它内部的函数类型的参数——那就是那些 Lambda 表达式——也内联过来。换句话说,这个函数被编译器贴过来的时候是完全展开铺平的:
kotlin源代码:
class LambdaTest {
inline fun testInline ( lambdaParams :()-> Unit ) {
lambdaParams ()
}
}
fun main () {
val lambdaTest = LambdaTest ()
for ( i in 0 .. 100000 ) {
lambdaTest . testInline {
println ( "hello world" )
}
}
}
反编译之后:
public final class LambdaTest {
public final void testInline ( @NotNull Function0 lambdaParams ) {
Intrinsics . checkNotNullParameter ( lambdaParams , "lambdaParams" );
lambdaParams . invoke ();
}
}
public final class MainKt {
public static final void main () {
LambdaTest lambdaTest = new LambdaTest ();
int $i$iv = 0 ;
int var3 ;
for ( var3 = 100000 ; $i$iv <= var3 ; ++ $i$iv ) {
System . out . println ( "hello world" );
}
}
}
高阶函数(Higher-order Functions)有它们天然的性能缺陷,我们通过 inline 关键字让函数用内联的方式进行编译,来减少参数对象的创建,从而避免出现性能问题。
inline另类用法 在kotlin的 UMath.kt 工具类中,有一个max方法:
@SinceKotlin ( "1.5" )
@WasExperimental ( ExperimentalUnsignedTypes :: class )
@ kotlin . internal . InlineOnly
public inline fun max ( a : UInt , b : UInt ): UInt {
return maxOf ( a , b )
}
这个maxOf方法,来自于另一个工具类 UComparisonsKt :
@SinceKotlin ( "1.5" )
@WasExperimental ( ExperimentalUnsignedTypes :: class )
public fun maxOf ( a : UInt , b : UInt ): UInt {
return if ( a >= b ) a else b
}
这里就通过内联的方式,将maxOf方法的代码块内联到了调用处。
可以直接通过方便的顶层函数的方式,来使用工具类,不需要创建实例或者带外部类名。
noinline inline 是内联,而 noinline 就是不内联。不过它不是作用于函数的,而是作用于函数的参数:对于一个标记了 inline 的内联函数,你可以对它的任何一个或多个函数类型的参数添加 noinline 关键字。添加了之后,这个参数就不会参与内联。
函数类型的参数,它本质上是个对象。我们可以把这个对象当做函数来调用,这也是最常见的用法。但同时我们也可以把它当做对象来用。比如把它当做返回值:
inline fun testInline ( lambdaParams :()-> Unit ) {
lambdaParams ()
return lambdaParams
}
但当我们把函数进行内联的时候,它内部的这些参数就不再是对象了,因为他们会被编译器拿到调用处去展开。
当一个函数被内联之后,它内部的那些函数类型的参数就不再是对象了,因为它们的壳被脱掉了。换句话说,对于编译之后的字节码来说,这个对象根本就不存在。一个不存在的对象,你怎么使用?
所以当你要把一个这样的参数当做对象使用的时候,Android Studio 会报错,告诉你这没法编译
noinline 就是用来局部地、指向性地关掉函数的内联优化的。既然是优化,为什么要关掉?因为这种优化会导致函数中的函数类型的参数无法被当做对象使用,也就是说,这种优化会对 Kotlin 的功能做出一定程度的收窄。而当你需要这个功能的时候,就要手动关闭优化了。这也是 inline 默认是关闭、需要手动开启的另一个原因:它会收窄 Kotlin 的功能。
crossinline 当 inline 函数将 Lambda 参数传递给另一个执行上下文 (如另一个函数、另一个线程、协程或其他作用域)时,为了防止非局部返回 ,必须使用 crossinline。
保持 Lambda 的内联优化,但禁止 在 Lambda 内部使用裸奔的 return 关键字(即非局部返回)。它确保 Lambda 只能使用标签返回 (return@label) 或隐式返回 。使用 crossinline 确保内联函数的行为符合预期,避免 Lambda 内部的 return 意外地跳出外部的非内联函数。
看这样一个情景:
一个内联函数,接受一个 lambda 参数。
inline fun lambdaReturnTest ( insertAction : () -> Unit ) {
insertAction ()
}
如果在调用处,lambda参数里带一个return:
override fun onCreate () {
super . onCreate ()
Log . i ( "sdvgsrhbTAG" , "before erftgyujhf" )
lambdaReturnTest {
println ( "Hello World" )
return
}
Log . i ( "sdvgsrhbTAG" , "after erftgyujhf" )
}
这时候结束的不是这个lambdaReturnTest方法,而是onCreate方法。因为lambdaReturnTest方法被内联了,会直接铺平展开到调用处,连带里面的return。
这样的话,我们每次在lambda里面使用return还需要确认这个函数是否是内联函数,才可以确认这个return结束的是哪一个函数。为此Kotlin规定 不允许在lambda参数中使用return,除非这个使用lambda参数的函数是内联函数 。
那这样的话规则就简单了:
Lambda 里的 return,结束的不是直接的外层函数,而是外层再外层的函数; 但只有内联函数的 Lambda 参数可以使用 return。 目前的Kotlin版本其实也可以在return后面使用\@来指明返回的哪一级的函数。
示例:异步或嵌套执行
假设您有一个 safeRun 函数,它在一个内部(非内联)的 Runnable 中执行您的 Lambda。
// 内部非内联函数,它接受一个普通 Lambda/Runnable
fun executeInExecutor ( block : () -> Unit ) {
// 实际的 Android/Java 场景可能是:Executor.execute(Runnable { ... })
println ( "任务被包装并排队..." )
block () // 模拟执行
}
// 场景:创建一个安全的执行块,但其中的任务会被传递到另一个函数中执行
inline fun safeRun ( crossinline block : () -> Unit ) {
println ( "--- 准备执行 ---" )
// 如果这里没有 crossinline,编译器无法保证 block() 不会被非局部返回跳出 safeRun 之外
executeInExecutor {
// block 的代码在这里被执行
block ()
}
println ( "--- 执行完毕 ---" )
}
fun main () {
fun callSafeRun () {
safeRun {
println ( "开始任务" )
// return // ❌ 编译错误:禁止非局部返回
return @ safeRun // ✅ 允许:只能使用标签返回,只跳出 safeRun
}
println ( "callSafeRun 结束" )
}
callSafeRun ()
}
/* 输出:
--- 准备执行 ---
任务被包装并排队...
开始任务
--- 执行完毕 ---
callSafeRun 结束
*/
如果没有 crossinline,Lambda { return } 理论上可以执行非局部返回,直接跳出 callSafeRun 函数。但由于 Lambda 实际是在非内联的 executeInExecutor 内部执行的,这种行为是不允许的,因此 crossinline 强制阻止了非局部返回,以保证程序的控制流是清晰且安全的。
双层嵌套的lambda场景 inline fun lambdaReturnTest ( insertAction : () -> Unit ) {
doubleLambda { insertAction () }
}
fun doubleLambda ( insertAction : () -> Unit ) {
insertAction ()
}
doubleLambda方法是一个普通函数,非内联函数,它的参数是一个函数类型的参数。
如果像这样带两层lambda调用,那么其中使用return就又会无法判断结束的到底是哪一层函数。 这里Kotlin是直接禁止了这种写法。
如果确实要有这种间接调用需求,那么可以使用crossinline来解决。当你给一个需要被间接调用的参数加上 crossinline,就对它进行了局部加强内联,相当于insertAction还是会被展开铺平到调用处,解除了这个限制,从而就可以对它进行双层间接调用了。
但是又会有return结束层级不确定性,所以Kotlin规定了使用了crossinline的函数,不能在lambda参数中使用return。
只能二选一了。
总结 结论就是:
inline 可以让你用内联——也就是函数内容直插到调用处——的方式来优化代码结构,从而减少函数类型的对象的创建; noinline 是局部关掉这个优化,来摆脱 inline 带来的「不能把函数类型的参数当对象使用」的限制; crossinline 是局部加强这个优化,让内联函数里的函数类型的参数可以被当做对象使用。 文章后半部分源码和介绍来自Kotlin官方网站
协程简介 协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码。协程 是在 1.3 版中添加到 Kotlin 的,基于既定的从其他语言转换成的概念。
在 Android 上,协程有助于管理长时间运行的任务,如果管理不当,这些任务可能会阻塞主线程并导致应用无响应。使用协程的专业开发者中有超过 50% 的人反映使用协程提高了工作效率。本主题介绍如何使用 Kotlin 协程解决以下问题,从而让您能够编写出更清晰、更简洁的应用代码。
协程和线程 线程 线程是操作系统级别的概念 我们开发者通过编程语言(Thread.java)创建的线程,本质还是操作系统内核线程的映射 JVM 中的线程与内核线程的存在映射关系,有“一对一”,“一对多”,“M对N”。* JVM 在不同操作系统中的具体实现会有差别,“一对一”是主流 一般情况下,我们说的线程,都是内核线程,线程之间的切换,调度,都由操作系统负责 线程也会消耗操作系统资源,但比进程轻量得多 线程,是抢占式的,它们之间能共享内存资源,进程不行 线程共享资源导致了多线程同步问题 有的编程语言会自己实现一套线程库,从而能在一个内核线程中实现多线程效果,早期 JVM 的“绿色线程” 就是这么做的,这种线程被称为“用户线程” 协程 协程不是操作系统级别的概念,无需操作系统支持 协程有点像上面提到的“绿色线程”,一个线程上可以运行成千上万个协程 协程是用户态的(userlevel),内核对协程无感知 协程是协作式的,由开发者管理,不需要操作系统进行调度和切换,也没有抢占式的消耗,因此它更加高效 协程它底层基于状态机实现,多协程之间共用一个实例,资源开销极小,因此它更加轻量 协程本质还是运行于线程之上,它通过协程调度器,可以运行到不同的线程上 项目使用实例 最常见的使用方式,在 ViewModel 或者 Controller 里写业务逻辑,在 Activity 里调用,这样就可以在IO线程执行网络请求,拿到结果后自动切换到主线程更新UI。
// viewModel或者controller里获取数据逻辑
// 使用suspend限制在协程里使用;withContext切换调度器,指定在IO线程执行下面的任务
suspend fun getUserName () = withContext ( Dispatchers . IO ) {
debugLog ( "thread name: ${Thread.currentThread().name}" )
ServiceCreator . createService < UserService >()
. getUserName ( "2cd1e3c5ee3cda5a" )
. execute ()
. body ()
}
// Activity调用处
override fun onCreate ( savedInstanceState : Bundle ?){
// 最直接的声明方法,在主线程执行下面的逻辑
lifeCycleScope . launch {
// 相当于get这一半是在IO线程执行
//拿到结果后的变量赋值这一半操作由调度器自动切换到主线程来执行了
val userName = mViewModel . getUserName ()
infoLog ( "userName: $userName" )
binding . tvUserName . text = userName
}
}
API介绍 四个基础概念 suspend function。即挂起函数,delay() 就是协程库提供的一个用于实现非阻塞式延时的挂起函数 CoroutineScope。即协程作用域,GlobalScope 是 CoroutineScope 的一个实现类,用于指定协程的作用范围,可用于管理多个协程的生命周期,所有协程都需要通过 CoroutineScope 来启动 CoroutineContext。即协程上下文,包含多种类型的配置参数。Dispatchers.IO 就是 CoroutineContext 这个抽象概念的一种实现,用于指定协程的运行载体,即用于指定协程要运行在哪类线程上 CoroutineBuilder。即协程构建器,协程在 CoroutineScope 的上下文中通过 launch、async 等协程构建器来进行声明并启动。launch、async 均被声明为 CoroutineScope 的扩展方法 Kotlin 协程(Coroutines)提供了一套丰富的 API 方法,用于简化异步编程。以下是一些常用的 API 方法及其简要说明:
启动 launch方法签名:
public fun CoroutineScope . launch (
context : CoroutineContext = EmptyCoroutineContext ,
start : CoroutineStart = CoroutineStart . DEFAULT ,
block : suspend CoroutineScope .() -> Unit
): Job {
val newContext = newCoroutineContext ( context )
val coroutine = if ( start . isLazy ){
LazyStandaloneCoroutine ( newContext , block )
} else {
StandaloneCoroutine ( newContext , active = true )
}
coroutine . start ( start , coroutine , block )
return coroutine
}
start参数代表启动方式:
CoroutineStart.DEFAULT:协程创建后,立即开始调度,但 有可能在执行前被取消。在调度前如果协程被取消,其将直接进入取消响应的状态。 CoroutineStart.LAZY:只要协程被需要时(主动调用该协程的 start、 join、 await等函数时 ), 才会开始调度,如果调度前就被取消,协程将直接进入异常结束状态。 CoroutineStart.ATOMIC:协程创建后,立即开始调度, 协程执行到第一个挂起点之前不响应取消。其将调度和执行两个步骤合二为一,就像它的名字一样,其保证调度和执行是原子操作,因此协程也 一定会执行。 CoroutineStart.UNDISPATCHED:协程创建后,立即在当前线程中执行,直到遇到第一个真正挂起的点。是立即执行,因此协程 一定会执行。
context上下文参数:
Job:工作空间。用于启动or取消协程。
Dispatchers为调度器。用于指定协程的执行线程。 Default:默认调度器 ,适合处理后台计算,其是一个 CPU 密集型任务调度器。 IO:IO 调度器,适合执行 IO 相关操作,其是 IO 密集型任务调度器。 Main:UI 调度器,根据平台不同会被初始化为对应的 UI 线程的调度器, 在Android 平台上它会将协程调度到 UI 事件循环中执行,即通常在 主线程上执行。 Unconfined:“无所谓”调度器,不要求协程执行在特定线程上。 CoroutineExceptionHandler:全局异常捕获(只能在根协程配置)。
CoroutineName:协程名称。
协程上下文就是CoroutineContext,其中可以用加和函数plus()来连接使用,比如:
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job + handler
这里的+就是加和函数,如上所写就是让CoroutineContext具备主线程+工作空间job,和CoroutineExceptionHandler的能力。
作用域 顶级作用域:GlobalScope–> 全局范围,不会自动结束执行,无法取消。 协同作用域:coroutineScope –> 抛出异常会取消父协程 主从作用域:supervisorScope –> 抛出异常,不会取消父协程 三种作用域真正常用的其实只有主从作用域,谁也不想让自己写的协程挂了导致app崩溃吧。但实际使用过程中,由于没有作用域的概念,往往会用到顶级作用域和协同作用域,协程挂了导致app崩溃,然后再去解决异常。
常用的主从作用域有下面这些:
MainScope :主线程的作用域,全局范围,可以取消。lifecycleScope : 生命周期范围,用于activity等有生命周期的组件,在Desroyed的时候会自动结束。viewModelScope :ViewModel范围,用于ViewModel中,在ViewModel被回收时会自动结束。主从作用域启动的协程,崩溃后不会影响其他协程执行。
以MainScope为例,在构建上下文时,加入了SupervisorJob(),SupervisorJob()是一个工作空间,它会在子协程抛出异常时,会将异常控制在子协程内部,不往上传递,不会影响父协程的执行。
线程切换 还是以launch方法签名为入口:
public fun CoroutineScope . launch (
context : CoroutineContext = EmptyCoroutineContext ,
start : CoroutineStart = CoroutineStart . DEFAULT ,
block : suspend CoroutineScope .() -> Unit
): Job {
val newContext = newCoroutineContext ( context )
val coroutine = if ( start . isLazy ){
LazyStandaloneCoroutine ( newContext , block )
} else {
StandaloneCoroutine ( newContext , active = true )
}
coroutine . start ( start , coroutine , block )
return coroutine
}
追进start方法:
/**
* Starts this coroutine with the given code [block] and [start] strategy.
* This function shall be invoked at most once on this coroutine.
*
* - [DEFAULT] uses [startCoroutineCancellable].
* - [ATOMIC] uses [startCoroutine].
* - [UNDISPATCHED] uses [startCoroutineUndispatched].
* - [LAZY] does nothing.
*/
public fun < R > start ( start : CoroutineStart , receiver : R , block : suspend R .() -> T ) {
start ( block , receiver , this )
}
可以看到模式启动模式下,使用的是 startCoroutineCancellable ,最终会调用到 resumeCancellableWith 方法,在 resumeCancellableWith 方法中,会判断当前上下文是否需要重新分发,如果需要就将上下文中提取新的Dispathers赋给dispatcher,否则就在当前线程直接执行。
inline fun resumeCancellableWith (
result : Result < T >,
noinline onCancellation : (( cause : Throwable ) -> Unit )?
) {
val state = result . toState ( onCancellation )
// 判断当前上下文是否需要重新分发,如果需要就将上下文中提取新的Dispathers赋给dispatcher,否则就在当前线程直接执行
if ( dispatcher . isDispatchNeeded ( context )) {
_state = state
resumeMode = MODE_CANCELLABLE
dispatcher . dispatch ( context , this )
} else {
executeUnconfined ( state , MODE_CANCELLABLE ) {
if (! resumeCancelled ( state )) {
resumeUndispatchedWith ( result )
}
}
}
}
在不同的JVM平台上,Dispatcher.Main 调度器的执行位置取决于 具体的UI框架 。以下是主要情况:
Android平台上会调度到Android的主线程(UI线程)执行,这是通过Handler(Looper.getMainLooper())实现的 JavaFX平台会调度到JavaFX的Application线程执行,这是通过Platform.runLater()实现的 Swing平台会调度到Swing的Event Dispatch Thread (EDT)执行,这是通过SwingUtilities.invokeLater()实现的 其他情况则会回退到单线程执行器 除了主调度器之外,其他几个切换也类似此流程,比如Dispatchers.Default是 创建了一个默认的线程池 ,而Dispatchers.IO也是沿用的线程池,只是对线程数量做了限制罢了。
IOS平台 在iOS平台上,Kotlin协程的线程切换主要通过以下方式实现:
Main Dispatcher(主线程调度器): 使用DispatchQueue.main来调度到主线程执行 这是通过Kotlin/Native与iOS的GCD(Grand Central Dispatch)集成实现的 协程会被调度到主队列(Main Queue)执行,确保UI操作在主线程进行 Default Dispatcher(默认调度器): 使用后台线程池执行任务 在iOS上,这通常是通过GCD的全局队列(Global Queue)实现的 使用DispatchQueue.global()来获取后台队列 IO Dispatcher(IO调度器): 专门用于IO密集型操作 同样基于GCD实现,但使用不同的队列优先级 使用DispatchQueue.global(qos: .utility)或DispatchQueue.global(qos: .background)来执行IO操作 简化api表达:
// Main Dispatcher实现
internal class MainDispatcher : CoroutineDispatcher () {
override fun dispatch ( context : CoroutineContext , block : Runnable ) {
DispatchQueue . main . async {
block . run ()
}
}
}
// Default Dispatcher实现
internal class DefaultDispatcher : CoroutineDispatcher () {
override fun dispatch ( context : CoroutineContext , block : Runnable ) {
DispatchQueue . global (). async {
block . run ()
}
}
}
流程图如下:
常用api 协程构建器 用于启动协程的主要方法。
launch 启动一个不会返回结果的协程(Job 类型)。GlobalScope . launch {
// 协程代码
}
async 启动一个会返回结果的协程(Deferred 类型),结果可以通过 await() 获取。val deferred = GlobalScope . async {
// 协程代码
"Result"
}
val result = deferred . await ()
runBlocking 阻塞当前线程,直到协程执行完毕。通常用于测试或主函数中。协程上下文与调度器 用于控制协程的执行线程或上下文。
Dispatchers.Default 用于 CPU 密集型任务的默认线程池。launch ( Dispatchers . Default ) {
// 在后台线程执行
}
Dispatchers.IO 用于 IO 密集型任务的线程池。launch ( Dispatchers . IO ) {
// 执行 IO 操作
}
Dispatchers.Main 用于在主线程(如 Android 的 UI 线程)执行任务。launch ( Dispatchers . Main ) {
// 更新 UI
}
Dispatchers.Unconfined 不限制协程的执行线程,根据调用点决定。launch ( Dispatchers . Unconfined ) {
// 不限制线程
}
withContext 切换协程的上下文。withContext ( Dispatchers . IO ) {
// 在 IO 线程执行
}
协程作用域 用于管理协程的生命周期。
GlobalScope 全局作用域,协程的生命周期与应用程序一致。GlobalScope . launch {
// 全局协程
}
CoroutineScope 自定义作用域,通常与 lifecycleScope 或 viewModelScope 结合使用。val scope = CoroutineScope ( Dispatchers . Main )
scope . launch {
// 协程代码
}
lifecycleScope (Android) 与 Lifecycle 绑定的作用域,协程在 Lifecycle 销毁时自动取消。lifecycleScope . launch {
// 协程代码
}
viewModelScope (Android) 与 ViewModel 绑定的作用域,协程在 ViewModel 销毁时自动取消。viewModelScope . launch {
// 协程代码
}
协程取消与超时 用于控制协程的执行时间或取消协程。
cancel() 取消协程。val job = launch {
// 协程代码
}
job . cancel ()
isActive 检查协程是否仍处于活动状态。if ( isActive ) {
// 协程仍在运行
}
withTimeout 设置协程的超时时间,超时后抛出 TimeoutCancellationException。withTimeout ( 1000 ) {
// 协程代码
}
withTimeoutOrNull 设置协程的超时时间,超时后返回 null 而不是抛出异常。val result = withTimeoutOrNull ( 1000 ) {
// 协程代码
}
协程挂起函数 用于在协程中挂起执行。
delay 挂起协程一段时间。yield 挂起当前协程,让出执行权给其他协程。协程异常处理 用于处理协程中的异常。
try-catch 捕获协程中的异常。try {
// 协程代码
} catch ( e : Exception ) {
// 处理异常
}
CoroutineExceptionHandler 全局异常处理器。val handler = CoroutineExceptionHandler { _ , exception ->
// 处理异常
}
launch ( handler ) {
// 协程代码
}
协程组合与并发 用于处理多个协程的组合与并发。
awaitAll 等待多个 Deferred 完成并返回结果列表。val deferred1 = async { 1 }
val deferred2 = async { 2 }
val results = awaitAll ( deferred1 , deferred2 )
supervisorScope 创建一个子作用域,子协程的失败不会影响其他子协程。supervisorScope {
launch {
// 子协程 1
}
launch {
// 子协程 2
}
}
coroutineScope 创建一个子作用域,子协程的失败会传播到父协程。coroutineScope {
launch {
// 子协程 1
}
launch {
// 子协程 2
}
}
协程间的通信Channel 编写具有共享可变状态的代码非常困难且容易出错(例如在使用回调的解决方案中)。更简单的方法是通过通信而不是使用公共可变状态来共享信息。协程可以通过通道相互通信。
通道是允许数据在协程之间传递的通信原语。 一个协程可以向通道发送一些信息,而另一个协程可以从该通道接收该信息 :
使用方法 发送(生产)信息的协程通常称为生产者,接收(消费)信息的协程称为消费者。一个或多个协程可以向同一个通道发送信息,一个或多个协程也可以从该通道接收数据。
当多个协程从同一个通道接收信息时,每个元素仅由其中一个消费者处理一次。 一旦元素被处理,它将立即从通道中移除。
可以将通道视为元素集合,或者更准确地说,队列这种数据结构 ,其中元素被添加到一端并从另一端接收。但是,有一个重要的区别:与集合不同,即使在其同步版本中,通道也可以暂停 send()和receive()操作。当通道为空或满时会发生这种情况。如果通道大小有上限,则通道可能会满。
Channel由三个不同的接口表示:SendChannel、ReceiveChannel和Channel,其中后者扩展了前两个。您通常会创建一个通道并将其作为SendChannel实例提供给生产者,以便只有他们可以向该通道发送信息。
您将通道作为ReceiveChannel实例提供给消费者,以便只有他们可以从中接收信息。send和receive方法都声明为suspend:
interface SendChannel < in E > {
suspend fun send ( element : E )
fun close (): Boolean
}
interface ReceiveChannel < out E > {
suspend fun receive (): E
}
interface Channel < E > : SendChannel < E >, ReceiveChannel < E >
生产者可以关闭一个通道来表明没有更多的元素到来。
库中定义了几种类型的通道。它们的区别在于内部可以存储多少个元素以及是否send()可以暂停调用。对于所有通道类型,receive()调用的行为都类似:如果通道不为空,则接收一个元素;否则,调用将被暂停。
创建通道时,请指定其类型或缓冲区大小(如果需要缓冲):
val rendezvousChannel = Channel < String >()
val bufferedChannel = Channel < String >( 10 )
val conflatedChannel = Channel < String >( CONFLATED )
val unlimitedChannel = Channel < String >( UNLIMITED )
默认情况下,会创建一个“Rendezvous”通道。
在以下任务中,您将创建一个“Rendezvous”通道、两个生产者协程和一个消费者协程:
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.*
fun main () = runBlocking < Unit > {
val channel = Channel < String >()
launch {
channel . send ( "A1" )
channel . send ( "A2" )
log ( "A done" )
}
launch {
channel . send ( "B1" )
log ( "B done" )
}
launch {
repeat ( 3 ) {
val x = channel . receive ()
log ( x )
}
}
}
fun log ( message : Any ?) {
println ( "[${Thread.currentThread().name}] $message" )
}
Flow API Flow API 是 Kotlin 协程库中的一部分,主要用于处理数据流。
flow 创建一个冷流(Cold Flow)。val flow = flow {
emit ( 1 )
emit ( 2 )
}
collect 收集流中的数据。flow . collect { value ->
// 处理数据
}
map 对流中的数据进行转换。flow . map { value -> value * 2 }
filter 过滤流中的数据。flow . filter { value -> value > 1 }
flatMapConcat 将流中的每个值映射为一个新流,并按顺序连接。flow . flatMapConcat { value -> flowOf ( value , value * 2 ) }
zip 将两个流合并为一个流。val flow1 = flowOf ( 1 , 2 )
val flow2 = flowOf ( "A" , "B" )
flow1 . zip ( flow2 ) { a , b -> "$a$b" }
关于更多Flow的基础和进阶使用,此前也写过更详细的一篇文章。
Kotlin Flow全面总结
Kotlin的Flow这个异步工具,在项目中其实一直在使用,得空参考下郭神在CSDN上的三篇文章,再自行扩展,在使用层面的规则上,进行一个相对较为全面的总结。
Flow最常见的使用场景,就是在viewmodel里面使用StateFlow 热流,在Ui层进行collect。用法和此前的LiveData是一样的。
例如一个获取Github仓库列表的网络请求数据:
private val githubReposSate = MutableStateFlow(GithubReposState())
val githubReposListStateFlow = githubReposSate.asStateFlow()
在Composable可组合项里进行消费:
val githubReposState by viewModel . githubReposListStateFlow . collectAsState ()
Column (
modifier = Modifier
. fillMaxSize ()
. background ( MaterialTheme . colors . background )
) {
LazyColumn (
modifier = Modifier
. fillMaxSize ()
. background ( MaterialTheme . colors . background )
) {
items ( githubReposState . githubReposList ) {
GithubRepoItem ( it )
}
}
}
冷流 使用flow构造器直接创建的为冷流,只有在调用collect函数时才会开始执行往外发送数据。
val TAG = "FlowOne" . apply {
Log . i ( this , "init" )
}
val flow = flow < Int > {
repeat ( 5 ) {
delay ( 500 )
emit ( it )
}
}
fun startCollect () {
CoroutineScope ( Dispatchers . IO ). launch {
delay ( 3000L )
Log . i ( TAG , "startCollect" )
flow . collect {
Log . i ( TAG , "FlowOne collect $it" )
}
}
}
外部测试调用:
打印可以看到,flow创建好之后,没有数据打印,而是在三秒后collect时,才会从头开始进行发送:
16:24:21.803 I init
16:24:24.825 I startCollect
16:24:25.328 I FlowOne collect 0
16:24:25.830 I FlowOne collect 1
16:24:26.332 I FlowOne collect 2
16:24:26.833 I FlowOne collect 3
16:24:27.334 I FlowOne collect 4
再次collect 如果我们在startCollect函数里再次调用collect。
fun startCollect () {
CoroutineScope ( Dispatchers . IO ). launch {
delay ( 3000L )
Log . i ( TAG , "startCollect" )
flow . collect {
Log . i ( TAG , "FlowOne collect $it" )
}
flow . collect {
Log . i ( TAG , "FlowOne collect twice $it" )
}
}
}
第一次收集完毕,延时6s,再次调用collect。打印结果如下:
19:34:57.752 I init
19:35:00.758 I startCollect
19:35:01.260 I FlowOne collect 0
19:35:01.761 I FlowOne collect 1
19:35:02.262 I FlowOne collect 2
19:35:02.765 I FlowOne collect 3
19:35:03.266 I FlowOne collect 4
19:35:03.771 I FlowOne collect twice 0
19:35:04.276 I FlowOne collect twice 1
19:35:04.779 I FlowOne collect twice 2
19:35:05.280 I FlowOne collect twice 3
19:35:05.782 I FlowOne collect twice 4
有两个值得关注的点,第一点是第二次 collect 打印开始的时间并不是紧跟着第一次,而是第一次收集所有数据完毕之后才开始。说明 collect() 函数是一个挂起的函数,只有在数据收集完毕之后,协程后面的函数恢复,才会继续往下执行。
简单来说,连续的两个 collect 操作是串行的,如果想要并行收集,就需要切换到不同的协程作用域。
第二个点是,第二次收集的数据也是从头开始打印的,说明冷流的每一次操作,都会从头开始。
collectLatest 有时候, collect 数据的地方,数据的消费逻辑没有走完,导致数据积压,会出现数据过时的情况,使用 collectLatest 可以解决这个问题。
collectLatest 函数,会在每次有新数据过来时,取消上一次还未执行完的逻辑,立即处理最新的这个数据。
例如,我们在collect函数里延时3s:
fun startCollect () {
CoroutineScope ( Dispatchers . IO ). launch {
delay ( 3000L )
Log . i ( TAG , "startCollect" )
flow . collectLatest {
Log . i ( TAG , "FlowOne collect $it" )
delay ( 3000L )
}
}
}
打印结果:
16:39:09.467 I init
16:39:12.494 I startCollect
16:39:12.997 I FlowOne collect 0
16:39:13.502 I FlowOne collect 1
16:39:14.007 I FlowOne collect 2
16:39:14.510 I FlowOne collect 3
16:39:15.013 I FlowOne collect 4
可以看到数据仍然是按照源头的时间间隔来发送的,并不是延时3s才打印。说明 collectLatest() 函数收集的流,会在每次有新数据过来时,取消上一次还未执行完的逻辑,立即处理最新的这个数据。
Flow常用操作符 map map 可以理解为一个拦截转换器,将 flow 的原数据,经过拦截器处理之后,转换成另一个数据发送出去。map接受一个lambda参数,lambda函数最后一行的数据,就是经过map转换后的返回值。
这里以平方转换为例:
fun mapTest () {
CoroutineScope ( Dispatchers . IO ). launch {
flowOf ( 1 , 2 , 3 , 4 , 5 ). map {
it * it
}. collectLatest {
Log . i ( TAG , "mapTest collect $it" )
}
}
}
打印结果:
16:44:00.302 I init
16:44:00.345 I mapTest collect 1
16:44:00.353 I mapTest collect 4
16:44:00.356 I mapTest collect 9
16:44:00.357 I mapTest collect 16
16:44:00.365 I mapTest collect 25
filter filter 函数,用于过滤数据,只将满足条件的数据发送出去。filter() 同样接受一个lambda参数,最后一行的需要返回一个Boolean类型,用于判断是否发送数据。
fun filterTest () {
CoroutineScope ( Dispatchers . IO ). launch {
flowOf ( 3 , 6 , 9 , 11 , 14 ). filter {
it % 3 == 0
}. collectLatest {
Log . i ( TAG , "filterTest collect $it" )
}
}
}
打印可以发现,只有369,即三的倍数通过了过滤器被接收。
onEach onEach函数,用于在每次数据发送之前,执行一些操作。可以打印查看原始的数据是否符合预期。
fun onEachTest () {
CoroutineScope ( Dispatchers . IO ). launch {
flowOf ( 1 , 2 , 3 , 4 , 5 ). onEach {
Log . i ( TAG , "onEachTest onEach $it" )
}. map { it + 10 }. collect {
Log . i ( TAG , "onEachTest collect $it" )
}
}
}
打印结果:
16:51:51.942 I init
16:51:51.982 I onEachTest onEach 1
16:51:51.984 I onEachTest collect 11
16:51:51.984 I onEachTest onEach 2
16:51:51.984 I onEachTest collect 12
16:51:51.985 I onEachTest onEach 3
16:51:51.986 I onEachTest collect 13
16:51:51.987 I onEachTest onEach 4
16:51:51.987 I onEachTest collect 14
16:51:51.988 I onEachTest onEach 5
16:51:51.993 I onEachTest collect 15
debounce debounce 函数,用于在一段时间内,只发送最后一次数据。两次数据的时间间隔太近,前一次的数据就会被丢弃,后一次数据,在延时这段时间后发送。类似Handler的remove和postDelayed的防抖操作。
@OptIn ( FlowPreview :: class )
fun debounceTest () {
CoroutineScope ( Dispatchers . IO ). launch {
flow {
emit ( 1 )
delay ( 100 )
emit ( 2 )
delay ( 1000 )
emit ( 3 )
delay ( 100 )
emit ( 4 )
delay ( 100 )
emit ( 5 )
}. debounce ( 500 ). collectLatest {
Log . i ( TAG , "debouneTest collect $it" )
}
}
}
打印结果:
16:58:41.402 I init
16:58:42.062 I debouneTest collect 2
16:58:42.767 I debouneTest collect 5
流程:
collect开始后,数据1立即发送,开始为期500ms的监测,100ms后数据2发送了,这时候数据1就被丢弃,再开始500ms监测。500ms后没有新数据来,将2发送出去。可以看到从init到第一次数据打印,就是耗时600ms。同理,2和5之间,间隔700ms。
sample sample 函数,作用类似debounce。sample有一个采样期,采样期结束,会将采样期内最后一次数据发送出去。
@OptIn ( FlowPreview :: class )
fun sampleTest () {
CoroutineScope ( Dispatchers . IO ). launch {
flow {
emit ( 1 )
delay ( 150 )
emit ( 2 )
delay ( 150 )
emit ( 3 )
delay ( 150 )
emit ( 4 )
delay ( 150 )
emit ( 5 )
}. sample ( 200 ). collect {
Log . i ( TAG , "debouneTest collect $it" )
}
}
}
每150ms发送一次数据,采样期为200ms,所以每200ms,会将采样期内最后一次数据发送出去。打印结果:
17:13:34.656 I init
17:13:34.891 I debouneTest collect 2
17:13:35.092 I debouneTest collect 3
17:13:35.295 I debouneTest collect 4
取值的过程如下图:
reduce reduce 函数,用于迭代操作,在上一次计算结果的基础上,拿当前的值再进行下一步计算。
例如,将1到5的数字相乘:
fun reduceTest () {
CoroutineScope ( Dispatchers . IO ). launch {
val totalResult =
flowOf ( 1 , 2 , 3 , 4 , 5 )
. reduce { acc , value ->
acc * value
}
Log . i ( TAG , "reduceTest collect $totalResult" )
}
}
结果打印为120.
fold fold 函数,和 reduce 基本一致,只是多了一个初始值。
fun foldTest () {
CoroutineScope ( Dispatchers . IO ). launch {
val totalResult =
flowOf ( 1 , 2 , 3 , 4 , 5 )
. fold ( 10 ) { acc , value ->
acc * value
}
Log . i ( TAG , "foldTest collect $totalResult" )
}
}
打印结果为1200.
reduce和fold不仅可以用于数字,还可以用于字符串的拼接。
flatMapConcat 以flatMap开头的操作符函数,分别是flatMapConcat、flatMapMerge和flatMapLatest。
flatMap的核心,就是将两个flow中的数据进行映射、合并、压平成一个flow,最后再进行输出。
flatMapConcat,是将两个flow中的数据进行合并,然后再进行输出。侧重点是按顺序拼接,类比C++里面两个数组的组合遍历。
@OptIn ( ExperimentalCoroutinesApi :: class )
fun flatMapConcatTest () {
CoroutineScope ( Dispatchers . IO ). launch {
flowOf ( 1 , 2 , 3 ). flatMapConcat {
flowOf ( "a$it" , "b$it" )
}. collect {
Log . i ( TAG , "flatMapConcatTest collect $it" )
}
}
}
打印结果:
17:40:40.835 I init
17:40:40.859 I flatMapConcatTest collect a1
17:40:40.859 I flatMapConcatTest collect b1
17:40:40.859 I flatMapConcatTest collect a2
17:40:40.860 I flatMapConcatTest collect b2
17:40:40.860 I flatMapConcatTest collect a3
17:40:40.861 I flatMapConcatTest collect b3
实际应用中,例如账号登陆获取用户数据的网络请求,需要先登录获取一个token,然后再拿这个token去获取用户数据。
fun getUserInfo () {
CoroutineScope ( Dispatchers . IO ). launch {
sendGetTokenRequest ()
. flatMapConcat { token ->
sendGetUserInfoRequest ( token )
}
. flowOn ( Dispatchers . IO )
. collect { userInfo ->
println ( userInfo )
}
}
}
flatMapMerge flatMapMerge,同样是将两个flow中的数据进行合并,然后再进行输出。但是他的侧重点是并行的,即flow1每个数据的操作不是串行的,而是并行的。
@OptIn ( ExperimentalCoroutinesApi :: class )
fun flatMapMergeTest () {
CoroutineScope ( Dispatchers . IO ). launch {
flowOf ( 300 , 200 , 100 )
. flatMapMerge {
flow {
delay ( it . toLong ())
emit ( "a$it" )
emit ( "b$it" )
}
}
. collect {
Log . i ( TAG , "flatMapMergeTest collect $it" )
}
}
}
打印结果:
17:55:37.695 I init
17:55:37.864 I flatMapMergeTest collect a100
17:55:37.865 I flatMapMergeTest collect b100
17:55:37.955 I flatMapMergeTest collect a200
17:55:37.956 I flatMapMergeTest collect b200
17:55:38.055 I flatMapMergeTest collect a300
17:55:38.058 I flatMapMergeTest collect b300
可以看到flatMapMerge处理之后,优先把耗时更少的数据添加到新的flow里面去进行发送。如果这里用的是flatMapConcat,那么结果就是按照300,200,100顺序发送。
flatMapLatest flatMapLatest,同样是将两个flow中的数据进行合并,然后再进行输出。但是他的侧重点是,只保留最新的一个数据。
@OptIn ( ExperimentalCoroutinesApi :: class )
fun flatMapLatestTest () {
CoroutineScope ( Dispatchers . IO ). launch {
flow {
emit ( 1 )
delay ( 150 )
emit ( 2 )
delay ( 50 )
emit ( 3 )
}. flatMapLatest {
flow {
delay ( 100 )
emit ( "$it" )
}
}. collect {
Log . i ( TAG , "flatMapLatestTest collect $it" )
}
}
}
和collectLatest类似,如果使用flatMapLatest来合并多个flow,当flow1的前一个数据给到了,但是flow2没有及时合并完成,flow1的下一个数据又过来了,那么前一个数据的处理逻辑就会被掐断丢弃,直接处理最新的这个数据。
打印结果:
17:59:19.282 I init 17:59:19.444 I flatMapLatestTest collect 1 17:59:19.657 I flatMapLatestTest collect 3
zip zip 函数和 flatMap 函数有点类似,都是作用在两个flow上的。
使用 zip 函数连接的两个flow,它们之间是并行的运行关系。而 flatMap 是一个flow中的数据流向另外一个flow,是串行的关系。
元素按照少的那个flow来决定 zip函数还有一个规则,就是 只要其中一个flow中的数据对应的数量,全部处理结束就会终止运行,剩余未处理的数据将不会得到处理。
@OptIn ( ExperimentalCoroutinesApi :: class )
fun zipTest () {
CoroutineScope ( Dispatchers . IO ). launch {
flowOf ( 1 , 2 , 3 , 4 , 5 )
. zip ( flowOf ( "a" , "b" , "c" , "d" )) { a , b ->
"$a+$b"
}
. collect {
Log . i ( TAG , "zipTest collect $it" )
}
}
}
第一个flow有5个元素,第二个flow有4个元素,按照zip函数的规则,最终只会处理4个元素,最后一个元素5不会被处理。
打印结果:
19:45:12.248 I init
19:45:12.261 I zipTest collect 1+a
19:45:12.262 I zipTest collect 2+b
19:45:12.263 I zipTest collect 3+c
19:45:12.264 I zipTest collect 4+d
运行时长按照长的那个flow来决定 下面例子中,flow1和flow2发送数据均有延时逻辑,zip是并行执行的,最终的运行时长,取决于运行时长更长的那个flow。
fun zipTest2 () {
CoroutineScope ( Dispatchers . IO ). launch {
val start = System . currentTimeMillis ()
val flow1 = flow {
delay ( 3000 )
emit ( "a" )
}
val flow2 = flow {
delay ( 2000 )
emit ( 1 )
}
flow1 . zip ( flow2 ) { a , b ->
a + b
}. collect {
val end = System . currentTimeMillis ()
Log . i ( TAG , "Time cost: ${end - start}ms" )
}
}
}
打印结果:
19:48:24.785 I init
19:48:27.801 I Time cost: 3012ms
zip的应用场景,好几个接口的请求返回耗时时长不一致,但是需要将数据一起返回给界面,就可以通过zip的特性,在耗时最长的flow执行完毕之后,再一同发送数据。
buffer 默认情况下,flow的数据发送和collect是在同一个协程上运行的,如果collect里面有耗时逻辑,也会对flow的数据发送造成影响。
在大多数情况,这都是最好规避掉的。
fun bufferTest () {
CoroutineScope ( Dispatchers . IO ). launch {
flow {
emit ( 1 )
delay ( 1000 )
emit ( 2 )
delay ( 1000 )
emit ( 3 )
}. onEach {
Log . i ( TAG , "bufferTest onEach $it" )
}. collect {
delay ( 1000 )
Log . i ( TAG , "bufferTest collect $it" )
}
}
}
collect 和 emit 都有1s的延时,互相挂起,串行执行,所以应该每个collect就变成了2s,打印结果如下:
19:55:52.000 I init
19:55:52.004 I bufferTest onEach 1
19:55:53.006 I bufferTest collect 1
19:55:54.011 I bufferTest onEach 2
19:55:55.013 I bufferTest collect 2
19:55:56.018 I bufferTest onEach 3
19:55:57.021 I bufferTest collect 3
这时候加一个 buffer 操作符,就可以让数据发送和collect并行执行。
加 buffer() 调用之后的结果打印:
20:00:03.426 I init
20:00:03.432 I bufferTest onEach 1
20:00:04.436 I bufferTest collect 1
20:00:04.437 I bufferTest onEach 2
20:00:05.439 I bufferTest collect 2
20:00:05.439 I bufferTest onEach 3
20:00:06.443 I bufferTest collect 3
buffer 操作符有一个可选的 capacity 参数,用于指定缓冲区的大小。如果不指定 capacity,则缓冲区的大小默认为 Channel.BUFFERED,这意味着缓冲区的大小是无限制的。 然而,需要注意的是,虽然缓冲区的大小可以是无限制的,但在实际应用中,过大的缓冲区可能会导致内存占用过高,从而影响应用的性能。因此,建议根据具体的应用场景和需求来合理设置缓冲区的大小。
conflate conflate 函数是对buffer函数的一个另选方案,它的作用是当收集器挂起之后,flow发射方把当前无法处理的数据丢弃掉,待收集器处理完逻辑,再给其发送新的值。可以解决buffer函数的问题,即当 collector 收集器过于耗时,又未指定容量,那缓存区的数据就越来越大。
与之有点相似的是 collectLatest 函数,上面展示了使用collectLatest函数,在数据发送的过程中,会取消上一次未运行完毕的收集逻辑,立即处理最新的数据。
而 conflate 函数,是在数据发送的过程中,如果本次 collect 仍然在运行,就把这个数据丢弃掉,等到 collector 收集器重新可接收数据之后,拿到的就是最新的数据。这样可以保证 collector 每次接收到数据之后,可以把当前的逻辑全部走完。
fun conflateTest () {
CoroutineScope ( Dispatchers . IO ). launch {
flow {
repeat ( 7 ){
delay ( 100 )
emit ( it )
}
}. conflate (). collect {
Log . i ( TAG , "conflateTest collect start handle $it" )
delay ( 210 )
Log . i ( TAG , "conflateTest collect end handle $it" )
}
}
}
打印结果:
20:17:58.356 I init
20:17:58.465 I conflateTest collect start handle 0
20:17:58.676 I conflateTest collect end handle 0
20:17:58.677 I conflateTest collect start handle 2
20:17:58.889 I conflateTest collect end handle 2
20:17:58.889 I conflateTest collect start handle 4
20:17:59.099 I conflateTest collect end handle 4
20:17:59.099 I conflateTest collect start handle 6
20:17:59.310 I conflateTest collect end handle 6
可以看到当collector挂起的时候发送的数据就丢弃掉了。
conflate和collectLatest共用的情况 conflate 函数和 collectLatest 函数,一个丢弃数据,一个丢弃逻辑,如果都用是什么效果呢?
实测是 collectLatest 函数的效果是优先的,收集器会掐断正在执行的逻辑,转而处理更新的数据。
fun conflateTest () {
CoroutineScope ( Dispatchers . IO ). launch {
flow {
repeat ( 7 ) {
delay ( 100 )
emit ( it )
}
}. conflate (). collectLatest {
Log . i ( TAG , "conflateTest collect start handle $it" )
delay ( 210 )
Log . i ( TAG , "conflateTest collect end handle $it" )
}
}
}
打印结果:
20:22:19.626 I init
20:22:19.736 I conflateTest collect start handle 0
20:22:19.838 I conflateTest collect start handle 1
20:22:19.938 I conflateTest collect start handle 2
20:22:20.039 I conflateTest collect start handle 3
20:22:20.140 I conflateTest collect start handle 4
20:22:20.242 I conflateTest collect start handle 5
20:22:20.342 I conflateTest collect start handle 6
20:22:20.553 I conflateTest collect end handle 6
StateFlow 和 SharedFlow 插入LiveData 在Java开发的时候,会使用LiveData来进行数据的传递。
LiveData是Android Jetpack的一部分,与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。
基本使用方式:
viewmodel里面维护一个私有的MutableLiveData数据,暴露一个公开的livedata变量,然后在activity中监听LiveData数据的变化。
class MainViewModel : ViewModel () {
private val _countLiveData = MutableLiveData < Int >( 0 )
val countLiveData : LiveData < Int >
get () = _countLiveData
suspend fun startCount () = withContext ( Dispatchers . IO ) {
while ( true ) {
delay ( 1000 )
withContext ( Dispatchers . Main ) {
_countLiveData . value = _countLiveData . value ?. plus ( 1 )
}
}
}
}
Activity中监听数据变化:
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity_main )
Log . i ( "MainActivity" , "onCreate" )
val tvCount = findViewById < TextView >( R . id . tv_count )
mainViewModel . countLiveData . observe ( this ) {
Log . i ( "MainActivity" , "observe data: $it" )
tvCount . text = it . toString ()
}
lifecycleScope . launch {
mainViewModel . startCount ()
}
}
运行之后count数据就开始每秒加 1 了,一段时间后上滑回到桌面,日志显示activity的 onStop() 方法被调用,activity的生命周期进入后台。这时候Observer就不会处理推送过来的数据。
再过一段时间后,重新打开应用界面,日志里看到 activity 的 onResume() 方法被调用,activity的生命周期进入前台。这时候 Observer 就会继续从最新的数据开始处理推送过来的数据。
打印结果:
10:58:10.265 I onCreate
10:58:10.414 I observe data: 0
10:58:10.417 I onResume
10:58:11.391 I observe data: 1
10:58:12.395 I observe data: 2
10:58:13.397 I observe data: 3
10:58:14.401 I observe data: 4
10:58:15.404 I observe data: 5
10:58:16.406 I observe data: 6
10:58:20.866 I onStop
10:58:26.357 I observe data: 15
10:58:26.357 I onResume
10:58:26.436 I observe data: 16
10:58:27.440 I observe data: 17
10:58:28.444 I observe data: 18
10:58:29.448 I observe data: 19
LiveData 遵循观察者模式。当生命周期状态发生变化时,LiveData 会通知 Observer 对象。当界面组件处于非活跃状态时,它不会接收任何 LiveData 事件。
下面是LiveData的observe的源码,展示了这种绑定注册关系:
@MainThread
public void observe ( @NonNull LifecycleOwner owner , @NonNull Observer <? super T > observer ) {
assertMainThread ( "observe" );
if ( owner . getLifecycle (). getCurrentState () == DESTROYED ) {
// ignore
return ;
}
LifecycleBoundObserver wrapper = new LifecycleBoundObserver ( owner , observer );
ObserverWrapper existing = mObservers . putIfAbsent ( observer , wrapper );
if ( existing != null && ! existing . isAttachedTo ( owner )) {
throw new IllegalArgumentException ( "Cannot add the same observer"
+ " with different lifecycles" );
}
if ( existing != null ) {
return ;
}
owner . getLifecycle (). addObserver ( wrapper );
}
Activity直接收集冷流flow 类比上面LiveData的写法,直接在Activity里收集冷流,试试看是什么效果。
class MainViewModel : ViewModel () {
val countnFlow = flow < Int > {
var count = 0
while ( true ) {
emit ( count ++)
delay ( 1000 )
}
}
}
Activity添加收集flow:
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity_main )
Log . i ( "MainActivity" , "onCreate" )
val tvCount = findViewById < TextView >( R . id . tv_count )
lifecycleScope . launch {
mainViewModel . countnFlow . collect {
Log . i ( "MainActivity" , "collect data: $it" )
tvCount . text = it . toString ()
}
}
}
打印结果:
10:47:56.742 I onCreate
10:47:56.865 I collect data: 0
10:47:56.900 I onResume
10:47:57.872 I collect data: 1
10:47:58.875 I collect data: 2
10:47:59.876 I collect data: 3
10:48:00.878 I collect data: 4
10:48:01.051 D visibilityChanged oldVisibility=true newVisibility=false
10:48:01.084 I onStop
10:48:01.880 I collect data: 5
10:48:02.882 I collect data: 6
10:48:03.886 I collect data: 7
10:48:04.889 I collect data: 8
10:48:05.891 I collect data: 9
10:48:06.661 I onResume
10:48:06.892 I collect data: 10
10:48:07.896 I collect data: 11
10:48:08.899 I collect data: 12
10:48:09.904 I collect data: 13
可以看到,在activity进入后台之后,数据依然在不断的发送,收集器也在不断的收集处理数据。因为这种方法并没有生命周期感知的特性。
使用repeatOnLifecycle 在协程里面,可以使用 repeatOnLifecycle 来让某些任务只在特定生命周期状态内才会执行。我们可以传入 Lifecycle.State.STARTED,表示只有activity在started状态下才运行。当再次处于started状态时,任务会重新开始执行。
使用 repeatOnLifecycle 需要导入 androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 包。
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity_main )
Log . i ( "MainActivity" , "onCreate" )
val tvCount = findViewById < TextView >( R . id . tv_count )
lifecycleScope . launch {
repeatOnLifecycle ( Lifecycle . State . STARTED ) {
mainViewModel . countnFlow . collect {
Log . i ( "MainActivity" , "collect data: $it" )
tvCount . text = it . toString ()
}
}
}
}
日志可以看到,onStop之后,数据的处理就停止了,在start之后,collect会重新调用,所以数据是从0开始的,日志打印结果:
11:08:40.925 I onCreate
11:08:41.068 I collect data: 0
11:08:41.072 I onResume
11:08:42.072 I collect data: 1
11:08:43.074 I collect data: 2
11:08:44.076 I collect data: 3
11:08:45.080 I collect data: 4
11:08:46.082 I collect data: 5
11:08:47.084 I collect data: 6
11:08:47.621 D visibilityChanged oldVisibility=true newVisibility=false
11:08:47.657 I onStop
11:09:03.861 I collect data: 0
11:09:03.862 I onResume
11:09:04.863 I collect data: 1
11:09:05.865 I collect data: 2
11:09:06.868 I collect data: 3
11:09:07.871 I collect data: 4
11:09:08.875 I collect data: 5
这样可以避免在activity处于后台的时候,数据的处理逻辑一直运行,导致资源浪费或者内存泄漏。
StateFlow 借助 repeatOnLifecycle,我们可以在activity处于started状态的时候,收集数据。使用StateFlow的效果可以说和LiveData几乎一致。
class MainViewModel : ViewModel () {
private val _stateFlow = MutableStateFlow ( 0 )
val countStateFlow = _stateFlow . asStateFlow ()
suspend fun startCount () = withContext ( Dispatchers . IO ) {
for ( i in 0 .. 100 ) {
delay ( 1000 )
_stateFlow . value = i
}
}
}
activity里收集,注意 startCount 和 collect 均为挂起函数,两个函数需要放在不同的作用域内调用:
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity_main )
Log . i ( "MainActivity" , "onCreate" )
val tvCount = findViewById < TextView >( R . id . tv_count )
lifecycleScope . launch {
launch {
mainViewModel . startCount ()
}
repeatOnLifecycle ( Lifecycle . State . STARTED ) {
mainViewModel . countStateFlow . collect {
Log . i ( "MainActivity" , "collect data: $it" )
tvCount . text = it . toString ()
}
}
}
}
结果打印,退到后台,数据的处理取消,回到前台后,重新collect,同时因为stateflow是热流,收集时会直接从最新的状态开始:
11:27:57.859 I onCreate
11:27:58.028 I collect data: 0
11:27:58.030 I onResume
11:28:00.004 I collect data: 1
11:28:01.005 I collect data: 2
11:28:02.007 I collect data: 3
11:28:03.009 I collect data: 4
11:28:04.012 I collect data: 5
11:28:05.014 I collect data: 6
11:28:05.165 D visibilityChanged oldVisibility=true newVisibility=false
11:28:05.210 I onStop
11:28:11.967 I collect data: 12
11:28:11.968 I onResume
11:28:12.045 I collect data: 13
11:28:13.027 I collect data: 14
11:28:14.029 I collect data: 15
11:28:15.031 I collect data: 16
config变化导致协程取消 当屏幕方向发生变化时,activity会销毁重新创建,这时候 lifecycle 协程会被取消,然后重新启动。计时器任务也会取消重新执行。
竖屏变横屏的日志打印:
13:39:19.215 I onCreate 13:39:19.426 I collect data: 0 13:39:19.429 I onResume 13:39:21.396 I collect data: 1 13:39:22.397 I collect data: 2 13:39:23.399 I collect data: 3 13:39:24.401 I collect data: 4 13:39:25.403 I collect data: 5 13:39:26.395 I onStop 13:39:26.404 I onDestroy 13:39:26.450 I onCreate 13:39:26.479 I collect data: 5 13:39:26.487 I onResume 13:39:27.460 I collect data: 0 13:39:28.461 I collect data: 1 13:39:29.463 I collect data: 2 13:39:30.466 I collect data: 3 13:39:31.468 I collect data: 4
借助stateIn将冷流变成StateFlow 上面的config变化导致协程取消的问题,可以借助 stateIn 函数将冷流变成热流。然后把计时的操作移植到冷流中。
class MainViewModel : ViewModel () {
private val timeFlow = flow {
var time = 0
while ( true ) {
emit ( time )
delay ( 1000 )
time ++
}
}
val countStateFlow =
timeFlow . stateIn (
viewModelScope ,
SharingStarted . WhileSubscribed ( 5000 ),
0
)
}
stateIn 扩展函数,有三个参数,第一个参数是协程作用域,第三个参数是初始值。
其第二个参数是共享的策略,因为横竖屏切换通常很快就能完成,这里我们通过stateIn函数的第2个参数指定了一个5秒的超时时长,那么只要在5秒钟内横竖屏切换完成了,Flow就不会停止工作。
反过来讲,这也使得程序切到后台之后,如果5秒钟之内再回到前台,那么Flow也不会停止工作。但是如果切到后台超过了5秒钟,Flow就会全部停止了。
竖屏变横屏的日志打印:
13:47:49.368 I onCreate
13:47:49.579 I collect data: 0
13:47:49.588 I onResume
13:47:50.589 I collect data: 1
13:47:51.592 I collect data: 2
13:47:52.594 I collect data: 3
13:47:53.597 I collect data: 4
13:47:54.600 I collect data: 5
13:47:55.385 I onStop
13:47:55.388 I onDestroy
13:47:55.471 I onCreate
13:47:55.477 I collect data: 5
13:47:55.485 I onResume
13:47:55.601 I collect data: 6
13:47:56.605 I collect data: 7
13:47:57.609 I collect data: 8
13:47:58.613 I collect data: 9
13:47:59.616 I collect data: 10
SharedFlow 粘性消息的概念 LiveData 的粘性,是指当一个新的观察者开始观察 LiveData 时,它会 立即接收到 LiveData 最后一次设置的值 ,即使这个值是在观察者开始观察之前设置的。这种行为被称为粘性,因为它就像观察者“粘”在了 LiveData 的最后一个值上。
粘性的实现原理是基于 LiveData 的版本号机制。每当 LiveData 的值发生变化时,它的版本号就会增加。当一个新的观察者开始观察 LiveData 时,它会检查当前的版本号,如果版本号大于 0,说明 LiveData 已经有了一个值,那么观察者会立即接收到这个值。
粘性的优点是可以确保新的观察者不会错过 LiveData 的任何重要状态变化,即使它们在状态变化之后才开始观察。这对于一些需要实时更新的场景非常有用,例如用户界面的状态管理。
然而,粘性也可能会导致一些问题,特别是在处理事件流时。如果 LiveData 被用作事件总线,粘性可能会导致新的观察者接收到旧的事件,这可能会导致应用程序的行为不符合预期。
通过之前的例子,发现stateflow也是粘性的,开始收集时,是从上一个最新的值开始的。
SharedFlow使用 SharedFlow 和 StateFlow 的用法还是略有不同的。
首先,MutableSharedFlow 是不需要传入初始值参数的。因为非粘性的特性,它本身就 不要求观察者在观察的那一刻就能收到消息 ,所以也没有传入初始值的必要。
另外就是,SharedFlow 无法像 StateFlow 那样通过给 value 变量赋值来发送消息,而是只能像传统 Flow 那样调用 emit 函数。而 emit 函数又是一个挂起函数,所以这里需要调用 viewModelScope 的 launch 函数启动一个协程,然后再发送消息。
class MainViewModel : ViewModel () {
private val _countSharedFlow = MutableSharedFlow < Int >()
val countSharedFlow = _countSharedFlow . asSharedFlow ()
init {
CoroutineScope ( Dispatchers . IO ). launch {
repeat ( 20 ) {
delay ( 1000 )
_countSharedFlow . emit ( it )
}
}
}
}
activity中收集代码仍然未改变:
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity_main )
Log . i ( "MainActivity" , "onCreate" )
val tvCount = findViewById < TextView >( R . id . tv_count )
lifecycleScope . launch {
repeatOnLifecycle ( Lifecycle . State . STARTED ) {
mainViewModel . countSharedFlow . collect {
Log . i ( "MainActivity" , "collect data: $it" )
tvCount . text = it . toString ()
}
}
}
}
SharedFlow 在运行之后的发送和收集是解耦的。这意味着发送者和接收者可以独立地进行操作,而不需要彼此之间的直接交互。
在 Kotlin 中, SharedFlow 是一个热流(hot flow),它可以在没有订阅者的情况下开始发送数据,并且可以有多个订阅者同时接收数据。这种设计使得 SharedFlow 非常适合 用于在多个组件之间共享数据 ,而不需要显式地管理订阅者的生命周期。
SharedFlow 主要关注其非粘性的特点,其实可以通过一些参数的配置来让 SharedFlow 在有观察者开始工作之前缓存一定数量的消息,甚至还可以让 SharedFlow 模拟出 StateFlow 的效果。SharedFlow 是一个非常强大的工具,特别适合处理事件总线、一次性操作和需要多个订阅者的场景。
线程的状态 一个 Thread 线程的生命周期:
各种状态一目了然,值得一提的是”blocked”这个状态:线程在Running的过程中可能会遇到阻塞(Blocked)情况
调用 join() 和 sleep() 方法,sleep() 时间结束或被打断,join() 中断去执行其他线程,IO完成都会回到 Runnable 状态,等待JVM的调度。 调用 wait() ,使该线程处于等待池(wait blocked pool),直到 notify()/notifyAll() ,线程被唤醒被放到锁定池(lock blocked pool),释放同步锁使线程回到可运行状态(Runnable) 对 Running 状态的线程加同步锁(Synchronized)使其进入(lock blocked pool),同步锁被释放进入可运行状态(Runnable)。 此外,在 runnable 状态的线程是处于被调度的线程,此时的调度顺序是不一定的。 Thread 类中的 yield() 方法可以让一个running状态的线程转入runnable。
为什么会有线程安全问题 如果不使用任何同步机制,在多线程中读写同一个变量。那么,程序的结果是难以预料的。
主要原因有一下几点:
简单的读写不是原子操作 CPU 可能会调整指令的执行顺序 在 CPU cache 的影响下,一个 CPU 执行了某个指令,不会立即被其它 CPU 看见 1. 原子操作 原子操作(Atomic Operation)是指在多线程或并发编程中,不可被中断的一个或一系列操作。这些操作 要么全部执行完成,要么完全不执行 ,不会出现执行到一半被其他线程干扰的情况,从而保证操作的完整性和一致性。
原子操作在执行过程中不会被其他线程或进程打断。并且操作完成后,结果会立即对其他线程可见(通常由硬件或底层内存模型保证)。
非原子操作的影响 举例:
int64_t i = 0; // global variable
Thread-1: Thread-2:
i++; std::cout << i;
C++ 并不保证 i++ 是原子操作。从汇编的角度看,读写内存的操作一般分为三步:
将内存单元读到 cpu 寄存器 修改寄存器中的值 将寄存器中的值回写入对应的内存单元 进一步,有的 CPU Architecture, 64 位数据(int64_t)在内存和寄存器之间的读写需要两条指令。
这就导致了 i++ 操作在 cpu 的角度是一个多步骤的操作。所以 Thread-2 读到的可能是一个中间状态。
2. CPU重排的影响 为了优化程序的执行性能,编译器和 CPU 可能会 调整指令的执行顺序 。为阐述这一点,下面的例子中,让我们假设所有操作都是原子操作:
int x = 0; // global variable
int y = 0; // global variable
Thread-1: Thread-2:
x = 100; while (y != 200) {}
y = 200; std::cout << x;
如果 CPU 没有乱序执行指令,那么 Thread-2 将输出 100。然而,对于 Thread-1 来说,x = 100; 和 y = 200; 这 两个语句之间没有依赖关系 ,因此,CPU可能会允许调整语句的执行顺序。
在这种情况下,Thread-2 的打印,有可能是 0 也有可能是 100 。
2. CPU CACHE的影响 CPU cache 也会影响到程序的行为。下面的例子中,假设从时间上来讲,A 操作先于 B 操作发生:
int x = 0; // global variable
Thread-1: Thread-2:
x = 100; // A std::cout << x; // B
x = 100; ,这个看似简短的语句,在 CPU 的实际执行步骤为:
取指令:CPU从指令缓存中读取 mov 指令。 解码:解码指令,识别操作(写入内存)和操作数(地址 [x] 和值 100)。 内存访问。计算变量 x 的内存地址。若 x 不在缓存中,触发缓存加载(Cache Miss)。 数据写入:将值 100 写入 x 的内存地址。 缓存同步:更新缓存线,可能通过缓存一致性协议(如MESI)通知其他核心。 尽管从时间上来讲,A 先于 B,但 CPU cache 的影响下, Thread-2 不能保证立即看到 A 操作的结果,所以 Thread-2 可能输出 0 或 100。
Java中常见实现线程安全的操作 1. 使用 final 属性 (Immutability) 声明一个字段为 final 后,它的值在对象构造完成后就不能再被改变。如果一个对象的所有字段都是 final 并且它们引用的对象(如果是引用类型)也是不可变的,那么这个对象就是不可变对象 (Immutable Object) 。不可变对象在多线程环境中天然是线程安全的,因为它们的状态不会被任何线程修改。
优点是简单、安全,是实现线程安全的“黄金法则”。应用场景比较有限。
代码举例:
public final class ImmutablePoint {
// 两个final属性,它们的值在构造函数中确定后不可更改
private final int x ;
private final int y ;
public ImmutablePoint ( int x , int y ) {
this . x = x ;
this . y = y ;
}
public int getX () {
return x ;
}
public int getY () {
return y ;
}
// 注意:没有提供任何修改x或y的方法(setter)
}
// 在多线程中使用 ImmutablePoint 对象时,无需任何同步措施。
2. ThreadLocal 线程隔离 ThreadLocal 为每个使用该变量的线程都提供了一个独立的、线程本地的副本。这样,一个线程对变量的修改不会影响到其他线程,从而实现了线程间的隔离,避免了共享资源的竞争。
适用于保存用户会话信息、数据库连接、事务上下文等,这些信息通常只需要在当前线程内共享。
代码举例:
public class ThreadLocalExample {
// 创建一个 ThreadLocal 实例
private static final ThreadLocal < String > threadLocalUser = new ThreadLocal <>();
public static void main ( String [] args ) throws InterruptedException {
Runnable task = () -> {
String threadName = Thread . currentThread (). getName ();
// 1. 设置当前线程的本地变量
threadLocalUser . set ( threadName + "'s Data" );
System . out . println ( threadName + " set data: " + threadLocalUser . get ());
// 输出: A set data: A's Data
try {
Thread . sleep ( 50 ); // 模拟耗时操作
} catch ( InterruptedException e ) {
Thread . currentThread (). interrupt ();
}
// 2. 获取当前线程的本地变量
System . out . println ( threadName + " get data: " + threadLocalUser . get ());
// 输出: A get data: A's Data
// 3. 推荐在线程结束时移除,避免内存泄漏(尤其是在线程池中)
threadLocalUser . remove ();
};
Thread threadA = new Thread ( task , "Thread-A" );
Thread threadB = new Thread ( task , "Thread-B" );
threadA . start ();
threadB . start ();
}
}
/*
可能的输出(Thread-A 和 Thread-B 的数据互不影响):
Thread-A set data: Thread-A's Data
Thread-B set data: Thread-B's Data
Thread-A get data: Thread-A's Data
Thread-B get data: Thread-B's Data
*/
3. volatile 关键字 volatile 保证了对变量读写的可见性 和操作的有序性 ,但 不保证原子性 。
可见性 (Visibility): 当一个线程修改了 volatile 变量的值,新值会立即同步回主内存;当其他线程读取该变量时,会从主内存中重新获取最新值,而不是使用自己的工作内存副本。有序性 (Ordering): 禁止 JVM 对 volatile 变量的读写操作进行重排序。volatile 变量的读写操作仍然在 CPU 缓存中进行,但 JVM 会在这些操作周围插入内存屏障 (Memory Barriers),来保证数据同步。
写操作之后会插入一个 Store Barrier (写屏障) 。这个屏障会强制要求 CPU 将本地缓存中的最新值立即刷新(写入)到主内存。同时,它还会使其他 CPU 核心中该变量的缓存副本失效(Invalidate)。 在读操作之前会插入一个 Load Barrier (读屏障) 。这个屏障会要求 CPU 重新从主内存中加载最新的值到本地缓存,而不是使用可能已过时的本地缓存副本。 适用于修饰状态标记 (flag) 或一次写、多次读的共享变量,但不适用于依赖当前值进行计算的场景(例如 i++)。
代码举例:
public class VolatileExample {
// 状态标志,一个线程修改后,其他线程需要立即看到最新值
private volatile boolean isRunning = true ;
public void stop () {
isRunning = false ; // 线程 A 修改
System . out . println ( Thread . currentThread (). getName () + " set isRunning to false" );
}
public void runLoop () {
// 线程 B 持续读取 isRunning
while ( isRunning ) {
// ... 执行任务
}
System . out . println ( Thread . currentThread (). getName () + " loop stopped." );
}
public static void main ( String [] args ) throws InterruptedException {
VolatileExample example = new VolatileExample ();
// 线程 B 启动循环
Thread runnerThread = new Thread ( example: : runLoop , "Runner-Thread" );
runnerThread . start ();
// 主线程等待一段时间,让 runnerThread 运行起来
Thread . sleep ( 100 );
// 线程 A (主线程) 停止循环
example . stop ();
runnerThread . join (); // 等待 runnerThread 结束
}
}
插入:乐观锁和悲观锁 悲观锁 (Pessimistic Locking) 假设最坏的情况,认为数据随时可能被其他线程或进程修改,所以 每次访问数据时都会先给数据上锁 ,防止其他人在自己操作期间修改数据。Java中的 synchronized , ReentrantLock 都属于悲观锁。
乐观锁 (Optimistic Locking) 假设最好的情况,认为数据被修改的概率很低,所以它不会在访问数据时加锁,而是 在更新数据时才去检查在此期间有没有人修改过数据 。
配合 CAS(Compare and Swap) 机制,这是 CPU 指令级别的原子操作,是 Java 中实现乐观锁的基石。CAS 操作是一个由 CPU 指令保证的原子操作 。
在写值时会先读取一遍当前内存中是否还是原值,如果是则执行写入,如果不是,则放弃修改。
Java 并发包中很多原子类(如 AtomicInteger)就是基于 CAS 乐观锁思想实现的。
4. synchronized 关键字 synchronized 是一种内置的 互斥锁 (Intrinsic Lock) 机制,它确保同一时刻只有一个线程可以执行被它保护的代码块或方法。它保证了操作的原子性 、可见性 和有序性 。
使用方式:
同步实例方法: 锁住当前实例对象 (this)。同步静态方法: 锁住当前类的 Class 对象。同步代码块: 锁住括号内指定的对象。代码举例 (同步代码块):
public class SynchronizedExample {
private int count = 0 ;
// 使用一个私有的 final 对象作为锁,避免外部干扰
private final Object lock = new Object ();
public void increment () {
// 只有获取到 lock 对象的锁的线程才能进入代码块
synchronized ( lock ) {
// 这是一个复合操作 (读->改->写),必须是原子性的
count ++;
}
}
// 也可以同步方法: public synchronized void increment() { count++; }
public int getCount () {
return count ;
// 实际上,为了保证可见性,这里读取操作也应该同步,或者将 count 声明为 volatile。
// 为了演示 synchronized 保证原子性,此处简化。
}
}
5. Lock 加锁 (J.U.C Lock Interface) Lock 接口(如 ReentrantLock)是 Java 5 引入的,属于 java.util.concurrent.locks 包下的显式锁机制。它提供了比 synchronized 更灵活、更强大的功能,例如:
可中断锁: lock.lockInterruptibly()尝试锁: lock.tryLock()定时锁: lock.tryLock(long timeout, TimeUnit unit)公平锁/非公平锁: 可以在构造函数中指定。代码举例 (ReentrantLock):
import java.util.concurrent.locks.Lock ;
import java.util.concurrent.locks.ReentrantLock ;
public class LockExample {
private int count = 0 ;
// 创建一个可重入锁实例
private final Lock lock = new ReentrantLock ();
public void increment () {
// 1. 获取锁
lock . lock ();
try {
// 确保同步代码块中的操作是原子性的
count ++;
} finally {
// 2. 释放锁。注意:必须放在 finally 块中,确保在发生异常时也能释放锁
lock . unlock ();
}
}
public int getCount () {
return count ;
}
}
常见集合类容器的线程安全分析 Java 中的集合类主要分为 非线程安全 (Non-thread-safe) 、线程安全 (Thread-safe) 的同步容器和 并发容器 (Concurrent) 三类。
1. 列表类 (List) ArrayList 是一个普通的、非同步的类,它的方法(如 add(), get(), remove() 等)都没有使用 synchronized 关键字进行同步控制。在多线程环境下并发操作(如增删改)会导致数据不一致或抛出 ConcurrentModificationException。
为什么说它不是线程安全的?
如果在多线程环境中,多个线程同时对同一个 ArrayList 实例进行修改操作 (例如一个线程在 add(),另一个线程在 remove()),就可能出现以下问题:
数据不一致(Data Corruption) :例如,两个线程同时尝试添加元素,可能会导致底层数组的数据混乱。竞态条件(Race Condition) :可能导致 ArrayList 的内部状态(如记录大小的 size 变量)被错误更新。抛出异常 :最常见的情况是在遍历(迭代)时,另一个线程修改了列表结构,会抛出 ConcurrentModificationException。如何使 List 线程安全? 如果您需要在多线程环境中使用一个类似 ArrayList 的列表,有以下几种线程安全的替代方案:
使用同步包装器(Synchronized Wrapper):
List < String > synchronizedList = Collections . synchronizedList ( new ArrayList < String >());
简单易用。但是性能较低,因为它对所有操作都是通过锁住整个列表对象来实现的,在高并发下会有性能瓶颈。
使用 JUC 包中的并发列表(推荐):
List < String > safeList = new CopyOnWriteArrayList < String >();
public void addItem ( String item ) {
safeList . add ( item ); // 线程安全
}
Java原生提供的 CopyOnWriteArrayList 性能更高,尤其是在 读多写少 的场景。其采用了 写时复制 的策略。当列表需要被修改时(add、set 等),它会创建一个新的底层数组副本,修改在新副本上进行,然后将列表的引用指向新副本。读取操作(get)则始终在旧的数组上进行,不需要加锁。
2. Map类 HashMap 是最常用的 Map 实现。基于哈希表(数组+链表/红黑树)实现。它的键和值都允许为 null。
HashMap 是 Java 中最常用的 Map 实现,它在设计时主要关注的是性能 (查找、插入等操作的平均时间复杂度为 $O(1)$),而不是线程安全。它 适用于单线程环境 。性能最高 O(1) 级别,但在并发环境下(多线程同时读写)会引发问题,例如多个线程同时对同一个 HashMap 实例进行修改操作(put()、remove() 等),会引发严重的问题,包括:
数据丢失或不一致: 多个线程同时操作同一个桶(Bucket)时,可能导致数据覆盖或链表结构混乱。死循环 (Infinite Loop): 在 HashMap 扩容(resize())的过程中,链表会被重新分配到新的数组中。在并发修改的情况下,可能会出现链表节点相互指向的情况,形成环状结构 。当另一个线程尝试遍历这个环时,就会导致 CPU 占用 100% 的死循环 ,使程序彻底挂死。这个问题在早期的 JDK 版本中尤其常见。抛出异常: 类似 ArrayList,在迭代过程中进行结构性修改,也会抛出 ConcurrentModificationException。Map线程安全的替代方案 如果需要在多线程环境中使用一个 Map 结构,类似于List的策略,可以使用以下线程安全的两种方案:
Collections.synchronizedMap() 包装一个HashMap可以实现线程安全,但是性能较差 。通过包装 HashMap,使用锁住整个对象的方式实现同步。适用于对性能要求不高的多线程环境。ConcurrentHashMap 是一个高性能的Map类。采用更细粒度的锁机制(如 Java 8 采用 CAS + Synchronized 锁住单个桶)。读操作通常是无锁的。绝大多数多线程 Map 场景的首选。 提供了高并发下的高性能。Git的各个库的组成部分主要分为以下几个区域:
区域的含义:
工作区域(Working Directory)就是你平时存放项目代码的地方。 暂存区域(Stage)用于临时存放你的改动,事实上它只是一个文件,保存即将提交的文件列表信息。 Git 仓库(Repository)就是安全存放数据的位置,这里边有你提交的所有版本的数据。其中,HEAD 指向最新放入仓库的版本(这第三棵树,确切的说,应该是 Git 仓库中 HEAD 指向的版本)。 remote就是远端的代码仓,存放在线的公共的代码。 基础提交代码三大步:
git add .
# 添加所有修改到暂存区
git commit -m "message"
# 提交修改到仓库区域
git push origin main
# 将提交的修改上传到git服务器仓库
两种拉取代码的方式 http克隆 直接通过仓库的https链接就可以拉取下载。
git clone https://github.com/stepheneasyshot/stepheneasyshot.github.io.git
ssh克隆 生成本地key 打开cmd命令行,输入以下命令:
C:\Users\stephen\Desktop>ssh-keygen -t rsa -b 4096 -C "zhanfeng990927@gmail.com"
Generating public/private rsa key pair.
Enter file in which to save the key (C:\Users\stephen/.ssh/id_rsa):
Created directory 'C:\\Users\\stephen/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in C:\Users\stephen/.ssh/id_rsa
Your public key has been saved in C:\Users\stephen/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:XXXXXXXXXX/4pbXXXXXXXXXXXXXXXXX zhanfeng990927@gmail.com
The key's randomart image is:
+---[RSA 4096]----+
| XXXXXX |
| XXXXXXXXX |
| XXXXX |
| XXXXXX |
| XXXXXXXXXX |
| XXXXXXXXXX |
| XXXXXXXXXXXX |
| XXXXXXXXXX |
| XXXXXXXXXXX |
+----[SHA256]-----+
Enter passphrase这两步是提示输入密码,建议不要设置,直接生成。
完毕后在C盘的用户文件夹下应该会生成一个 .ssh 文件夹,可以在 .ssh 目录下看到两个文件:id_rsa 和 id_rsa.pub
其中的pub公钥就是需要上传到git服务器上做验证的文件。
上传key到git服务器 生成SSH Key后,需要将公钥添加到远程Git仓库中。使用cat命令查看公钥内容,Windows上应该直接使用记事本打开即可显示:
cat ~/.ssh/id_rsa.pub
一般格式如下:
ssh-rsa XXXXXX...lrw== zhanfeng990927@gmail.com
然后,复制这串公钥内容。 登录到远程Git仓库,找到SSH Key配置页面,将公钥粘贴到相应位置并保存。
验证并拉取代码 打开git bash,输入ssh -T git@github.com做初次验证,然后就可以通过ssh的方式拉取github上的代码了:
~/Desktop:$ ssh -T git@github.com
The authenticity of host 'github.com (20.205.243.166)' can't be established.
ED25519 key fingerprint is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? y
Please type 'yes', 'no' or the fingerprint: yes
Warning: Permanently added 'github.com' (ED25519) to the list of known hosts.
Hi stepheneasyshot! You've successfully authenticated, but GitHub does not provide shell access.
~/Desktop:$ git clone git@github.com:stepheneasyshot/stepheneasyshot.github.io.git
Cloning into 'stepheneasyshot.github.io'...
Git文件状态 通常我们需要查看一个文件的状态,使用git status
Changes not staged for commit 表示得大概就是工作区有该内容,但是缓存区没有,需要我们git add.
Changes to be committed 一般而言,这个时候,文件放在缓存区了,我们需要git commit.
nothing to commit, working tree clean 这个时候,我们将本地的代码推送到远端即可。
git配置 列出当前配置
列出当前Repository配置
git config --local --list
列出全局配置
git config --global --list
列出系统配置
git config --system --list
配置用户名
git config --global user.name "your name"
配置用户邮箱
git config --global user.email "youremail@github.com"
分支管理 查看本地分支
查看远程分支
查看本地和远程分支
从当前分支,切换到其他分支
git checkout <branch-name>
创建并切换到新建分支
git checkout -b <branch-name>
删除分支
git branch -d <branch-name>
// 若有未提交的代码,仍然强行删除刻可以使用 D
当前分支与指定分支合并
查看哪些分支已经合并到当前分支
查看哪些分支没有合并到当前分支
查看各个分支最后一个提交对象的信息
删除远程分支
git push origin -d <branch-name>
重命名分支
git branch -m <oldbranch-name> <newbranch-name>
拉取远程分支并创建本地分支
git checkout -b 本地分支名x origin/远程分支名x
// 另外一种方式,也可以完成这个操作。
git fetch origin <branch-name>:<local-branch-name>
fetch推荐写法
git fetch origin <branch-name>:<local-branch-name>
一般而言,这个origin是远程主机名,一般默认就是origin。
branch-name 你要拉取的分支
local-branch-name 通常而言,就是你本地新建一个新分支,将origin下的某个分支代码下载到本地分支。
撤销操作 撤销工作区修改
暂存区文件撤销 (不覆盖工作区)
版本回退
git reset --(soft | mixed | hard ) < HEAD ~(num) >
可以使用git log 找到commitid,以精确还原
比较差异 比较工作区与缓存区
比较缓存区与本地库最近一次commit内容
比较工作区与本地最近一次commit内容
比较两个commit之间差异
git diff <commit ID> <commit ID>
一般分支命名 master分支 主分支,用于部署生产环境的分支,确保稳定性。 master分支一般由develop以及hotfix分支合并,任何情况下都不能直接修改代码。
develop 分支 develop为开发分支,通常情况下,保存最新完成以及bug修复后的代码。 开发新功能时,feature分支都是基于develop分支下创建的。
feature分支 开发新功能,基本上以develop为基础创建feature分支。 分支命名:feature/ 开头的为特性分支, 命名规则: feature/user_module、 feature/cart_module。
release分支 release 为预上线分支,发布提测阶段,会release分支代码为基准提测。
hotfix分支 分支命名:hotfix/ 开头的为修复分支,它的命名规则与 feature 分支类似。线上出现紧急问题时,需要及时修复,以master分支为基线,创建hotfix分支,修复完成后,需要合并到master分支和develop分支。
.gitignore文件配置 这个文件的作用,会去忽略一些不需要纳入Git管理这种,我们也不希望出现在未跟踪文件列表。
一般配置方法:
# 井号,此行是注释 会被Git忽略
# 忽略 node_modules/ 目录下所有的文件
node_modules/
# 忽略所有.vscode结尾的文件
.vscode
# 忽略所有.md结尾的文件
*.md
# 但README.md 除外
!README.md
# 会忽略 doc/something.txt 但不会忽略doc/images/arch.txt
doc/*.txt
# 忽略 doc/ 目录下所有扩展名为txt文件
doc/**/*.txt
增加效率的一些操作 git stash 某一天你正在 feature 分支开发新需求,突然产品经理跑过来说线上有bug,必须马上修复。而此时你的功能开发到一半,于是你急忙想切到 master 分支,然后你就会看到以下报错:
error: Your local changes to the following files would be overwritten by checkout:
src/entries/index/config/group.js
Please commit your changes or stash them before youswitch branches.
Aborting
因为当前有文件更改了,需要提交commit保持工作区干净才能切分支。由于情况紧急,你只有急忙 commit 上去,commit 信息也随便写了个“暂存代码”,于是该分支提交记录就留了一条黑历史…
不想随便写一个commit,又要切代码回去,就可以用git stash这个命令。 代码修改会被暂存下来,当从主分支修完bug切回来时,可以使用
来恢复代码。
相关命令如下:
# 保存当前未commit的代码
git stash
# 保存当前未commit的代码并添加备注
git stash save "备注的内容"
# 列出stash的所有记录
git stash list
# 删除stash的所有记录
git stash clear
# 应用最近一次的stash
git stash apply
# 应用最近一次的stash,随后删除该记录
git stash pop
# 删除最近的一次stash
git stash drop
可以生成多条stash,并使用git stash list来查看。
$ git stash list
stash@{0}: WIP on ...
stash@{1}: WIP on ...
stash@{2}: On ...
应用第二条记录:
$ git stash apply stash@{1}
pop,drop 同理。
git reset 有soft和hard两种。
git reset –hard commitId 将git头部回溯到这笔提交,这笔之后的所有本地修改全部删除 ,新建的文件貌似不会自动删除。
依次提交a,b,c,
使用hard模式回溯到a,
那么本地b,c的内容将被删除
git reset –soft commitId 将git的头部回溯到这一笔提交,之后的所有变更修改都会被保存下来 放到暂存区,并当作下一笔的提交内容。
依次提交a,b,c,使用soft模式回溯,会将b,c的修改保留,在下一次commit的时候将合并到一起。
git cherry-pick 将一个分支的某些提交,复制到另一个分支上,比如release分支上修复的某一些bug,在开发dev分支上也需要同步上去。 这时候就可以使用这个命令。
单个提交举例 如果要把分支 b 的 test 提交给复制到分支 a 上。 先在b分支上使用git log,获取 test 这笔提交的commitHash值,切到分支a,使用:
git cherry-pick commitHash
就可以同步分支b的这一笔提交。
一次转移多个提交 git cherry-pick commit1 commit2
上面的命令将 commit1 和 commit2 两个提交应用到当前分支。
多个连续的commit,也可区间复制:
git cherry-pick commit1^..commit2
上面的命令将 commit1 到 commit2 这个区间的 commit 都应用到当前分支(包含commit1、commit2),commit1 是最早的提交。
cherry-pick冲突 如果分支b有提交1,2,3,4,5需要同步到分支a。 1,2,3顺利合并了,但是复制到第4个提交时,与分支a的本地提交修改的内容有冲突了,这时候需要解决冲突。
有三种方式
放弃cherry-pick 这个操作会将已经合入的123也给回退掉,就像什么都没有发生过。
退出cherry-pick 保留已经合入的1,2,3笔提交,还有正卡在冲突处的第4笔提交,退出合入过程,这时候第5笔就相当于放弃了。
解冲突后继续合入 git cherry-pick --continue
在手动解完冲突并commit提交后,执行continue命令,会把未合入的第5笔提交也同步进来。
git revert 有提交1,2,3,现在发现提交2会引起严重问题,需要将这一笔撤销掉。可以使用reset命令,但是如果使用reset –hard,会将提交3也回退,需要重新提一次第3笔。
这时候可以使用revert精准回退某一笔提交:
输入完后push上去,可以只回退提交2的内容。
包含merge节点的revert回退 如果有提交1,2,3,三笔提交,其中提交3是dev分支合并到主分支上的信息。
这时候直接revert掉2,git会搞不清楚是回退主分支上的2,还是回退掉dev分支上的2,执行不成功。
需要使用 -m 手动指定保留的分支,另一条分支上的2就会被回退。
-m 后面要跟一个 parent number 标识出”主线”,一般使用 1 保留主分支代码。
git revert -m 1 <commitHash>
revert了主分支,dev修好之后合并失效 情景和上面类似,但是把master分支上有问题的提交给revert掉,
如下图,有问题的提交是 b 这一笔,在master上revert掉之后,在dev上修复了这个有问题的提交,准备再次往主分支合并,这时候发现dev上的 b 提交没有同步到master,因为master上已经有一笔 b 的合入记录,在dev修改完毕准备合入时,git一比较,这两笔相同的commitHash,就不会合入dev上 b 的提交。
解决方案 使用reset将master回滚出问题之前,也就是 a 这一笔提交。这样master上面就不会有b的残留记录了。
然后把修复完毕的dev分支合并进来。
git reflog reflog可以找到所有的操作记录。用于误删或者回退过多的场景。
即使在使用了 git reset --hard 或 git branch -D 等命令后,仍然可以通过 reflog 找到之前的状态。
如果有提交a,b,c。其中 c 是你提交的有问题的一笔,需要reset掉,但是看错了commitHash值,将别人提交的 b 都给回退掉了,现在使用git log回溯,git记录上只有a这笔提交了。
这时候可以使用git reflog,它记录了所有的操作记录,记下需要恢复的commitID,就可以还原回来被误删的 b 这笔提交了。
设置git的短命令 方式一
git config --global alias.ps push
方式二 打开用户目录下的 .gitconfig 文件,写入:
[alias]
co = checkout
ps = push
pl = pull
mer = merge --no-ff
cp = cherry-pick
就可以使用短命令来进行git操作了。
例如:
git rebase git rebase最大的作用就是让提交记录更加简洁。
分支变基 现在有这一个场景:
master分支上A之后拉出一条开发分支,而后一个同事提交了B。
你在dev分支上提交了C和D,现在想把dev合并到主分支,使用merge的话,会多出一条空白的merge提交信息。
这时候可以使用
git checkout feature
git rebase master
//这两条命令等价于git rebase master feature
相当于让dev分支的基底从提交A的master开始,变成了从提交B开始,后面提交的C和D就是顺序的提交了。
整个提交记录则是ABCD连续的,合并到主分支上时不会产生merge信息。
同样适用于同一分支上不同开发的提交信息简化。
没有merge信息有好有坏吧,虽然看起来简洁,也无法回溯合代码的历史了。
重塑提交历史 写法如下:
可以让你把最近的几笔提交信息进行操作,可以提交的调整顺序,合并,删除不想要的提交。即可以将多笔记录合并成一个。
例如:
就可以列出HEAD开始往前四笔提交,供你编辑。
推荐场景 不同公司,不同情况有不同使用场景,不过大部分情况推荐如下:
单人开发的时候,拉公共分支最新代码的时候使用rebase,也就是git pull -r或git pull –rebase。这样的好处很明显,提交记录会比较简洁。但有个缺点就是rebase以后我就不知道我的当前分支最早是从哪个分支拉出来的了,因为基底变了嘛,所以看个人需求了。
多人开发,往公共分支上合代码的时候,使用merge。如果使用rebase,那么其他开发人员想看主分支的历史,就不是原来的历史了,历史已经被你篡改了。举个例子解释下,比如张三和李四从共同的节点拉出来开发,张三先开发完提交了两次然后merge上去了,李四后来开发完如果rebase上去(注意李四需要切换到自己本地的主分支,假设先pull了张三的最新改动下来,然后执行<git rebase 李四的开发分支>,然后再git push到远端),则李四的新提交变成了张三的新提交的新基底,本来李四的提交是最新的,结果最新的提交显示反而是张三的,就乱套了。
正因如此,大部分公司其实会禁用rebase,不管是拉代码还是push代码统一都使用merge,虽然会多出无意义的一条提交记录“Merge … to …”,但至少能清楚地知道主线上谁合了的代码以及他们合代码的时间先后顺序。
Pagination © 2024. All rights reserved. LICENSE | NOTICE | CHANGELOG
Powered by Hydejack v9.2.1