引言

在 Java 语言中提供了线程中断的能力,但并不是所有的线程都可以中断的,因为 interrupt 方法并不是真正的终止线程,而是将一个标志位标记为中断状态,当运行到下一次中断标志位检查时,才能触发终止线程。

但无论如何,终止线程是一个糟糕的方案,因为在线程的销毁和重建,是要消耗系统资源的,造成了不必要的开销。Kotlin 协程提供了更优雅的取消机制,这也是协程比较核心的功能之一。

协程的状态

在了解取消机制之前我们需要知道一些关于 Job 状态的内容:

State isActive(是否活跃) isCompleted(是否完成) isCancelled(是否取消)
New (可选初始状态) false false false
Active (默认初始状态) true false false
Completing (短暂态) true false false
Cancelling (短暂态) false false true
Cancelled (完成态) false true true
Completed (完成态) false true false

可以看出,在完成和取消的过程中,会经过一个短暂的进行中的状态,然后才变成已完成/已取消。

在这里只关注一下取消相关的状态:

  • Cancelling
  • 抛出异常的 Job 会导致其进入 Cancelling 状态,也可以使用 cancel 方法来随时取消 Job 使其立即转换为 Cancelling 状态。
  • Cancelled
  • 当它递归取消子项,并等待所有的子项都取消后,该 Job 会进入 Cancelled 状态。

取消协程的用法

协程在代码中抽象的类型是 Job , 下面是一个官方的代码示例,用来展示如何取消协程的执行:

suspend fun main(): Unit = coroutineScope {
    val job = launch {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancel() // cancels the job
    job.join() // waits for job's completion 
    println("main: Now I can quit.")
}

它的输出是:

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.

一旦 mian 方法中调用了 job.cancel() ,我们就看不到其他协程的任何输出,因为它已被取消了。

协程取消的有效性

协程代码必须通过与挂起函数的配合才能被取消。kotlinx.coroutines 中所有挂起函数(带有 suspend 关键字函数)都是可以被取消的。suspend 函数会检查协程是否需要取消并在取消时抛出 CancellationException

但是,如果协程在运行过程中没有挂起点,则不能取消协程,如下例所示:

suspend fun main(): Unit = coroutineScope {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // computation loop, just wastes CPU
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i  } ...")
                nextPrintTime  = 500L
            }
        }
    }
    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.")
}

在这个 job 中,并没有执行任何 suspend 函数,所以在执行过程中并没有对协程是否需要取消进行检查,自然也就无法触发取消。

同样的问题也可以在通过 捕获 CancellationException 并且不抛出的情况下 观察到:

suspend fun main(): Unit = coroutineScope {
    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.")
}

打印结果是:

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@614acfe9
job: I'm sleeping 3 ...
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@614acfe9
job: I'm sleeping 4 ...
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@614acfe9
main: Now I can quit.

从打印结果来看,循环 5 次全部执行了,好像取消并没有起到作用。但实际上不是这样的,为了便于观察加上时间戳:

1665217217682: job: I'm sleeping 0 ...
1665217218196: job: I'm sleeping 1 ...
1665217218697: job: I'm sleeping 2 ...
1665217218996: main: I'm tired of waiting!
1665217219000: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@3a1efc0d
1665217219000: job: I'm sleeping 3 ...
1665217219000: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@3a1efc0d
1665217219000: job: I'm sleeping 4 ...
1665217219000: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@3a1efc0d
1665217219001: main: Now I can quit.

加上时间可以看出,抛出第一次异常后的两次循环和异常捕获都是在同一瞬间完成的。这说明了捕获到异常后,仍然会执行代码,但是所有的 delay 方法都没有生效,即该 Job 的所有子 Job 都失效了。但该 Job 仍在继续循环打印。原因是,父 Job 会等所有子 Job 处理结束后才能完成取消。

而如果我们不使用 try-catch 呢?

suspend fun main(): Unit = coroutineScope {
    val job = launch(Dispatchers.Default) {
        repeat(5) { i ->
            // print a message twice a second
            println("job: I'm sleeping $i ...")
            delay(500)
        }
    }
    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.")
}

打印结果:

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.

很顺利的取消了,这是因为协程抛出 Exception 直接终止了。

注意协程抛出 CancellationException 并不会导致 App Crash 。

使用 try-catch 来捕获 CancellationException 时需要注意,在挂起函数前的代码逻辑仍会多次执行,从而导致这部分代码仿佛没有被取消一样。

如何写出可以取消的代码

有两种方法可以使代码是可取消的。第一种方法是定期调用挂起函数,检查是否取消,就是上面的例子中的方法;另一个是显式检查取消状态:

suspend fun main(): Unit = coroutineScope {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) { // cancellable computation loop
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i  } ...")
                nextPrintTime  = 500L
            }
        }
    }
    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.")
}

将上面的循环 5 次通过使用 while (isActive) 进行替换,实现显示检查取消的代码。isActive 是通过 CoroutineScope 对象在协程内部可用的扩展属性。

在 finally 中释放资源

在前面的例子中我们使用 try-catch 捕获 CancellationException 发现会产生父协程等待所有子协程完成后才能完成,所以建议不用 try-catch 而是 try{…} finally{…} ,让父协程在被取消时正常执行终结操作:

val job = launch {
    try {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    } finally {
        println("job: I'm running finally")
    }
}
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.")

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.

使用不可取消的 block

如果在在上面的示例的 finally 代码块中使用 suspend 函数,会导致抛出 CancellationException 。

因为运行这些代码的协程已经被取消了。通常情况下这不会有任何问题,然而,在极少数情况下,如果你需要在 finally 中使用一个挂起函数,你可以通过使用 withContext(NonCancellable) { ... }

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) // 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.")

CancellationException

在上面的内容中,我们知道协程的取消是通过抛出 CancellationException 来进行的,神奇的是抛出 Exception 并没有导致应用程序 Crash 。

CancellationException 的真实实现是 j.u.c. 中的 CancellationException :

public actual typealias CancellationException = java.util.concurrent.CancellationException

如果协程的 Job 被取消,则由可取消的挂起函数抛出 CancellationException 。它表示协程的正常取消。在默认的 CoroutineExceptionHandler 下,它不会打印到控制台/日志。

上面引用了这个类的注释,看来处理抛出异常的逻辑在 CoroutineExceptionHandler 中:

public interface CoroutineExceptionHandler : CoroutineContext.Element {
    /**
     * Key for [CoroutineExceptionHandler] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
    /**
     * Handles uncaught [exception] in the given [context]. It is invoked
     * if coroutine has an uncaught exception.
     */
    public fun handleException(context: CoroutineContext, exception: Throwable)
}

通常,未捕获的 Exception 只能由使用协程构建器的根协程产生。所有子协程都将异常的处理委托给他们的父协程,父协程也委托给它自身的父协程,直到委托给根协程处理。所以在子协程中的 CoroutineExceptionHandler 永远不会被使用。

使用 SupervisorJob 运行的协程不会将异常传递给它们的父协程,SupervisorJob 被视为根协程。

使用 async 创建的协程总是捕获它的所有异常通过结果 Deferred 对象回调出去,因此它不能导致未捕获的异常。

CoroutineExceptionHandler 用于记录异常、显示某种类型的错误消息、终止和/或重新启动应用程序。

如果需要在代码的特定部分处理异常,建议在协程中的相应代码周围使用 try-catch。通过这种方式,您可以阻止异常协程的完成(异常现在被捕获),重试操作,和/或采取其他任意操作。 这也就是我们前面论证的在协程中使用 try-catch 导致的取消失效。

默认情况下,如果协程没有配置用于处理异常的 Handler ,未捕获的异常将按以下方式处理:

如果 exception 是 CancellationException ,那么它将被忽略(因为这是取消正在运行的协程的假定机制)。

其他情况:

  • 如果上下文中有一个 Job,那么调用 job.cancel()
  • 否则,通过 ServiceLoader 找到的 CoroutineExceptionHandler 的所有实例并调用当前线程的 Thread.uncaughtExceptionHandler 来处理异常。

超时取消

取消协程执行的最合适的应用场景是它的执行时间超过了规定的最大时间时自动取消任务。在 Kotlin 协程库中提供了 withTimeout 方法来实现这个功能:

withTimeout(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
}

执行结果:

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

TimeoutCancellationException 是 CancellationException 的子类,TimeoutCancellationException 通过 withTimeout 函数抛出。

在本例中,我们在main函数中使用了withTimeout ,运行过程中会导致 Crash 。

有两种解决办法,就是使用 try{…} catch (e: TimeoutCancellationException){…} 代码块;另一种办法是使用在超时的情况下不是抛出异常而是返回 null 的 withTimeoutOrNull 函数:

val result = withTimeoutOrNull(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
    "Done" // will get cancelled before it produces this result
}
println("Result is $result")

打印结果:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null

异步的超时和资源

withTimeout 中的超时事件相对于在其块中运行的代码是异步的,并且可能在任何时间发生,甚至在从超时块内部返回之前。如果你在块内部打开或获取一些资源,需要关闭或释放到块外部。

例如,在这里,我们用 Resource 类模拟一个可关闭资源,它只是通过对获得的计数器递增,并对该计数器从其关闭函数递减来跟踪创建次数。让我们用小超时运行大量的协程,尝试在一段延迟后从withTimeout块内部获取这个资源,并从外部释放它。

var acquired = 0
class Resource {
    init { acquired   } // Acquire the resource
    fun close() { acquired-- } // Release the resource
}
fun main() {
    runBlocking {
        repeat(100_000) { // Launch 100K 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
}

如果运行上面的代码,您将看到它并不总是打印 0,尽管它可能取决于您的机器的时间,在本例中您可能需要调整超时以实际看到非零值。

要解决这个问题,可以在变量中存储对资源的引用,而不是从withTimeout块返回它。

fun main() {
    runBlocking {
        repeat(100_000) { // Launch 100K 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
}

这样这个例子总是输出0。资源不会泄漏。

取消检查的底层原理

在探索协程取消的有效性时,我们知道协程代码必须通过与挂起函数的配合才能被取消。

kotlinx.coroutines 中所有挂起函数(带有 suspend 关键字函数)都是可以被取消的。suspend 函数会检查协程是否需要取消并在取消时抛出 CancellationException 。

关于协程的取消机制,很明显和 suspend 关键字有关。为了测试 suspend 关键字的作用,实现下面的代码:

class Solution {
    suspend fun func(): String {
        return "测试 suspend 关键字"
    }
}

作为对照组,另一个是不加 suspend 关键字的 func 方法:

class Solution {
    fun func(): String {
        return "测试 suspend 关键字"
    }
}

两者反编译成 Java :

// 普通的方法
public final class Solution {
    public static final int $stable = LiveLiterals$SolutionKt.INSTANCE.Int$class-Solution();
    @NotNull
    public final String func() {
        return LiveLiterals$SolutionKt.INSTANCE.String$fun-func$class-Solution();
    }
}
// 带有 suspend 关键字的方法
public final class Solution {
    public static final int $stable = LiveLiterals$SolutionKt.INSTANCE.Int$class-Solution();
    @Nullable
    public final Object func(@NotNull Continuation<? super String> $completion) {
        return LiveLiterals$SolutionKt.INSTANCE.String$fun-func$class-Solution();
    }
}

suspend 关键字修饰的方法反编译后默认生成了带有 Continuation 参数的方法。说明 suspend 关键字的玄机在 Continuation 类中。

Continuation 是 Kotlin 协程的核心思想 Continuation-Passing Style 的实现。原理参考简述协程的底层实现原理 。

通过在普通函数的参数中增加一个 Continuation 参数,这个 continuation 的性质类似于一个 lambda 对象,将方法的返回值类型传递到这个 lambda 代码块中。

什么意思呢?就是本来这个方法的返回类型直接 return 出来的:

val a: String = func()
print(a)

而经过 suspend 修饰,代码变成了这个样子:

func { a ->
    print(a)
}

Kotlin 协程就是通过这样的包装,将比如 launch 方法,实际上是 launch 最后一个参数接收的是 lambda 参数。也就是把外部逻辑传递给函数内部执行。

回过头来再来理解 suspend 关键字,我们知道带有 suspend 关键字的方法会对协程的取消进行检查,从而取消协程的执行。从这个能力上来看,我理解他应该会自动生成类似下面的逻辑代码:

生成的函数 {
    if(!当前协程.isActive) {
        throw CancellationException()
    }
    // ... 这里是函数真实逻辑
}

suspend 修饰的函数,会自动生成一个挂起点,来检查协程是否应该被挂起。

显然 Continuation 中声明的函数也证实了挂起的功能:

public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext
    /**
     * 恢复相应协程的执行,将成功或失败的结果作为最后一个挂起点的返回值传递。
     */
    public fun resumeWith(result: Result<T>)
}

协程本质上是产生了一个 switch 语句,每个挂起点之间的逻辑都是一个 case 分支的逻辑。参考 协程是如何实现的 中的例子:

        Function1 lambda = (Function1)(new Function1((Continuation)null) {
            int label;
            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
                byte text;
                @BlockTag1: {
                    Object result;
                    @BlockTag2: {
                        result = IntrinsicsKt.getCOROUTINE_SUSPENDED();
                        switch(this.label) {
                            case 0:
                                ResultKt.throwOnFailure($result);
                                this.label = 1;
                                if (SuspendTestKt.dummy(this) == result) {
                                    return result;
                                }
                                break;
                            case 1:
                                ResultKt.throwOnFailure($result);
                                break;
                            case 2:
                                ResultKt.throwOnFailure($result);
                                break @BlockTag2;
                            case 3:
                                ResultKt.throwOnFailure($result);
                                break @BlockTag1;
                            default:
                                throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
                        }
                        text = 1;
                        System.out.println(text);
                        this.label = 2;
                        if (SuspendTestKt.dummy(this) == result) {
                            return result;
                        }
                    }
                    text = 2;
                    System.out.println(text);
                    this.label = 3;
                    if (SuspendTestKt.dummy(this) == result) {
                        return result;
                    }
                }
                text = 3;
                System.out.println(text);
                return Unit.INSTANCE;
            }
            @NotNull
            public final Continuation create(@NotNull Continuation completion) {
                Intrinsics.checkNotNullParameter(completion, "completion");
                Function1 funcation = new <anonymous constructor>(completion);
                return funcation;
            }
            public final Object invoke(Object object) {
                return ((<undefinedtype>)this.create((Continuation)object)).invokeSuspend(Unit.INSTANCE);
            }
        });

可以看出,在每个分支都会执行一次 ResultKt.throwOnFailure($result); ,从名字上就知道,这就是检查是否需要取消并抛出异常的代码所在:

@PublishedApi
@SinceKotlin("1.3")
internal fun Result<*>.throwOnFailure() {
    if (value is Result.Failure) throw value.exception
}

这里的 Result 类是一个包装类,它将成功的结果封装为类型 T 的值,或将失败的结果封装为带有任意Throwable异常的值。

        @Suppress("INAPPLICABLE_JVM_NAME")
        @InlineOnly
        @JvmName("success")
        public inline fun <T> success(value: T): Result<T> =
            Result(value)
        /**
         * Returns an instance that encapsulates the given [Throwable] [exception] as failure.
         */
        @Suppress("INAPPLICABLE_JVM_NAME")
        @InlineOnly
        @JvmName("failure")
        public inline fun <T> failure(exception: Throwable): Result<T> =
            Result(createFailure(exception))

成功和失败的方法类型是不一样的,证实了这一点,success 方法接收类型为 T 的参数;failure 接收 Throwable 类型的参数。

到这里 suspend 方法挂起的原理就明了了:在协程的状态机中,通过挂起点会分割出不同的状态,对每一个状态,会先进行挂起结果的检查。 这会导致以下结果:

  • 协程的取消机制是通过挂起函数的挂起点检查来进行取消检查的。证实了为什么如果没有 suspend 函数(本质是挂起点),协程的取消就不会生效。
  • 协程的取消机制是需要函数合作的,就是通过 suspend 函数来增加取消检查的时机。
  • 父协程会执行完所有的子协程(挂起函数),因为代码的本质是一个循环执行 switch 语句,当一个子协程(或挂起函数)执行结束,会继续执行到下一个分支。但是最后一个挂起点后续的代码并不会被执行,因为最后一个挂起点检查到失败,不会继续跳到最后的 label 分支。

以上就是Kotlin 协程的取消机制详细解读的详细内容,更多关于Kotlin 协程取消机制的资料请关注Devmax其它相关文章!

Kotlin 协程的取消机制详细解读的更多相关文章

  1. Kotlin难点解析:extension和this指针

    扩展是Kotlin语言中使用非常简单的一个特性。关于这个问题,其实我之前的一篇文章[[Kotlin]LambdaandExtension](https://www.jianshu.com/p/d7a...中有提到过。为了解决这个问题,官方提出了两个新的概念:dispatchreceiver和extensionreceiver。extensionreceiver:中文翻译为扩展接收者。为了简化,这里我们将dispatchreceiver简称为DR,将extensionreceiver简称为ER。如果你习惯了

  2. android – Kotlin类NoClassDefFoundError崩溃

    我有一个使用以下库的现有Android项目:>Autovalue>Dagger2>RxJava>Retrolambda我正在尝试添加Kotlin支持,以便我可以将项目慢慢迁移到Kotlin.这就是我所做的.>添加了Kotlin依赖.>将其中一个类转换为Kt类并转移到src/main/kotlin/..包中.>在源集中添加了kotlin.sourceSets{main.java.srcDirs=’s

  3. android – Kotlin和Dagger2

    我正在尝试将Kotlin添加到我的项目中,但在启用Kotlin之后我无法构建,因为Dagger2类不再生成.我尝试了第二个项目,我有同样的问题.这些是我为启用Kotlin所做的改变:项目build.gradle:Appbuild.gradle:错误发生在这里:其中不再定义DaggerObjectGraph.任何帮助将不胜感激.解决方法只需删除

  4. android – 在Kotlin中不能使用argb color int值吗?

    当我想在Kotlin中为TextView的textColor设置动画时:发生此错误:似乎在Kotlin中不能将值0xFF8363FF和0xFFC953BE强制转换为Int,但是,它在Java中是正常的:有任何想法吗?提前致谢.解决方法0xFF8363FF是Long,而不是Int.你必须明确地将它们转换为Int:关键是0xFFC953BE的数值是4291384254,因此它应该存储在Long变量中.但这里的高位是符号位,表示负数:-3583042,可以存储在Int中.这就是两种语言之间的区别.在Kotlin

  5. 什么是我可以使用Kotlin的最早的Android API级别?

    我认为这个问题很清楚但是我能在Kotlin上定位的最早API级别是什么?解决方法实际上,任何API级别.这是因为Kotlin被编译为JVM6平台的字节码,所有AndroidAPI级别都支持该字节码.因此,除非您在Kotlin代码中使用任何较新的AndroidAPI,否则它不需要任何特定的API级别.

  6. android – Kotlin数据类和可空类型

    我是Kotlin的新手,我不知道为什么编译器会抱怨这段代码:编译器抱怨测试?.data.length,它说我应该这样做:test?.length.但是数据变量是String,而不是String?,所以我不明白为什么我要把它?当我想检查长度.解决方法表达式test?.data部分可以为空:它是test.data或null.因此,获取其长度并不是零安全的,而是应该再次使用safecalloperator:test?.length.可空性通过整个调用链传播:你必须将这些链写成?.)).e),因为,如果其中一个左

  7. android – Kotlin自定义获取执行方法调用

    像这样的东西:仍在使用Kotlin并且不确定get()方法是否会引用编辑器而不是创建新的编辑器.解决方法第二个属性声明适合您的需要:它有一个customgetter,因此获取属性值将始终执行getter,并且不存储该值.你可能会被等号get()=…

  8. android – Kotlin合成扩展和几个包含相同的布局

    我找了一些这样的:我在Studio中看到我可以访问dayName但是dayNameTextView引用了哪一个?正常,如果我只有一个包含的布局,它工作正常.但现在我有多次包含相同的布局.我当然可以这样做:但我正在寻找好的解决方案.版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  9. android – java.lang.IllegalArgumentException:指定为非null的参数为null:方法kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull

    我收到了这个错误java.lang.IllegalArgumentException:指定为非null的参数为null:方法kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull,参数事件为线覆盖funonEditorAction(v:TextView,actionId:Int,event:KeyEvent)以下是整个代码.这段代码最初是在ja

  10. android – Kotlin:如何访问CustomView的Attrs

    我在Kotlin中创建了一个自定义视图,并希望访问它的属性资源.以下是我的代码请注意,这将在init函数的attrs中出错.我想知道如何进入attrs?

随机推荐

  1. Flutter 网络请求框架封装详解

    这篇文章主要介绍了Flutter 网络请求框架封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Android单选按钮RadioButton的使用详解

    今天小编就为大家分享一篇关于Android单选按钮RadioButton的使用详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

  3. 解决android studio 打包发现generate signed apk 消失不见问题

    这篇文章主要介绍了解决android studio 打包发现generate signed apk 消失不见问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  4. Android 实现自定义圆形listview功能的实例代码

    这篇文章主要介绍了Android 实现自定义圆形listview功能的实例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. 详解Android studio 动态fragment的用法

    这篇文章主要介绍了Android studio 动态fragment的用法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. Android用RecyclerView实现图标拖拽排序以及增删管理

    这篇文章主要介绍了Android用RecyclerView实现图标拖拽排序以及增删管理的方法,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下

  7. Android notifyDataSetChanged() 动态更新ListView案例详解

    这篇文章主要介绍了Android notifyDataSetChanged() 动态更新ListView案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下

  8. Android自定义View实现弹幕效果

    这篇文章主要为大家详细介绍了Android自定义View实现弹幕效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  9. Android自定义View实现跟随手指移动

    这篇文章主要为大家详细介绍了Android自定义View实现跟随手指移动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Android实现多点触摸操作

    这篇文章主要介绍了Android实现多点触摸操作,实现图片的放大、缩小和旋转等处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部