2.4-结构化并发:协程的结构化异常管理
文章目录
- 协程结构化异常流程
- 协程结构化异常流程和取消流程的区别
- 子协程异常为什么要连带取消父协程?
- CoroutineExceptionHandler
- 异常协程异常的最后一道拦截:CoroutineExceptionHandler
- CoroutineExceptionHandler 为什么只能设置给最外层协程才有效?
- async() 对异常的处理
- SupervisorJob
- 总结
协程的异常很多人都觉得很难,[难] 主要体现在两个地方,表面原因是它很复杂,深层原因是大多数人没有理解协程的异常结构化管理有什么用。
所以接下来我们就从这两个 [难] 的原因由浅到深的讲解协程的结构化异常管理。
下面的内容将会在 协程的结构化取消 的基础上讲解,建议提前了解才能更好掌握协程结构化异常流程。
协程结构化异常流程
协程结构化异常流程和取消流程的区别
了解结构化流程我们直接上示例代码:
fun main() = runBlocking {val scope = CoroutineScope(EmptyCoroutineContext)scope.launch {println("Parent started")launch {println("Child started")throw RuntimeException()println("Child finished")}println("Parent finished")}delay(10000)
}输出结果:
Parent started
Parent finished
Child started
// 抛出异常
上面的代码在子协程抛出了异常,会导致父协程也被取消。更具体的说,当协程抛出异常时(除了 CancellationException),它往上、往下的整个父子协程树全都会因为这个异常而被取消掉。
实际上协程的异常处理和协程的取消用的是同一套逻辑。我们可以通过打印协程的状态验证:
fun main() = runBlocking {val scope = CoroutineScope(EmptyCoroutineContext)var childJob: Job? = nullval parentJob = scope.launch {childJob = launch {println("Child started")delay(3000)println("Child done")}delay(1000)throw IllegalStateException("Wrong user!")}// 抛异常前打印状态delay(500)println("before, isActive: parent - ${parentJob.isActive}, child - ${childJob?.isActive}")println("before, isCancelled: parent - ${parentJob.isCancelled}, child - ${childJob?.isCancelled}")// 抛异常后打印状态delay(1000)println("after, isActive: parent - ${parentJob.isActive}, child - ${childJob?.isActive}")println("after, isCancelled: parent - ${parentJob.isCancelled}, child - ${childJob?.isCancelled}")delay(10000)
}输出结果:
Child started // 子协程只打印了一行
before, isActive: parent - true, child - true
before, isCancelled: parent - false, child - false
// 抛出异常
after, isActive: parent - false, child - false
after, isCancelled: parent - true, child - true // 抛异常会导致协程都取消
上面的代码会延时 500ms 时打印父子协程的状态,在父协程 1s 后抛出异常,抛异常后再打印状态。可以看到父协程抛出了异常,父子协程的 isActive 都打印为 false,isCancelled 都打印为 true,说明父子协程都被取消了。
如果我们把异常换成了 CancellationException:
fun main() = runBlocking {val scope = CoroutineScope(EmptyCoroutineContext)var childJob: Job? = nullval parentJob = scope.launch {childJob = launch {println("Child started")delay(3000)println("Child done")}delay(1000)throw CancellationException("Wrong user!")}// 抛异常前打印状态delay(500)println("before, isActive: parent - ${parentJob.isActive}, child - ${childJob?.isActive}")println("before, isCancelled: parent - ${parentJob.isCancelled}, child - ${childJob?.isCancelled}")// 抛异常后打印状态delay(1000)println("after, isActive: parent - ${parentJob.isActive}, child - ${childJob?.isActive}")println("after, isCancelled: parent - ${parentJob.isCancelled}, child - ${childJob?.isCancelled}")delay(10000)
}输出结果:
Child started
before, isActive: parent - true, child - true
before, isCancelled: parent - false, child - false
after, isActive: parent - false, child - false
after, isCancelled: parent - true, child - true
可以看到除了 CancellationException 不会导致打印异常之外,父子协程状态 isActive 都打印 false,isCancelled 都打印 true,父子协程的状态更改都是一样的,也能印证异常流程和取消流程用的是同一套逻辑。
也可以理解为 当在协程抛出的是 CancellationException 时,协程会走简化版的异常流程,也就是取消流程。
那么取消流程比异常流程简化掉了什么呢?
-
取消流程的连带取消只是向内的,即协程取消只会连带性的取消它的子协程
-
异常流程的连带取消是双向的,不仅会向内取消它的子协程,还会向外取消它的父协程,每一个被取消的父协程序也会把它们的每个子协程取消,直到整个协程树都被取消
每个协程被取消时,它的父协程都会调用 childCancelled 函数:
JobSupport.ktpublic open class JobSupport constructor(active: Boolean) : Job, ChildJob, ParentJob {// 每个协程被取消的时候,它的父协程都会调用这个函数// cause:触发子协程取消的那个异常// cancelImpl(cause) 就是取消父协程自己public open fun childCancelled(cause: Throwable): Boolean {if (cause is CancellationException) return true // 协程取消不会取消父协程return cancelImpl(cause) && handlesException // 普通异常会取消父协程}
}
在 childCancelled 函数判断异常类型为 CancellationException 时,协程取消不会连带性把父协程取消直接返回 true;如果是其他异常时,它会执行父协程的取消流程 cancelImpl。
用一个简单的示例验证下:
fun main() = runBlocking {val scope = CoroutineScope(EmptyCoroutineContext)scope.launch {launch {// 开启一个孙子协程launch {println("Grand child started")delay(3000)println("Grand child done")}delay(1000)// 在子协程取消throw CancellationException("Child cancelled!")// throw IllegalStateException("User invalid!")}println("Parent started")delay(3000)println("Parent done")}delay(500)delay(10000)
}输出结果:
// 子协程抛出 CancellationException
Parent started
Grand child started
Parent done // 子协程取消,孙子协程没有打印,父协程有打印没有被取消// 子协程抛出其他异常
Parent started
Grand child started
// 父协程和孙子协程都没有打印,都被取消了
上面的代码在子协程抛出 CancellationException 时,也就是子协程被取消了,可以看到父协程还是正常打印,但在子协程启动的孙子协程后续没有再打印了;而抛出其他异常时,父协程和孙子协程都没有再打印。
总结下结构化异常流程和结构化取消流程的区别:
-
异常流程的子协程抛异常会导致父协程被取消;取消流程只会取消子协程
-
异常流程只有内部抛异常的方式触发(也很好理解,调用函数触发异常也太奇怪了);取消流程可以从外部 job.cancel() 或者内部抛 CancellationException 取消
-
异常流程抛的异常除了用来取消协程,还会把这个异常暴露给线程世界;取消流程抛 CancellationException 只用来取消
子协程异常为什么要连带取消父协程?
或许看了上面的代码和示例代码验证后你会很疑惑:为什么异常流程除了取消子协程还要取消父协程?子协程抛出异常就只会影响它的子协程取消?
因为子协程一旦抛异常,影响了外部大流程的执行,让其他协程的执行失去了意义,父协程的执行也失去了意义。
协程的结构化有两个特性:
-
父子协程连带性取消的性质:从正常逻辑来讲,当一个大流程都被取消的时候,它内部的子流程也就直接没用
-
父协程等待子协程的性质:父协程会等待所有子协程都完成之后才完成自己,这也是子流程都完成了,整个大流程才能算是完成
子流程被取消,对外部大流程来说可能是一个正常的事件而已,所以子协程的取消并不会导致父协程取消。
但如果子协程是抛异常了(非 CancellationException 的异常),这通常被理解为子协程坏掉了,导致大流程无法完成,也就相当于父协程也坏掉了。所以协程因为普通异常而被取消,它的父协程也被以这个异常为原因而取消。
简单理解就是,子协程的异常通常意味着父协程的损坏,所以干脆把父协程取消。
CoroutineExceptionHandler
当我们在协程抛出异常时,和在线程内抛异常一样最终会导致崩溃,但在协程世界和线程世界之间如果还想捕获异常,还有一个方法:CoroutineExceptionHandler。
异常协程异常的最后一道拦截:CoroutineExceptionHandler
有一些协程的初学者在写协程代码的时候可能会这么写:
fun main() = runBlocking {val scope = CoroutineScope(EmptyCoroutineContext)try {scope.launch {throw RuntimeException()}} catch (e: Exception) {//}delay(10000)
}输出结果:
// 抛出异常
上面的代码很简单,用 try-catch 包住协程的执行,想要捕获协程的异常,但是最终发现并没有捕获到。
为什么按上面这么写无法捕获到里面的异常呢?我把上面的代码换一下你就明白了:
fun main() = runBlocking {val scope = CoroutineScope(EmptyCoroutineContext)try {// 换成了线程,线程内部抛异常,也是没法捕获thread {throw RuntimeException()}} catch (e: Exception) {//}delay(10000)
}
我们把协程换成了线程,在线程内部抛异常,外部的 try-catch 也是无法捕获异常的,都在不同的线程了肯定不能捕获到异常。
那么刚才的协程也是同理,try-catch 只能捕获协程启动的代码,协程启动结束了 try-catch 代码块也就结束了。
协程太简洁了让我们产生了错觉,觉得 launch 的大括号里面和外面是同一套流程,但实际上协程都是并行的流程是无法互相 try-catch。
当然这也是因为协程是可以套着写启动子协程,写多了就会有一个错觉能用 try-catch 捕获异常,在线程我们很少像协程一样套着启动线程所以没这个想法:
scope.launch {try {launch {}} catch (e: Exception) {}launch {}
}
如果我们想捕获协程的异常,可以用 CoroutineExceptionHandler 传给最外面的父协程:
fun main() = runBlocking {val scope = CoroutineScope(EmptyCoroutineContext)val handler = CoroutineExceptionHandler { _, exception -> println("Caught $exception")}// CoroutineExceptionHandler 设置给最外层的协程scope.launch(handler) {launch {throw RuntimeException("Error!")}}delay(10000)
}输出结果:
Caught java.lang.RuntimeException: Error!
对于一个多层结构的协程树,只要给它们最外层的父协程设置一个 CoroutineExceptionHandler,它里面所有子协程包括最外层父协程的异常都会集中到它这里来统一处理。
需要注意的是,CoroutineExceptionHandler 只能设置到最外面的父协程,设置到内层协程是没用的。
CoroutineExceptionHandler 为什么只能设置给最外层协程才有效?
在上面我们有提到使用 CoroutineExceptionHandler 必须将它设置给最外层的父协程才能生效捕获到子协程抛出的异常。
为什么只能设置给最外层呢?在讲解这个原理之前,我们先了解下 Java 提供的 UncaughtExceptionHandler。
fun main() = runBlocking {// 对所有线程异常都捕获的 UncaughtExceptionHandlerThread.setDefaultUncaughtExceptionHandler { t, e -> println("Caught default: $e")// 记录异常日志收尾后将程序杀死或重启}val thread = Thread {throw RuntimeException("Thread error!")}// 针对单个线程的 UncaughtExceptionHandler// 优先级比 Thread.setDefaultUncaughtExceptionHandler 高// 如果线程处理的东西比较独立也可以针对线程设置// 但大多数时候都是用通用的设置
// thread.setUncaughtExceptionHandler { t, e ->
// println("Caught $e")
// }thread.start()
}输出结果:
Caught default: java.lang.RuntimeException: Thread error!
示例代码分别提供了两种方式:
-
thread.setUncaughtExceptionHandler:指定线程单独设置,线程抛出异常时捕获异常
-
Thread.setDefaultUncaughtExceptionHandler:通用设置,对所有线程都捕获异常
设置 UncaughtExceptionHandler 能让线程在抛出异常时被捕获到,它主要的作用是在线程发生了意料之外的未知异常(线程内没有 try-catch 我们有意识处理的异常)时用于记录使用,而不是将异常捕获吞掉让程序运行在异常混乱的状态。
UncaughtExceptionHandler 是异常处理的最后一道拦截,此时线程已经结束执行无法修复,只能收尾后杀死应用或重启。
比如在 Android 抛出异常就会直接 force close,也是用的 UncaughtExceptionHandler 捕获异常记录后让应用停止运行。
说完 UncaughtExceptionHandler,现在我们说回 CoroutineExceptionHandler。
CoroutineExceptionHandler 其实和针对单个线程设置 thread.setUncaughtExceptionHandler 是一样的,没法像线程那样设置 Thread.setDefaultUncaughtExceptionHandler 对全局协程处理。
因为协程是在线程之上的,所以有未知异常设置了 CoroutineExceptionHandler 就会在它这里捕获到,如果没有设置那也会被线程的 UncaughtExceptionHandler 捕获:
fun main() = runBlocking {Thread.setDefaultUncaughtExceptionHandler { t, e -> println("Caught default: $e")}val scope = CoroutineScope(EmptyCoroutineContext)val handler = CoroutineExceptionHandler { _, e -> println("Caught in Coroutine: $e")}scope.launch(handler) {launch {throw RuntimeException("Error!")}}delay(10000)
}输出结果:
// scope.launch 设置 CoroutineExceptionHandler
Caught in Coroutine: java.lang.RuntimeException: Error!// scope.launch 没设置 CoroutineExceptionhandler
Caught default: java.lang.RuntimeException: Error!
通常我们使用协程可以独立的完成一个完整的功能,在某个子协程抛出异常时能取消掉整个协程树的条件下(这在线程是很麻烦的),又能将异常汇报到一个地方(CoroutineExceptionHandler),然后做一些善后工作就很方便。
CoroutineExceptionHandler 的作用就是针对单个协程树的未知异常做善后工作,因为注册 CoroutineExceptionHandler 的目的是做善后工作,那么自然的它就得在最外层的父协程设置是最方便处理。
async() 对异常的处理
启动协程有两种方式:launch 和 async。
在之前的讲解协程结构化异常都是用的 launch,不过 async 对异常的处理其实跟 launch 基本上是一致的,但还是有一些区别:
fun main() = runBlocking {val scope = CoroutineScope(EmptyCoroutineContext)val handler = CoroutineExceptionHandler { _, e ->println("Caught in Coroutine: $e")}scope.launch(handler) {val deferred = async {delay(1000)throw RuntimeException("Error!")}launch {// async 抛出了异常,await() 也会抛出异常// 进而在它调用所在的协程也会影响抛出异常try {deferred.await()} catch (e: Exception) {println("Caught in await: $e")}// 验证协程是否被取消try {delay(1000)} catch (e: Exception) {println("Caught in delay $e")}}}delay(10000)
}输出结果:
// 先触发了 async 抛出的异常而不是 CancellationException
// 因为结构化的异常流程还没走完,就先提前触发了 RuntimeException
Caught in await: java.lang.RuntimeException: Error!
Caught in delay kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=StandaloneCoroutine{Cancelling}@1924f17a
Caught in Coroutine: java.lang.RuntimeException: Error!
上面的例子用 async 启动了一个协程,然后在另一个协程调用 await(),当 async 抛异常时,调用 deferred.await() 的位置也会抛异常,进而让所在的协程也被取消,最终最外层协程设置的 CoroutineExceptionHandler 捕获了异常。
但看输出的日志打印,deferred.await() 并不是抛出的 CancellationException,而是在 async 抛出的 RuntimeException。按照我们对结构化异常流程的理解,为什么在 deferred.await() 的 try-catch 捕获的不是 CancellationException?
实际上 在 async 里面抛异常时是有 [双重影响] 的:它不仅会用这个异常来触发它所在的协程树的结构化异常处理流程取消协程,还会直接让它的 await() 调用也抛出这个异常。
async 和 launch 的另一个异常流程的区别是,即使 async 作为最外层父协程,对 async 设置 CoroutineExceptionHandler 也是没有效果的:
fun main() = runBlocking {Thread.setDefaultUncaughtExceptionHandler { _, e ->println("Caught default: $e")}val scope = CoroutineScope(EmptyCoroutineContext)val handler = CoroutineExceptionHandler { _, e ->println("Caught in Coroutine: $e")}val deferred = scope.async (handler) {launch {throw RuntimeException("Error!")}}deferred.await()delay(10000)
}输出结果:
// 异常被抛到线程世界,没有被 CoroutineExceptionHandler 拦截
Caught default: java.lang.RuntimeException: Error!
** async 不会往线程世界抛异常,因为 async 抛出的异常要给 await(),await() 还是运行在协程的;而 launch 会把内部的异常抛给线程世界是因为它已经是整个流程的终点了,CoroutineExceptionHandler 只能用在 launch 启动的最外层的父协程**。
SupervisorJob
在了解协程结构化异常管理机制后,我们知道子协程在抛出异常时,整个协程树都会被连带性的取消:
JobSupport.ktpublic open class JobSupport constructor(active: Boolean) : Job, ChildJob, ParentJob {// 每个协程被取消的时候,它的父协程都会调用这个函数// cause:触发子协程取消的那个异常// cancelImpl(cause) 就是取消父协程自己public open fun childCancelled(cause: Throwable): Boolean {if (cause is CancellationException) return true // 协程取消不会取消父协程return cancelImpl(cause) && handlesException // 普通异常会取消父协程}
}
那么是否会有一种需求:子协程在抛出异常时不希望父协程也被连带性取消。为此协程也提供了 SupervisorJob。
SupervisorJob 中的 [Supervisor] 就是主管的意思,它和 Job 的区别是它重写了 childCancelled() 直接返回 false:
Supervisor.ktprivate class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {override fun childCancelled(cause: Throwable): Boolean = false
}
SupervisorJob 的作用是,它的子协程因为非 CancellationException 异常取消时,父协程不会连带性的被取消。简单说就是 [取消你的时候是你的父协程,你抛异常的时候不是你的父协程]。
fun main() = runBlocking {val scope = CoroutineScope(EmptyCoroutineContext)val supervisorJob = SupervisorJob()// supervisorJob 作为父协程scope.launch(supervisorJob) {throw RuntimeException("Error!")}delay(1000)println("Parent Job cancelled: ${supervisorJob.isCancelled}")delay(10000)
}输出结果:
// 打印异常
Parent Job cancelled: false // 父协程没有被取消
SupervisorJob 在实际使用场景会有两种常见方式。
第一种常见的方式是 SupervisorJob 作为子协程和父协程之间的桥梁,类似半链条的方式:
fun main() = runBlocking {val scope = CoroutineScope(EmptyCoroutineContext)val parentJob = scope.launch {// 加上 coroutineContext.job 是为了让 SupervisorJob 和外部协程是父子关系// 此时 SupervisorJob 是外面协程的子协程,也是里面协程的父协程// 这时 SupervisorJob 子协程抛出异常不会影响外部父协程被取消// 外部父协程取消时又能正常取消子协程,SupervisorJob 半链条的写法很常用launch(SupervisorJob(coroutineContext.job)) {throw RuntimeException("Error!")}}delay(1000)println("Parent Job cancelled: ${parentJob.isCancelled}")delay(10000)
}输出结果:
Parent Job cancelled: false
将 SupervisorJob 传给子协程作为它的父协程,同时为了不断开和外部协程的关系,SupervisorJob 也传入 coroutineContext.job 让外部协程取消时能正常取消子协程。
第二种方式是将 SupervisorJob 提供给 CoroutineScope:
val scope = CoroutineScope(SupervisorJob())
scope.launch {}
用这个 scope 启动的所有协程在抛异常时,都不会触发外面的 CoroutineScope 的取消;一旦外面的 CoroutineScope 取消,scope 启动的所有协程都会被取消。这也符合 SupervisorJob 的特性。
SupervisorJob 在异常管理时也会完全像一个 [最外层父协程] 一样工作:
fun main() = runBlocking {val scope = CoroutineScope(EmptyCoroutineContext)val parentJob = scope.launch {val handler = CoroutineExceptionHandler { _, e -> println("Caught in handler: $e")}// 按照结构化异常管理机制,不是最外层父协程设置 CoroutineExceptionHandler 是无效的// 但在这里却生效能捕获了,说明 SupervisorJob 充当了 [最外层的父协程]launch(SupervisorJob(coroutineContext.job) + handler) {launch {throw RuntimeException("Error!")}}}delay(1000)println("Parent Job cancelled: ${parentJob.isCancelled}")delay(10000)
}输出结果:
Caught java.lang.RuntimeException: Error!
Parent Job cancelled: false
当子协程抛出异常时,SupervisorJob 充当了 [最外层的父协程] 的角色,设置 CoroutineExceptionHandler 捕获子协程异常生效了。
SupervisorJob 就是一个 [会取消子协程,但不会被子协程取消] 的 Job。
总结
一、协程结构化异常流程
协程的异常处理和协程的取消用的是同一套逻辑:
-
取消流程的连带取消只是向内的,即协程取消只会连带性的取消它的子协程
-
异常流程的连带取消是双向的,不仅会向内取消它的子协程,还会向外取消它的父协程,每一个被取消的父协程序也会把它们的每个子协程取消,直到整个协程树都被取消
结构化异常流程和结构化取消流程的区别:
-
异常流程的子协程抛异常会导致父协程被取消;取消流程只会取消子协程
-
异常流程只有内部抛异常的方式触发;取消流程可以从外部 job.cancel() 或者内部抛 CancellationException 取消
-
异常流程抛的异常除了用来取消协程,还会把这个异常暴露给线程世界;取消流程抛 CancellationException 只用来取消
二、子协程异常为什么要连带取消父协程?
子协程一旦抛异常,影响了外部大流程的执行,让其他协程的执行失去了意义,父协程的执行也失去了意义:
-
子流程被取消,对外部大流程来说可能是一个正常的事件而已,所以子协程的取消并不会导致父协程取消
-
子协程抛异常(非 CancellationException 的异常),这通常被理解为子协程坏掉了,导致大流程无法完成,也就相当于父协程也坏掉了。所以协程因为普通异常而被取消,它的父协程也被以这个异常为原因而取消
一句话总结:子协程的异常通常意味着父协程的损坏,所以干脆把父协程取消。
三、CoroutineExceptionHandler
1、UncaughtExceptionHandler 和 CoroutineExceptionHandler 的类比
在线程分别提供了两种方式拦截异常:
-
thread.setUncaughtExceptionHandler:指定线程单独设置,线程抛出异常时捕获异常
-
Thread.setDefaultUncaughtExceptionHandler:通用设置,对所有线程都捕获异常
UncaughtExceptionHandle 的作用是在线程发生了意料之外的未知异常(线程内没有 try-catch 我们有意识处理的异常)时用于记录使用,而不是将异常捕获吞掉让程序运行在异常混乱的状态。
UncaughtExceptionHandler 是异常处理的最后一道拦截,此时线程已经结束执行无法修复,只能收尾后杀死应用或重启。
CoroutineExceptionHandler 和针对单个线程设置 thread.setUncaughtExceptionHandler 是一样的,没法像线程那样设置 Thread.setDefaultUncaughtExceptionHandler 对全局协程处理。
对于一个多层结构的协程树,只要给它们最外层的父协程设置一个 CoroutineExceptionHandler,它里面所有子协程包括最外层父协程的异常都会集中到它这里来统一处理。
2、为什么 CoroutineExceptionHandler 只能设置在最外层协程才有效?
CoroutineExceptionHandler 的作用就是针对单个协程树的未知异常做善后工作,又因为协程结构化异常会有连带性的特性(异常流程双向取消并抛异常),最终异常会走到最外层的协程,让它从协程世界将异常抛到线程世界,那么自然的它就得在最外层的父协程设置是最方便处理。
四、async 的异常处理
-
在 async 里面抛异常时是有 [双重影响] 的:它不仅会用这个异常来触发它所在的协程树的结构化异常处理流程取消协程,还会直接让它的 await() 调用也抛出这个异常。
-
async 作为最外层父协程,对 async 设置 CoroutineExceptionHandler 也是没有效果的。async 不会往线程世界抛异常,因为 async 抛出的异常要给 await(),await() 还是运行在协程的;而 launch 会把内部的异常抛给线程世界是因为它已经是整个流程的终点了,CoroutineExceptionHandler 只能用在 launch 启动的最外层的父协程
五、SupervisorJob
SupervisorJob 的作用是,它的子协程因为非 CancellationException 异常取消时,父协程不会连带性的被取消。简单说就是 [取消你的时候是你的父协程,你抛异常的时候不是你的父协程]。
当子协程抛出异常时,SupervisorJob 充当了 [最外层的父协程] 的角色,设置 CoroutineExceptionHandler 捕获子协程异常会生效。
一句话总结:SupervisorJob 就是一个 [会取消子协程,但不会被子协程取消] 的 Job。
相关文章:
2.4-结构化并发:协程的结构化异常管理
文章目录 协程结构化异常流程协程结构化异常流程和取消流程的区别子协程异常为什么要连带取消父协程? CoroutineExceptionHandler异常协程异常的最后一道拦截:CoroutineExceptionHandlerCoroutineExceptionHandler 为什么只能设置给最外层协程才有效&…...
Android 12.0 debug版本打开OEM解锁开关功能实现
通常为了方便push在debug版本会采用如下命令 adb root adb disable-verity 提示: Device is locked. Please unlock the device first. 查找日志可以发现system/core/set-verity-state/set-verity-state.cpp文件中is_avb_device_locked方法里 这个获取ro.boot…...

linux用户组练习
准备工作 [rootlocalhost ~]# watch -n 1 tail -n 5 /etc/group使用watch 动态监控 1.建立用户组 shengcan,其id 为2000 2.建立用户组 caiwu,其id 为 2001 3.足建立用户组 jishu,其id 为 2002 4.建立用户lee,指定其主组id为sh…...

[Docker][Docker Container]详细讲解
目录 1.什么是容器?2.容器命令1.docker creatre2.docker run3.docker ps4.docker logs5.docker attach6.docker exec7.docker start8.docker stop9.docker restart10.docker kill11.docker top12.docker stats13.docker container inspect14.docker port15.docker c…...

塑造美好心灵,激发创造活力|第三届瓷艺中华“陶溪川杯”儿童青少年陶瓷作品展开展
第三届瓷艺中华“陶溪川杯”儿童青少年陶瓷作品展 展览现场 由中央美术学院、景德镇陶瓷大学、景德镇陶文旅控股集团共同主办,由中国非物质文化遗产保护协会陶瓷分会、中国文化艺术发展促进会陶瓷专业委员会、中央美术学院陶瓷艺术研究院、中央美术学院少儿美术教…...
鸿蒙开发刷新单个item会闪一下处理
鸿蒙开发刷新单个item会闪一下 首先我用的是懒加载方式,改变某位数据后我调listener.onDataChange(index),发现item的改动是变了,但是item也闪了一下。 先分析为什么item会闪一下 其他是因为item上有图片,加载的网络图。你onDataChange(index)时,它会重新加载这一item,…...
您需要了解的有关 5G 的一切。
转载 https://www.qualcomm.com/5g/what-is-5g 在这里,您可以找到 5G 技术的解释——5G 的工作原理、5G 的重要性以及它如何改变世界连接和沟通的方式。在 Qualcomm,我们发明了使 5G 成为可能的根本性突破。 问:什么是 5G? 答&…...
【redis】初识redis入门,基础部署以及介绍
本站以分享各种运维经验和运维所需要的技能为主 《python零基础入门》:python零基础入门学习 《python运维脚本》: python运维脚本实践 《shell》:shell学习 《terraform》持续更新中:terraform_Aws学习零基础入门到最佳实战 《k8…...
数据库基础 -- 数据库约束
数据库基础 – 数据库约束 1.约束 1.1 概念 约束是用于强制数据库中数据 完整性 和 一致性 的规则。它们定义了对表中数据的限制,确保数据的有效性和正确性,实际上就是表中数据的限制条件。 1.2 分类 1.2.1 完整性约束 主键约束(Primary Key Const…...

U盘文件或目录损坏无法读取?专业恢复策略全解析
U盘困境:文件目录的隐形危机 在日常的数字生活中,U盘作为便捷的数据存储与传输工具,扮演着至关重要的角色。然而,当U盘中的文件或目录突然遭遇损坏,导致无法被正常读取时,这无疑给用户带来了极大的困扰。这…...

dpdk实现udp协议栈
使用DPDK实现UDP用户态协议栈,实现流程中包括: 三类线程 1、收发包线程 2、用户态协议栈线程 3、udp服务端线程 两类缓冲区: 1、协议栈收包缓冲区和协议栈发包缓冲区 2、udp收包缓冲区和udp发包缓冲区 协议栈缓冲区中存储的数据是str…...

Shell编程——基础语法(2)和 Shell流程控制
文章目录 基础语法(2)echo命令read命令printf命令test命令 Shell流程控制if-else语句for 循环while 语句until 循环case ... esac跳出循环 基础语法(2) echo命令 Shell 的 echo 指令与 PHP 的 echo 指令类似,都是用于…...

Python基础教程(二)字符串和函数
6.字符串 6.1 字符串的表示方式 6.1.1 普通字符串 普通字符串指用单引号()或双引号(”")括起来的字符串。例如:Hello或"Hello" >>> Hello Hello >>> "Hello" Hello >>> s\u0048\u0065\u006c\u006c\u006f >>> …...

智算新风向丨趋动科技获中国信通院泰尔实验室首张智算资源池化能力泰尔测评证书
近日,趋动科技“OrionX AI算力资源池化软件”经中国泰尔实验室依据《FG-Z14-0172-01智算资源池化平台测试方案》评估测试,获得智算资源池化能力泰尔测评证书,成为该领域首个完成此评价的产品。 图1.OrionX通过智算资源池化平台评测 随着AI大…...

计算机基础(Windows 10+Office 2016)教程 —— 第4章 计算机网络与Internet(上)
第4章 计算机网络与Internet 4.1 计算机网络概述4.1.1 计算机网络的定义4.1.2 计算机网络的发展4.1.3 计算机网络的功能4.1.4 计算机网络体系结构和TCP/IP 参考模型 4.2 计算机网络的组成和分类4.2.1 计算机网络的组成4.2.2 计算机网络的分类 4.3 网络传输介质和通信设备4.3.1 …...

MES系统在数字化转型中的核心作用与影响
数字化转型是企业利用数字技术改变其业务模式、运营方式、组织结构、产品服务等方面的过程,旨在提高效率、降低成本、增强竞争力并实现可持续发展。数字化转型涉及多个层面,主要包括以下几个方面: 数字化转型转什么 转战略:由构…...
装修施工注意事项
1 地漏保护 咋墙拆改时,一定要用保护盖把所有的地漏下水管道都拧紧 2 卫生间防水做完,必须要先用水泥砂浆做好保护层再贴,不然后续施工,不小心破坏防水层,以后漏水后悔都晚了。 3 入户门口处,一定要用…...
【Docker学习记录】
Docker学习记录 目录 1. Windows上使用wsl1.1 安装docker后遇到的一些疑惑1.2. wsl的一些相关命令1.3. 补一点,wsl的作用 2. docker一些常用的命令2.1 构建docker镜像2.2 运行镜像 3. Dockerfile的编写3.0 docker的一些概念3.0.1 容器的分层3.0.2 COPY-ON-WRITE 3.…...

互联网政务应用安全管理规定
互联网政务应用安全管理规定 (2024年2月19日中央网络安全和信息化委员会办公室、中央机构编制委员会办公室、工业和信息化部、公安部制定 2024年5月15日发布) 第一章 总则 第一条为保障互联网政务应用安全,根据《中华人民共和国网络安全法…...
HarmonyOS开发商城首页实现
目录 一:功能概述 二:代码实现 三:效果图 一:功能概述 这一节我们主要在鸿蒙OS系统中实现的一个底部导航功能,并在首页底部使用TabSwitch组件进行导航切换。同时,首页采用Search组件实现商品搜索框,Scroll和Swiper组件实现图片轮播功能,以展示推荐内容或图片。 1:…...

UE5 学习系列(二)用户操作界面及介绍
这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…...

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...

dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...

2.Vue编写一个app
1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...

(转)什么是DockerCompose?它有什么作用?
一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器。 Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...

Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...

dify打造数据可视化图表
一、概述 在日常工作和学习中,我们经常需要和数据打交道。无论是分析报告、项目展示,还是简单的数据洞察,一个清晰直观的图表,往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server,由蚂蚁集团 AntV 团队…...

用机器学习破解新能源领域的“弃风”难题
音乐发烧友深有体会,玩音乐的本质就是玩电网。火电声音偏暖,水电偏冷,风电偏空旷。至于太阳能发的电,则略显朦胧和单薄。 不知你是否有感觉,近两年家里的音响声音越来越冷,听起来越来越单薄? —…...
#Uniapp篇:chrome调试unapp适配
chrome调试设备----使用Android模拟机开发调试移动端页面 Chrome://inspect/#devices MuMu模拟器Edge浏览器:Android原生APP嵌入的H5页面元素定位 chrome://inspect/#devices uniapp单位适配 根路径下 postcss.config.js 需要装这些插件 “postcss”: “^8.5.…...

招商蛇口 | 执笔CID,启幕低密生活新境
作为中国城市生长的力量,招商蛇口以“美好生活承载者”为使命,深耕全球111座城市,以央企担当匠造时代理想人居。从深圳湾的开拓基因到西安高新CID的战略落子,招商蛇口始终与城市发展同频共振,以建筑诠释对土地与生活的…...