kotlin 协程上下文异常处理详解
作者:aruba 发布时间:2022-09-30 06:32:52
引言
从前面我们可以大致了解了协程的玩法,如果一个协程中使用子协程,那么该协程会等待子协程执行结束后才真正退出,而达到这种效果的原因就是协程上下文,上下文贯穿了协程的生命周期,这套思想和我们app的上下文很像
在开始真正了解协程上下文之前,我们先来看看下面的例子
下面的图代表了一个协程a的生命,就像一条从上至下的直线,它的生命只有100ms
当我们在a协程延迟函数100ms之前开启一个子协程b,b做了200ms的事情,如果不考虑调度消耗的时间,那么a协程的生命也会延长成200ms
代码验证下:
fun `test context life`() = runBlocking {
//定义一个作用域
val a = CoroutineScope(Dispatchers.Default)
val startTime = System.currentTimeMillis()
//协程a开启
val jobA = a.launch {
//子协程b开启
val jobB = launch {
delay(200)
}
delay(100)
}
//等待协程a结束
jobA.join()
val endTime = System.currentTimeMillis()
println(endTime - startTime)
}
fun main() {
`test context life`()
}
结果:237
如果我们把子协程b增加到delay 300ms,那么结果也会相应的变为:
323
通过上面的列子,来对协程上下文的有一个初步概念:可以说协程的生命周期,就是上下文的生命周期
协程拥有很多新的概念,很多人一开始接触就能难理解(包括我自己),这些概念都是在上下文的基础上引申而来的,所以我一再强调它的重要性,协程的上下文必须理解透,才能玩好协程,接下来我们来真正了解协程上下文
一、协程上下文
1.CoroutineContext
协程上下文有以下几项构成,它们都是实现了CoroutineContext.Element接口,有些是实现了AbstractCoroutineContextElement接口,而AbstractCoroutineContextElement继承CoroutineContext.Element接口
1.Job:控制协程的生命周期,也是我们能拿到操作协程任务的唯一对象
2.CoroutineDispatcher:就是之前介绍的调度器
3.CoroutineName:协程的名字,一般输出日志用的
4.CoroutineExceptionHandler:处理未捕获的异常
协程上下文实现了运算符重载,我们可以用+号来组合一个CoroutineContext的元素
2.CorountineScope
一般情况下,协程体内所有的子协程,都继承至根协程,协程的继承的关系不是我们所了解的类的继承关系,而是父协程和子协程的生命周期关系,还记得我们上面举得例子么,除非在协程体内自己手动创建协程作用域,即:创建一个全新的协程上下文,我们之前已经介绍过了:
CorountineScope:创建协程作用域,新起线程,观察源码,内部实际实例化的是ContextScope,ContextScope被internal修饰,内部使用,我们实例化不了
其他的实际上都是继承父协程上下文,或者内部实例化了ContextScope:
1.runBlocking:将主线程转变为协程,会阻塞主线程,实际上用的是一个EmptyCoroutineContext作为上下文,它是一个主线程的协程上下文,静态的全局变量,我们其实就可以理解成是主线程
2.GlobalScope:也是用的EmptyCoroutineContext
3.MainScope:使用ContextScope构造了新的上下文
4.coroutineScope:继承的父协程上下文,不能算是全新的协程
等等
3.子协程继承父协程
子协程继承父协程时,除了Job会自动创建新的实例外,其他3项的不手动指定的话,都会自动继承父协程的,Job对应的是协程任务,每次新的任务肯定都是新的Job对象
有了这些概念后,接下来通过代码,再熟悉巩固下
例子1:
fun `test context life1`() = runBlocking {
//定义一个作用域
val a = CoroutineScope(Dispatchers.Default)
//协程a开启
val jobA = a.launch {
delay(100)
println("jobA finished")
}
println("main finished")
}
结果:
main finished
由于a是一个根协程,全新的上下文,runBlocking 是主线程的协程上下文,所以当a开启任务时,不会阻塞主线程,当我们的进程都跑完了,jobA finished肯定不会打印了
例子2:
fun `test context life2`() = runBlocking {
//定义一个作用域
val a = CoroutineScope(Dispatchers.Default)
//协程a开启
val jobA = a.launch {
delay(100)
println("jobA finished")
}
jobA.join()
println("main finished")
}
结果:
jobA finished
main finished
我们在主协程(主线程的协程)中,手动调用jobA的join方法,那么主线程就会阻塞,直到jobA执行完毕。这个和我们的多线程操作是一样的,主线程等待A线程执行完后再往后执行
例子3:
fun `test context life3`() = runBlocking {
launch {
delay(100)
println("jobA finished")
}
println("main finished")
}
结果:
main finished
jobA finished
这回我们没有构建新的协程作用域,而是在根协程中直接使用子协程的方式,当然了,协程的上下文继承关系,使得我们的主协程等待子协程执行完毕后才结束生命
例子4:
fun `test context life4`() = runBlocking {
launch(Dispatchers.IO + CoroutineName("jobA")) {
delay(100)
println("${coroutineContext[CoroutineName]} finished")
}
println("main finished")
}
结果:
main finished
CoroutineName(jobA) finished
即使我们指定了子协程的调度器和协程名,也不会影响协程上下文继承关系,主协程还是会等待子协程执行完毕后才结束生命
如果你已经完全理解了,那么就可以知道以上例子使用async启动也是一样的效果
二、协程的异常传递
1.协程的异常传播
协程的异常传播也是遵循了协程上下文的机制,除了取消异常(CancellationException)外,当一个协程有了异常,如果没有主动捕获异常,那么异常会向上传播,直到根协程,子协程的异常都会导致根协程退出,自然其他子协程也会退出
例子1:
fun `test coroutineScope exception1`() = runBlocking {
val job1 = launch {
delay(2000)
println("job finished")
}
val job2 = launch {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
delay(3000)
println("finished")
}
结果:
job2 finished
Exception in thread "main" java.lang.IllegalArgumentException
at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception1$1$job2$1.invokeSuspend(exceptionTest.kt:46)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.ResumeModeKt.resumeMode(ResumeMode.kt:67)
at kotlinx.coroutines.DispatchedKt.resume(Dispatched.kt:309)
at kotlinx.coroutines.DispatchedKt.dispatch(Dispatched.kt:298)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:250)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:260)
at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:332)
at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.kt:298)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.kt:116)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:80)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:54)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:36)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt.test coroutineScope exception1(exceptionTest.kt:37)
at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt.main(exceptionTest.kt:54)
at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt.main(exceptionTest.kt)Process finished with exit code 1
job2 1000ms后就发生了异常,导致job1和父协程都直接退出
2.不同上下文(没有继承关系)之间协程异常会怎么样?
例子1:
fun `test coroutineScope exception2`() = runBlocking {
val job1 = launch {
delay(2000)
println("job finished")
}
val job2 = CoroutineScope(Dispatchers.IO).launch{
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
println("new CoroutineScope finished")
}
delay(3000)
println("finished")
}
结果:
job2 finished
Exception in thread "DefaultDispatcher-worker-2" java.lang.IllegalArgumentException
at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception1$1$job2$1.invokeSuspend(exceptionTest.kt:46)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
job finished
finishedProcess finished with exit code 0
可以看出不同根协程的协程之间,异常并不会自动传递,我们的主线程上下文协程正常执行
再看例子2:
fun `test coroutineScope exception3`() = runBlocking {
val job1 = launch {
delay(2000)
println("job finished")
}
val job2 = CoroutineScope(Dispatchers.IO).async{
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
println("new CoroutineScope finished")
}
delay(3000)
println("finished")
}
结果:
job2 finished
job finished
finished
和例子1的唯一区别是,使用了全新上下文的协程使用了async启动,哈哈,这就奇怪了,为什么会这样?
3.向用户暴露异常
还记得async启动的协程返回的是一个Deferred么,它可以使用await函数,来获取协程运行结果。那么试想一下,如果我就是想要一个协程执行完返回一个异常呢?
所以async中的异常会作为返回值,返回给调用await函数
fun `test coroutineScope exception4`() = runBlocking {
val job1 = launch {
delay(2000)
println("job finished")
}
val job2 = CoroutineScope(Dispatchers.IO).async{
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
println("new CoroutineScope finished")
}
job2.await()
delay(3000)
println("finished")
}
结果:
job2 finished
Exception in thread "main" java.lang.IllegalArgumentException
at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception4$1$job2$1.invokeSuspend(exceptionTest.kt:96)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)Process finished with exit code 1
await的时候出现异常了,当然会导致协程退出,我们可以在await的时候捕获下这个异常,就不会影响主线程上下文的协程运行了
fun `test coroutineScope exception4`() = runBlocking {
val job1 = launch {
delay(2000)
println("job finished")
}
val job2 = CoroutineScope(Dispatchers.IO).async {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
println("new CoroutineScope finished")
}
try {
job2.await()
} catch (e: Exception) {
e.printStackTrace()
}
delay(3000)
println("finished")
}
结果:
job2 finished
java.lang.IllegalArgumentException
at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception4$1$job2$1.invokeSuspend(exceptionTest.kt:96)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
job finished
finishedProcess finished with exit code 0
值得注意的是,同一继承关系下的协程使用await并无法捕获异常,还是会遵循第一条,导致整个协程生命周期结束
fun `test coroutineScope exception5`() = runBlocking {
val job2 = CoroutineScope(Dispatchers.IO).launch {
val job1 = launch {
delay(2000)
println("job finished")
}
val job3 = async {
delay(1000)
println("job3 finished")
throw IllegalArgumentException()
}
try {
job3.await()
} catch (e: Exception) {
e.printStackTrace()
}
delay(2000)
println("job2 finished")
}
job2.join()
println("finished")
}
结果:
job3 finished
java.lang.IllegalArgumentException
at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception5$1$job2$1$job3$1.invokeSuspend(exceptionTest.kt:119)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
Exception in thread "DefaultDispatcher-worker-1" java.lang.IllegalArgumentException
at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception5$1$job2$1$job3$1.invokeSuspend(exceptionTest.kt:119)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
finishedProcess finished with exit code 0
可以发现job3.await()的try catch并没有生效,所以向用户暴露异常只适用于不同上下文(没有继承关系)的协程
三、协程的异常处理
使用SupervisorJob
如果想要一个协程出现异常后,不影响其继承关系中的其他协程,可以使用SupervisorJob
fun `test SupervisorJob exception`() = runBlocking {
val job1 = launch {
delay(2000)
println("job finished")
}
val job2 = async(SupervisorJob()) {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
delay(3000)
println("finished")
}
结果:
job2 finished
job finished
finished
可以看到,job2的异常并没有影响其他继承关系的协程的执行
SupervisorScope,这个我们前面已经用过了,就不重复介绍了
异常捕获器CoroutineExceptionHandler
协程上下文的4项之一,可以用CrashHandler理解,不过它并不能阻止协程的退出,只能够获取异常的信息
它使用有两个条件:
1.异常是自动抛出异常(launch)
2.实例化CoroutineScope的时候指定异常捕获器 或者 在一个根协程中
例子1:
fun `test SupervisorHandler exception1`() = runBlocking {
val handler = CoroutineExceptionHandler { _, throwable ->
println("caught: $throwable")
}
val scope = CoroutineScope(handler)
val job1 = scope.launch {
val job2 = launch {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
delay(2000)
println("job finished")
}
delay(4000)
println("finished")
}
结果:
job2 finished
caught: java.lang.IllegalArgumentException
finished
job2抛出了异常,被捕获到了,但是scope的其他协程随之生命周期也都结束了
例子2:
fun `test SupervisorHandler exception2`() = runBlocking {
val handler = CoroutineExceptionHandler { _, throwable ->
println("caught: $throwable")
}
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch(handler) {
val job2 = launch {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
delay(2000)
println("job finished")
}
delay(4000)
println("finished")
}
结果:
job2 finished
caught: java.lang.IllegalArgumentException
finished
和例子1相同,因为我们handler指定在了根协程
例子3:
fun `test SupervisorHandler exception3`() = runBlocking {
val handler = CoroutineExceptionHandler { _, throwable ->
println("caught: $throwable")
}
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch {
val job2 = launch(handler) {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
delay(2000)
println("job finished")
}
delay(4000)
println("finished")
}
结果:
job2 finished
Exception in thread "DefaultDispatcher-worker-4" java.lang.IllegalArgumentException
at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test SupervisorHandler exception$1$job1$1$job2$1.invokeSuspend(exceptionTest.kt:161)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
finishedProcess finished with exit code 0
handler不是在根协程中,不能捕获
如果一个子协程会抛出异常,那么对它进行等待时(join或await),包裹一层try catch 会出现意料之外的事
例子4:
fun `test SupervisorHandler exception4`() = runBlocking {
val handler = CoroutineExceptionHandler { _, throwable ->
println("caught: $throwable")
}
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch(handler) {
val job2 = launch {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
try {
job2.join()
}catch (e:Exception){
}
// val job3 = scope.launch {
// println("job3 finished")
// }
println("job delay")
delay(2000)
for(i in 0..10){
println(i)
}
println("job finished")
}
delay(4000)
println("finished")
}
结果:
job2 finished
job delay
caught: java.lang.IllegalArgumentException
finished
如果把scope根协程中的delay函数注释掉,会怎么样呢?
fun `test SupervisorHandler exception4`() = runBlocking {
val handler = CoroutineExceptionHandler { _, throwable ->
println("caught: $throwable")
}
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch(handler) {
val job2 = launch {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
try {
job2.join()
}catch (e:Exception){
}
// val job3 = scope.launch {
// println("job3 finished")
// }
println("job delay")
// delay(2000)
for(i in 0..10){
println(i)
}
println("job finished")
}
delay(4000)
println("finished")
}
结果:
job2 finished
job delay
0
1
2
3
4
5
6
7
8
9
10
job finished
caught: java.lang.IllegalArgumentException
如果不包裹try catch 那么println("job delay")都不会执行
由例子4和例子5,我们可以推断,如果子协程有异常发生了,我们在等待时捕获异常后,根协程执行了挂起函数,那么它会直接中断,不执行挂起函数以下的代码,如果没有挂起函数,那么后面的代码还是会执行
为了加强验证这点,我们使用Thread.sleep(2000)替换delay函数测试下:
fun `test SupervisorHandler exception4`() = runBlocking {
val handler = CoroutineExceptionHandler { _, throwable ->
println("caught: $throwable")
}
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch(handler) {
val job2 = launch {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
try {
job2.join()
}catch (e:Exception){
}
// val job3 = scope.launch {
// println("job3 finished")
// }
println("job delay")
// delay(2000)
Thread.sleep(2000)
for(i in 0..10){
println(i)
}
println("job finished")
}
delay(4000)
println("finished")
}
结果还是和例子5一样:
job2 finished
job delay
0
1
2
3
4
5
6
7
8
9
10
job finished
caught: java.lang.IllegalArgumentException
finished
Process finished with exit code 0
其实出现这个情况,和我们之前取消协程是一样的,出现异常后会开始取消协程,但是CPU密集型的代码还会执行,但是遇到挂起函数就会抛一个CancellationException,导致协程结束运行,如果我们在挂起函数加上try catch打印,那么我们就可以看到CancellationException了
例子6,把job3的注释放开:
fun `test SupervisorHandler exception4`() = runBlocking {
val handler = CoroutineExceptionHandler { _, throwable ->
println("caught: $throwable")
}
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch(handler) {
val job2 = launch {
delay(1000)
println("job2 finished")
throw IllegalArgumentException()
}
try {
job2.join()
}catch (e:Exception){
}
val job3 = scope.launch {
println("job3 finished")
}
println("job delay")
delay(2000)
// Thread.sleep(2000)
for(i in 0..10){
println(i)
}
println("job finished")
}
delay(4000)
println("finished")
}
结果:
job2 finished
job delay
caught: java.lang.IllegalArgumentException
Exception in thread "DefaultDispatcher-worker-1" java.lang.IllegalArgumentException
at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test SupervisorHandler exception4$1$job1$1$job2$1.invokeSuspend(exceptionTest.kt:227)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
finishedProcess finished with exit code 0
显然有异常没有被捕获,很明显这个异常是调用job3时输出的,由此又可以推断出,如果在等待任务结束时,任务出现异常并且手动捕获异常后,再启动子协程时,也会抛出异常,并且不可捕获
注意:新版本kotlin已修复这个bug,不会抛出异常了
Android中全局异常的处理
最后,感谢动脑学院Jason老师出的kotlin协程教程,得到了很多理解和启发
来源:https://www.jianshu.com/p/a933a2d980c7


猜你喜欢
- 最近用到的一个日历控件,记录下,效果如图代码下载地址:点击打开链接布局文件<LinearLayout xmlns:android=&q
- 一.线程池简介线程池的概念线程池就是首先创建一些线程,它们的集合称为线程池,使用线程池可以很好的提高性能,线程池在系统启动时既创建大量空闲的
- Maven使用说明及规范此文档主要说明Maven的基础使用方式,以及在使用过程过程中需要遵守哪些默认的准则。我们工作中会经常写maven的配
- 1. 使用try-with-resources简化文件读取操作:修改前:FileInputStream fis = null;try { &
- 本文实例讲述了Android开发之绘制平面上的多边形功能。分享给大家供大家参考,具体如下:计算机里的3D图形其实是由很多个平面组合而成的。所
- Looper是什么用于为线程运行消息循环的类。默认情况下,线程没有与之关联的消息循环。要创建一个,在要运行循环的线程中调用 prepare(
- 在Java中,当为一个类创建了多个构造函数时,有时想在一个构造函数中调用另一个构造函数以减少代码量。这时可以使用this关键字来实现。有关构
- 2011年6月iBatis 更名为 MyBatis,从 iBatis 到 MyBatis,不只是名称上的变化,MyBatis 提供了更为强大
- 本文演示以Spark作为分析引擎,Cassandra作为数据存储,而使用Spring Boot来开发驱动程序的示例。1.前置条件安装Spar
- 本文实例讲述了Android实现简单MD5加密的方法。分享给大家供大家参考,具体如下:网上关于android下MD5加密的资料很多,但是测试
- 前言缓存技术被认为是减轻服务器负载、降低网络拥塞、增强Web可扩展性的有效途径之一,其基本思想是利用客户访问的时间局部性(Temproral
- 一、前言本文小编将基于 SpringBoot 集成 Shiro 实现动态uri权限,由前端vue在页面配置uri,Java后端动态刷新权限,
- 一般在web应用中,对客户端提交上来的图片肯定需要进行压缩的。尤其是比较大的图片,如果不经过压缩会导致页面变的很大,打开速度比较慢,当然了如
- 以下通过3个知识点给大家讲解了上拉加载和下拉刷新的Fragment实现的方法,在对每个知识点介绍了一下用法。1.效果预览1.1.这个首页就是
- 本文实例讲述了Java截取字符串的方法。分享给大家供大家参考。具体实现方法如下:public static void main(String
- 这几天对Android中实现画圆弧及圆弧效果中所实现的效果进行了修改,改为进度圆心进度条,效果如图所示TasksCompletedView.
- 小编在之前给大家介绍过很多android项目打包的经验,本篇内容我们通过一个项目实例来给大家讲解android每一步打包和签名的过程。and
- 1、未配置之前2、开始配置 2.1 新建一个unauth.html<!DOCTYPE html><html la
- 项目地址:gitee.com/baojh123/rp…netty-study 这个项目是没用到的,可以删掉,主要是测试
- 详解Kotlin Android开发中的环境配置在Android Studio上面进行安装插件在Settings ->Plugins