Kotlin - 协程结构化并发Structured Concurrency

前言

Kotlin的Project Lead,Roman Elizarov的一片文章https://elizarov.medium.com/structured-concurrency-722d765aa952介绍了Structured Concurrency发展的背景。相对Kotlin1.1时代,后来新增的Structured Concurrency理念,也就是我们现在所熟悉的协程版本所具备的特性,解决了各种复杂业务场景下,例如协程嵌套、异步等等使用方式时所面临的生命周期管理问题。本文通过梳理源码来试图理解Structured Concurrency的具体含义和实现原理。

概念理解

常见的业务场景如下:

复制代码
suspend` `fun` `loadAndCombine(name1: String, name2: String): Image {` 
    `val deferred1 = async {` `loadImage(name1)` `}`
    `val deferred2 = async {` `loadImage(name2)` `}`
    `return` `combineImages(deferred1.await(), deferred2.await())`
`}

deferred1和deferred2都是异步执行的,最终需要将二者的执行结果合并后返回。而如果此时其中一个loadImage执行异常,或者主动取消,很难去通知另一个LoadImage及时停止执行,释放资源。

或者如下场景:

复制代码
val scope =` `CoroutineScope(Job())`
`    scope.launch` `{`
        `printLog("launch1")`
`        launch {`
            `delay(20000)`
            `printLog("launch1-1")`
        `}`
        `printLog("launch1 done")`
        `cancel()`
    `}

在外层的launch执行到最后,希望cancel内部所有的子协程。在没有Structrued Concurrency特性的时候,要实现这种逻辑需要类似使用线程时的处理方式。而Structrued Concurrency特性可以让我们在cancel外层协程时,自动cancel其里面所有的子协程。

这就是所谓的对协程生命周期的管理。为了能够将所有协程的生命周期完全管理起来,Kotlin使用了CoroutineScope。

Coroutines are always related to some local scope in your application, which is an entity with a limited life-time, like a UI element.

CoroutineScope相当于圈定了一个空间,所有的协程都在这个空间里面执行。这样所有协程的声明周期就可以通过CoroutineScope来进行管理了。

实现原理

我们知道每个launch都是一个Job。Job和CoroutineScope的关系如下:

再次根据这个例子,看这种关系如何实现的:

复制代码
val scope =` `CoroutineScope(Job())`
`    scope.launch` `{`
        `printLog("launch1")`
`        launch {`
            `delay(20000)`
            `printLog("launch1-1")`
        `}`
        `printLog("launch1 done")`
        `cancel()`
    `}`
`

首先看新建CoroutineScope(Job())

复制代码
kotlinx.coroutines-master\kotlinx-coroutines-core\common\src\CoroutineScope.kt`

`public` `fun` `CoroutineScope(context: CoroutineContext): CoroutineScope =`
    `ContextScope(if` `(context[Job]` `!=` `null) context else context +` `Job())
复制代码
internal` `class` `ContextScope(context: CoroutineContext)` `: CoroutineScope {`
    `override` `val coroutineContext: CoroutineContext = context`
    `// CoroutineScope is used intentionally for user-friendly representation`
    `override` `fun` `toString(): String =` `"CoroutineScope(coroutineContext=$coroutineContext)"`
`}`
`

CoroutineScope本身是一个接口,这里的CoroutineScope不是构造函数,而是一个顶层函数。这里有两个关注点:

context[Job]和context + Job()

所有的Job、CoroutineDispatcher都继承于CoroutineContext。因此CoroutineScope函数的参数我们可以新建一个Job(), 也可以传一个CoroutineDispatcher。以Job()为例,看下其实现:

复制代码
public` `interface Job : CoroutineContext.Element` `{`
    `/**`
`     * Key for [Job] instance in the coroutine context.`
`     */`
    `public` `companion` `object Key : CoroutineContext.Key<Job>

Job继承于CoroutineContext.Element,

复制代码
public` `interface Element : CoroutineContext {`
        `/**`
`         * A key of this coroutine context element.`
`         */`
        `public` `val key: Key<*>`

        `public` `override` `operator` `fun` `<E : Element>` `get(key: Key<E>): E?` `=`
            `@Suppress("UNCHECKED_CAST")`
            `if` `(this.key == key)` `this` `as E else` `null`

        `public` `override` `fun` `<R>` `fold(initial: R, operation:` `(R, Element)` `-> R): R =`
            `operation(initial,` `this)`

        `public` `override` `fun` `minusKey(key: Key<*>): CoroutineContext =`
            `if` `(this.key == key) EmptyCoroutineContext else` `this`
    `}

注意这里的get函数,其返回值取决于key。key在哪里赋值的?

Job也是一个接口,其Job()也是一个顶层函数:

复制代码
public` `fun` `Job(parent: Job?` `=` `null): CompletableJob =` `JobImpl(parent)
复制代码
internal` `open` `class` `JobImpl(parent: Job?)` `:` `JobSupport(true), CompletableJob {

JobImp继承JobSupport,而JobSupport是Job的具体实现

复制代码
public` `open` `class JobSupport constructor(active: Boolean)` `: Job, ChildJob, ParentJob {`
    `final` `override` `val key: CoroutineContext.Key<*>` `get()` `= Job

可以看到key的实际值为Job。

所以,如果CoroutineScope(...)的参数传入的是Job(), 则context[Job]返回的是Job。

那context + Job()代表什么呢?

在CoroutineContext的接口声明里看到了plus操作符重载:

复制代码
public` `operator` `fun` `plus(context: CoroutineContext): CoroutineContext =`
        `if` `(context === EmptyCoroutineContext)` `this` `else` `// fast path -- avoid lambda creation`
`            context.fold(this)` `{ acc, element ->`
                `val removed = acc.minusKey(element.key)`
                `if` `(removed === EmptyCoroutineContext) element else` `{`
                    `// make sure interceptor is always last in the context (and thus is fast to get when present)`
                    `val interceptor = removed[ContinuationInterceptor]`
                    `if` `(interceptor ==` `null)` `CombinedContext(removed, element)` `else` `{`
                        `val left = removed.minusKey(ContinuationInterceptor)`
                        `if` `(left === EmptyCoroutineContext)` `CombinedContext(element, interceptor)` `else`
                            `CombinedContext(CombinedContext(left, element), interceptor)`
                    `}`
                `}`
            `}

是将两个CoroutineContext合并成了CombinedContext。CombinedContext本身也是一个CoroutineContext。

综上, 在新建CoroutineScope的时候,如果传入了一个Job,则使用这个Job,如果没有传入Job(可能传入一个CoroutineDispatcher),则新建一个Job。然后将Job赋值给ContextScope的coroutineContext成员变量。

如此一来,一个新建的CoroutineScope就关联了一个顶层的Job。

使用launch创建一个协程:
复制代码
kotlinx.coroutines-master\kotlinx-coroutines-core\common\src\Builders.common.kt`

`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`
`}`
`

首先,launch是CoroutineScope的扩展函数,也就是说只能在CoroutineScope内创建协程。newCoroutineContext(context):

复制代码
public` `actual` `fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {`
    `val combined =` `foldCopies(coroutineContext, context,` `true)`
    `val debug =` `if` `(DEBUG) combined +` `CoroutineId(COROUTINE_ID.incrementAndGet())` `else combined`
    `return` `if` `(combined !== Dispatchers.Default && combined[ContinuationInterceptor]` `==` `null)`
`        debug + Dispatchers.Default else debug`
`}

这里context是EmptyCoroutineContext,coroutineContext是刚才CoroutineScope(Job())传入的顶层Job。经过foldCopies后,返回的combined可以看做是顶层Job的封装。在return语句中可以看到debug(即顶层Job)加上了debug + Dispatchers.Default,这就是为什么默认会运行在Dispatchers.Default线程的原因。

创建了newContext后,如果start.isLazy会构建LazyStandaloneCoroutine,否则构建StandaloneCoroutine。start是协程的执行方式,默认为立即执行,也可以懒加载执行。具体见kotlinx.coroutines-master\kotlinx-coroutines-core\common\src\CoroutineStart.kt

这里构建的是默认的StandaloneCoroutine

复制代码
private` `open` `class` `StandaloneCoroutine(`
`    parentContext: CoroutineContext,`
`    active: Boolean`
`)` `: AbstractCoroutine<Unit>(parentContext, initParentJob =` `true, active = active)` `{`
    `override` `fun` `handleJobException(exception: Throwable): Boolean {`
        `handleCoroutineException(context, exception)`
        `return` `true`
    `}`
`}

parentContext参数传入的是刚才构建的newContext,也就是顶层Job。initParentJob默认值为true。接着看下他的继承类AbstractCoroutine:

复制代码
public` `abstract` `class AbstractCoroutine<in T>(`
`    parentContext: CoroutineContext,`
`    initParentJob: Boolean,`
`    active: Boolean`
`)` `:` `JobSupport(active), Job, Continuation<T>, CoroutineScope {`

    `init` `{`
        `/*`
`         * Setup parent-child relationship between the parent in the context and the current coroutine.`
`         * It may cause this coroutine to become _cancelling_ if the parent is already cancelled.`
`         * It is dangerous to install parent-child relationship here if the coroutine class`
`         * operates its state from within onCancelled or onCancelling`
`         * (with exceptions for rx integrations that can't have any parent)`
`         */`
        `if` `(initParentJob)` `initParentJob(parentContext[Job])`
    `}

AbstractCoroutine继承了JobSupport、Job,也就是说,StandaloneCoroutine实际上是构造了一个Job。看下这里的initParentJob(parentContext[Job]),parentContext是刚才传进来的顶层Job的封装newContext,这里取出其Job传进initParentJob

复制代码
protected` `fun` `initParentJob(parent: Job?)` `{`
`        assert { parentHandle ==` `null` `}`
        `if` `(parent ==` `null)` `{`
`            parentHandle = NonDisposableHandle`
            `return`
        `}`
`        parent.start()` `// make sure the parent is started`
        `val handle = parent.attachChild(this)`
`        parentHandle = handle`
        `// now check our state _after_ registering (see tryFinalizeSimpleState order of actions)`
        `if` `(isCompleted)` `{`
`            handle.dispose()`
`            parentHandle = NonDisposableHandle // release it just in case, to aid GC`
        `}`
    `}

这里会执行parent.attachChild(this)。字面上理解即将launch创建出来的新Job作为Child加入到顶层的Job中去。

关联父子Job

看下具体实现:

kotlinx.coroutines-master\kotlinx-coroutines-core\common\src\JobSupport.kt

复制代码
public` `final` `override` `fun` `attachChild(child: ChildJob): ChildHandle {`
        `val node =` `ChildHandleNode(child).also` `{ it.job =` `this` `}`
        `val added =` `tryPutNodeIntoList(node)` `{ _, list ->`
            `// First, try to add a child along the cancellation handlers`
            `val addedBeforeCancellation = list.addLast(`
`                node,`
`                LIST_ON_COMPLETION_PERMISSION or LIST_CHILD_PERMISSION or LIST_CANCELLATION_PERMISSION`
            `)`
            `...`
`                node.invoke(rootCause)`
                `if` `(addedBeforeCompletion)` `{`
                    `/** The root cause can't be null: since the earlier addition to the list failed, this means that`
`                     * the job was already cancelled or completed. */`
`                    assert { rootCause !=` `null` `}`
                    `true`
                `}` `else` `{`
                    `/** No sense in retrying: we know it won't succeed, and we already invoked the handler. */`
                    `return NonDisposableHandle`
                `}`
            `}`
        `}`
        `if` `(added)` `return node`
        `/** We can only end up here if [tryPutNodeIntoList] detected a final state. */`
`        node.invoke((state as? CompletedExceptionally)?.cause)`
        `return NonDisposableHandle`
    `}

首先构造一个ChildHandleNode

复制代码
private` `class` `ChildHandleNode(`
    `@JvmField` `val childJob: ChildJob`
`)` `:` `JobNode(), ChildHandle {`
    `override` `val parent: Job get()` `= job`
    `override` `val onCancelling: Boolean get()` `=` `true`
    `override` `fun` `invoke(cause: Throwable?)` `= childJob.parentCancelled(job)`
    `override` `fun` `childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)`
`}

这里parent传入的是顶层Job,childJob是launch新建的Job

tryPutNodeIntoList

复制代码
private` `inline` `fun` `tryPutNodeIntoList(`
`        node: JobNode,`
`        tryAdd:` `(Incomplete, NodeList)` `-> Boolean`
    `): Boolean {`
`        loopOnState { state ->`
            `when` `(state)` `{`
                `is Empty ->` `{` `// EMPTY_X state -- no completion handlers`
                    `if` `(state.isActive)` `{`
                        `// try to move to the SINGLE state`
                        `if` `(_state.compareAndSet(state, node))` `return` `true`
                    `}` `else`
                        `promoteEmptyToNodeList(state)` `// that way we can add listener for non-active coroutine`
                `}`
                `is Incomplete ->` `when` `(val list = state.list)` `{`
                    `null` `->` `promoteSingleToNodeList(state as JobNode)`
                    `else` `->` `if` `(tryAdd(state, list))` `return` `true`
                `}`
                `else` `->` `return` `false`
            `}`
        `}`
    `}
复制代码
private` `val _state = atomic<Any?>(if` `(active) EMPTY_ACTIVE else EMPTY_NEW)`

`private` `inline` `fun` `loopOnState(block:` `(Any?)` `-> Unit): Nothing {`
        `while` `(true)` `{`
            `block(state)`
        `}`
    `}

state是什么?

复制代码
kotlinx.coroutines-master\kotlinx-coroutines-core\common\src\JobSupport.kt`

`private` `val EMPTY_NEW =` `Empty(false)`
`private` `val EMPTY_ACTIVE =` `Empty(true)

在JobSupport中,维护了一个状态机,管理Job的不同状态阶段。这里EMPTY_NEW和 EMPTY_ACTIVE是其具体状态。

复制代码
private` `class` `Empty(override` `val isActive: Boolean)` `: Incomplete {`
    `override` `val list: NodeList?` `get()` `=` `null`
    `override` `fun` `toString(): String =` `"Empty{${if (isActive) "Active" else "New" }}"`
`}

其内维护了一个list

简言之,就是tryAdd(state, list)会将自己的state内的list传递给调用它的tryPutNodeIntoList,在回头看tryPutNodeIntoList,

复制代码
val addedBeforeCompletion = list.addLast(`
`                    node,`
`                    LIST_CHILD_PERMISSION or LIST_ON_COMPLETION_PERMISSION`
                `)

会将子Job加到list中。

由此一来,CoroutineScope边构建了它的Job树。

Job的执行

回到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`
`}

在构建完coroutine后,执行coroutine.start

复制代码
public` `fun` `<R>` `start(start: CoroutineStart, receiver: R, block:` `suspend R.()` `-> T)` `{`
        `start(block, receiver,` `this)`
    `}
复制代码
public` `enum` `class CoroutineStart {`
`...`
`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`
        `}

在这里开始执行协程。

Structured Concurrency的典型作用:协程的cancel

当执行scope的cancel时:

复制代码
public` `fun CoroutineScope.cancel(cause: CancellationException?` `=` `null)` `{`
    `val job = coroutineContext[Job]` `?:` `error("Scope cannot be cancelled because it does not have a job: $this")`
`    job.cancel(cause)`
`}

是通过coroutineContext[Job]获取了顶层Job,然后执行其cancel

复制代码
kotlinx.coroutines-master\kotlinx-coroutines-core\common\src\JobSupport.kt`
`public` `override` `fun` `cancel(cause: CancellationException?)` `{`
        `cancelInternal(cause ?:` `defaultCancellationException())`
    `}`

 `public` `open` `fun` `cancelInternal(cause: Throwable)` `{`
        `cancelImpl(cause)`
    `}`
    
`internal` `fun` `cancelImpl(cause: Any?): Boolean {`
        `var finalState: Any?` `= COMPLETING_ALREADY`
        `if` `(onCancelComplete)` `{`
            `// make sure it is completing, if cancelMakeCompleting returns state it means it had make it`
            `// completing and had recorded exception`
`            finalState =` `cancelMakeCompleting(cause)`
            `if` `(finalState === COMPLETING_WAITING_CHILDREN)` `return` `true`
        `}`
        `if` `(finalState === COMPLETING_ALREADY)` `{`
`            finalState =` `makeCancelling(cause)`
        `}`
        `return` `when` `{`
`            finalState === COMPLETING_ALREADY ->` `true`
`            finalState === COMPLETING_WAITING_CHILDREN ->` `true`
`            finalState === TOO_LATE_TO_CANCEL ->` `false`
            `else` `->` `{`
                `afterCompletion(finalState)`
                `true`
            `}`
        `}`
    `}

以makeCancelling为例

复制代码
private` `fun` `makeCancelling(cause: Any?): Any?` `{`
        `var causeExceptionCache: Throwable?` `=` `null` `// lazily init result of createCauseException(cause)`
`        loopOnState { state ->`
            `when` `(state)` `{`
                `is Finishing ->` `{` `// already finishing -- collect exceptions`
                    `val notifyRootCause =` `synchronized(state)` `{`
                        `if` `(state.isSealed)` `return TOO_LATE_TO_CANCEL // already sealed -- cannot add exception nor mark cancelled`
                        `// add exception, do nothing is parent is cancelling child that is already being cancelled`
                        `val wasCancelling = state.isCancelling // will notify if was not cancelling`
                        `// Materialize missing exception if it is the first exception (otherwise -- don't)`
                        `if` `(cause !=` `null` `||` `!wasCancelling)` `{`
                            `val causeException = causeExceptionCache ?:` `createCauseException(cause).also` `{ causeExceptionCache = it }`
`                            state.addExceptionLocked(causeException)`
                        `}`
                        `// take cause for notification if was not in cancelling state before`
`                        state.rootCause.takeIf` `{` `!wasCancelling }`
                    `}`
`                    notifyRootCause?.let` `{` `notifyCancelling(state.list, it)` `}`
                    `return COMPLETING_ALREADY`
                `}`
                `is Incomplete ->` `{`
                    `// Not yet finishing -- try to make it cancelling`
                    `val causeException = causeExceptionCache ?:` `createCauseException(cause).also` `{ causeExceptionCache = it }`
                    `if` `(state.isActive)` `{`
                        `// active state becomes cancelling`
                        `if` `(tryMakeCancelling(state, causeException))` `return COMPLETING_ALREADY`
                    `}` `else` `{`
                        `// non active state starts completing`
                        `val finalState =` `tryMakeCompleting(state,` `CompletedExceptionally(causeException))`
                        `when` `{`
`                            finalState === COMPLETING_ALREADY ->` `error("Cannot happen in $state")`
`                            finalState === COMPLETING_RETRY ->` `return@loopOnState`
                            `else` `->` `return finalState`
                        `}`
                    `}`
                `}`
                `else` `->` `return TOO_LATE_TO_CANCEL // already complete`
            `}`
        `}`
    `}

假如协程在运行中,则执行tryMakeCancelling

复制代码
private` `fun` `tryMakeCancelling(state: Incomplete, rootCause: Throwable): Boolean {`
`        assert { state !is Finishing }` `// only for non-finishing states`
`        assert { state.isActive }` `// only for active states`
        `// get state's list or else promote to list to correctly operate on child lists`
        `val list =` `getOrPromoteCancellingList(state)` `?:` `return` `false`
        `// Create cancelling state (with rootCause!)`
        `val cancelling =` `Finishing(list,` `false, rootCause)`
        `if` `(!_state.compareAndSet(state, cancelling))` `return` `false`
        `// Notify listeners`
        `notifyCancelling(list, rootCause)`
        `return` `true`
    `}

state.compareAndSet进行状态机切换,随后执行notifyCancelling

复制代码
private` `fun` `notifyCancelling(list: NodeList, cause: Throwable)` `{`
        `// first cancel our own children`
        `onCancelling(cause)`
`        list.close(LIST_CANCELLATION_PERMISSION)`
        `notifyHandlers(list, cause)` `{ it.onCancelling }`
        `// then cancel parent`
        `cancelParent(cause)` `// tentative cancellation -- does not matter if there is no parent`
    `}
复制代码
private` `fun` `notifyCancelling(list: NodeList, cause: Throwable)` `{`
        `// first cancel our own children`
        `onCancelling(cause)`
`        list.close(LIST_CANCELLATION_PERMISSION)`
        `notifyHandlers(list, cause)` `{ it.onCancelling }`
        `// then cancel parent`
        `cancelParent(cause)` `// tentative cancellation -- does not matter if there is no parent`
    `}`
    
 `private` `inline` `fun` `notifyHandlers(list: NodeList, cause: Throwable?, predicate:` `(JobNode)` `-> Boolean)` `{`
        `var exception: Throwable?` `=` `null`
`        list.forEach` `{ node ->`
            `if` `(node is JobNode &&` `predicate(node))` `{`
                `try` `{`
`                    node.invoke(cause)`
                `}` `catch` `(ex: Throwable)` `{`
`                    exception?.apply` `{` `addSuppressed(ex)` `}` `?: run {`
`                        exception =` `CompletionHandlerException("Exception in completion handler $node for $this", ex)`
                    `}`
                `}`
            `}`
        `}`
`        exception?.let` `{` `handleOnCompletionException(it)` `}`
    `

node.invoke(cause)的实现

复制代码
private` `class` `InvokeOnCancelling(`
    `private` `val handler: CompletionHandler`
`)` `:` `JobNode()`  `{`
    `// delegate handler shall be invoked at most once, so here is an additional flag`
    `private` `val _invoked =` `atomic(false)`
    `override` `val onCancelling get()` `=` `true`
    `override` `fun` `invoke(cause: Throwable?)` `{`
        `if` `(_invoked.compareAndSet(expect` `=` `false, update =` `true)) handler.invoke(cause)`
    `}`
`}
复制代码
private` `fun` `cancelParent(cause: Throwable): Boolean {`
        `// Is scoped coroutine -- don't propagate, will be rethrown`
        `if` `(isScopedCoroutine)` `return` `true`

        `/* CancellationException is considered "normal" and parent usually is not cancelled when child produces it.`
`         * This allow parent to cancel its children (normally) without being cancelled itself, unless`
`         * child crashes and produce some other exception during its completion.`
`         */`
        `val isCancellation = cause is CancellationException`
        `val parent = parentHandle`
        `// No parent -- ignore CE, report other exceptions.`
        `if` `(parent ===` `null` `|| parent === NonDisposableHandle)` `{`
            `return isCancellation`
        `}`

        `// Notify parent but don't forget to check cancellation`
        `return parent.childCancelled(cause)` `|| isCancellation`
    `}

即先将自己的状态切换到取消中,随后notifyHandlers通过遍历list通知自己的children执行cancel。最后再通过cancelParent告知父Job自己的分支cancel完毕。

总结:

  1. 所有协程都运行在CoroutineScope中,这种限定是通过launch、async、runBlock等构建协程的函数都是作为CoroutineScope扩展函数来实现的。
  2. CoroutineScope创建过程中,必定会构建一个顶层Job(后者外部传入),通过coroutineContext与其关联。
  3. 每个launch都响应构建了一个Job,并将此Job加入到父Job的list中,由此维护了一个Job树。
  4. Structure Concurrency 的 具体实现 是 通过 维护 Job 树 的生命周期 完成 的 。
相关推荐
FunnySaltyFish1 天前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
Kapaseker1 天前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
Kapaseker2 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
A0微声z4 天前
Kotlin Multiplatform (KMP) 中使用 Protobuf
kotlin
alexhilton5 天前
使用FunctionGemma进行设备端函数调用
android·kotlin·android jetpack
lhDream5 天前
Kotlin 开发者必看!JetBrains 开源 LLM 框架 Koog 快速上手指南(含示例)
kotlin
RdoZam5 天前
Android-封装基类Activity\Fragment,从0到1记录
android·kotlin
Kapaseker6 天前
研究表明,开发者对Kotlin集合的了解不到 20%
android·kotlin
糖猫猫cc6 天前
Kite:两种方式实现动态表名
java·kotlin·orm·kite
如此风景6 天前
kotlin协程学习小计
android·kotlin