当前位置: 首页 > news >正文

Kotlin 协程与 Flow


简介
KotlinFlowKotlin 在异步编程方面的一个重要组件,它提供了一种声明式的、可组合的、基于协程的异步编程模型。Flow 的设计灵感来自于 Reactive StreamsRxJavaFlux 和其他异步编程库,但它与 Kotlin 协程无缝集成,并提供了一种更具 Kotlin 特性的 API。

Flow 是用于异步数据流的概念,它可以看作是一系列异步的、可并发的值或事件的流。可以通过操作符链更改、过滤、转换这些流,并且可以对这些流进行组合、合并、扁平化等操作。Flow 中的数据可以是异步的、非阻塞的,也可以是懒加载的,因此它非常适合处理类似于网络请求、数据库查询、传感器数据等异步任务。

使用示例

fun main() = runBlocking {flow {// 上游,发源地emit(111) //挂起函数emit(222)emit(333)emit(444)emit(555)}.filter { it > 200 } //过滤.map { it * 2 } //转换.take(2) //从流中获取前 2 个元素.collect {println(it)}
}//对应输出
444
666Process finished with exit code 0

看到这种链式调用是不是感觉很熟悉,没错,同RxJava一样Kotlin的Flow也分为上游和下游,emm… 也可以理解Kotlin的Flow就是为了替代RxJava,如果对RxJava有一定了解的话,学习Flow就更容易了。

应用场景

  • 异步任务处理:Flow 可以方便地处理异步任务,如网络请求、数据库查询等。
  • UI 事件响应:Flow 可以用于处理 UI 事件,例如按钮点击、搜索操作等。
  • 数据流管道:Flow 可以作为数据处理的管道,从一个数据源源源不断地发射数据,供后续处理或展示。
  • 数据流转换:Flow 可以方便地对数据进行转换、过滤、分组等操作,实现复杂的数据流处理逻辑。

下面来看一些常见的Flow使用示例,感受下Flow的魅力:

Flow 提供了各种操作符,用于转换、过滤和组合流,例如 map()filter()transform()zip()flatMapConcat() 等。这些操作符可以通过链式调用来对流进行链式操作。

Flow 还具有背压支持,可以通过 buffer()conflate()collectLatest() 等操作符来控制流的发送速率,从而避免生产者和消费者之间的资源不平衡问题。

1. 转换操作

  • map(): 将 Flow 中的每个元素转换为另一种类型。
fun createFlow(): Flow<Int> = flow {for (i in 1..5) {delay(1000)emit(i)}
}fun main() = runBlocking {createFlow().map { it * it } // 将元素平方.collect { value ->println(value) // 打印平方后的值}
}
//对应输出
1
4
9
16
25Process finished with exit code 0
  • take() 函数有以下几种重载形式:
  1. take(n: Int): 从流中获取前 n 个元素。
  2. takeWhile(predicate: (T) -> Boolean): 获取满足条件的元素,直到遇到第一个不满足条件的元素。
  3. takeLast(n: Int): 从流的末尾获取最后 n 个元素。
  • filter(): 根据给定的谓词函数过滤 Flow 中的元素。
fun createFlow(): Flow<Int> = flow {for (i in 1..5) {delay(1000)emit(i)}
}fun main() = runBlocking {createFlow().filter { it % 2 == 0 } // 过滤偶数.collect { value ->println(value) // 打印偶数}
}
//对应输出
2
4Process finished with exit code 0

2. 组合操作

  • zip(): 将两个 Flow 的元素一对一地组合在一起。
fun createFlowA(): Flow<Int> = flow {for (i in 1..5) {delay(1000)emit(i)}
}fun createFlowB(): Flow<String> = flow {for (i in 5 downTo 1) {delay(1000)emit("Item $i")}
}fun main() = runBlocking {createFlowA().zip(createFlowB()) { a, b -> "$a - $b" } // 组合 FlowA 和 FlowB 的元素.collect { value ->println(value) // 打印组合后的元素}
}
//对应输出
1 - Item 5
2 - Item 4
3 - Item 3
4 - Item 2
5 - Item 1Process finished with exit code 0
  • flatMapConcat(): 将 Flow 中的元素扁平化为多个 Flow,并按顺序连接起来。
fun createFlowOfList(): Flow<List<Int>> = flow {for (i in 1..3) {delay(1000)emit(List(i) { it * it }) // 发出包含整数平方的列表}
}fun main() = runBlocking {createFlowOfList().flatMapConcat { flowOfList -> flowOfList.asFlow() } // 扁平化列表中的元素.collect { value ->println(value) // 打印平方后的值}
}
//对应输出
0
0
1
0
1
4Process finished with exit code 0解释下为什么是这样的打印结果,因为上面发送了三个Flow<List<Int>>,第一个List元素个
数为1所以打印 索引的平方即只有一个元素 下表索引就是0,输出打印0的平方还是0,第二个List
元素个数为2,返回索引下标01,扁平化List后打印 01。以此类推...

除了使用 flow{} 创建 Flow 以外,还可以使用 flowOf() 这个函数

fun main() = runBlocking {flowOf(1, 2, 3, 4, 5).collect { value ->println(value) // 打印 Flow 中的元素}
}
//对应输出
1
2
3
4
5Process finished with exit code 0

flowOf 函数是用于快速创建 Flow 的便捷方式。它接受可变数量的参数,并将这些参数作为发射项放入到 Flow 中。这样,我们就可以直接在 flowOf 中指定要发射的元素,而无需使用流构建器 flow { }

在某些场景下,我们甚至可以把 Flow 当做集合来使用,或者反过来,把集合当做 Flow 来用。

Flow.toList():
toList()Kotlin Flow 中的一个终端操作符。它用于将 Flow 中的元素收集到一个列表中,并在该列表中返回。它将 Flow 中的所有元素收集起来,然后在流完成时返回一个包含所有元素的列表。

以下是 toList() 的示例用法:

fun createFlow(): Flow<Int> = flow {for (i in 1..5) {delay(1000)emit(i)}
}fun main() = runBlocking {val list: List<Int> = createFlow().toList() // 将 Flow 中的元素收集到列表中println(list) // 打印列表
}
//对应输出
[1, 2, 3, 4, 5]Process finished with exit code 0

需要注意,toList() 操作符会等待整个流完成,然后将所有元素收集到列表中。因此,如果 Flow 是一个无限流,则可能永远不会完成,或者在内存和计算资源耗尽之前无法完成。
List.asFlow():
asFlow()Kotlin 标准库中 List 类的扩展函数,用于将 List 转换为 Flow。它允许将 List 中的元素作为发射项逐个发送到 Flow 中。

以下是 asFlow() 的示例用法:

fun main() = runBlocking {val list = listOf(1, 2, 3, 4, 5)list.asFlow() // 将 List 转换为 Flow.collect { value ->println(value) // 打印 Flow 中的元素}
}
//对应输出
1
2
3
4
5Process finished with exit code 0

asFlow() 的作用是将其他具有迭代性质的数据结构(如 List、Array 等)转换为 Flow,以便能够使用 Flow 的操作符和函数来处理这些数据。这对于在流式数据处理中与现有数据结构进行集成非常有用。

值得注意的是,使用 asFlow() 转换的 Flow 在发送元素时遵循迭代器的顺序。也就是说,Flow 发射的元素的顺序与原始数据结构(例如 List)中的元素顺序相同。

到目前为止可知的三种创建 Flow 的方式:

Flow创建方式适用场景用法
flow{}未知数据集flow { emit(getLock()) }
flowOf()已知具体的数据flow(1,2,3)
asFlow()数据集合list.asFlow()

由上面代码示例,可以看出Flow的API总体分为三部分:上游、中间操作、下游,上游发送数据,下游接收 处理数据,其中最复杂的就是中间操作符,下面就详细介绍下Flow的中间操作符。

中间操作符

生命周期

在学习中间操作符之前,先了解下Flow生命周期

  1. 创建流(Flow creation):通过使用 flow { ... } 构建器或其他 Flow 构建器创建一个流。在此阶段,流是冷的,不会发射任何值。
  2. 收集流(Flow collection):通过调用 collect 函数或其他流收集操作符(如 toListfirstreduce 等)来收集流的值。在此阶段,流会开始发射值,并触发相关的操作。
  3. 流完成(Flow completion):当发射的所有值都被消费后,流会完成,并标记为完成状态。此时,流的生命周期结束。
  4. 取消流(Flow cancellation):如果收集流的代码块被取消(使用协程的 cancel 函数),或者流的收集器被销毁(如ActivityFragment 被销毁),则流的收集过程将被取消。

需要注意的是,Flow 是基于协程的,因此其生命周期与协程的生命周期密切相关。当协程被取消时,与该协程相关联的流收集也会被取消,所以在使用Flow封装网络请求时,如果想取消某个请求即把相应的协程取消即可。

先来看下onStartonCompletion

fun main() = runBlocking {flow {emit(1)emit(2)emit(3)}.onStart {println("Flow started emitting values")}.onCompletion {println("Flow completed")}.collect { value ->println("Received value: $value")}
}
//对应输出
Flow started emitting values
Received value: 1
Received value: 2
Received value: 3
Flow completedProcess finished with exit code 0

onStart 函数允许在 Flow 开始发射元素之前执行一些操作,包括添加日志、初始化操作等。
onCompletion 函数允许在 Flow 完成之后执行一些操作,包括资源清理、收尾操作等。

并且onCompletion{} 在面对以下三种情况时都会进行回调:

  1. 正常执行完毕
  2. 出现异常
  3. 被取消

异常处理

Flow中的catch操作符用于捕获流中的异常,考虑到 Flow 具有上下游的特性,catch 这个操作符的作用是和它的位置强相关的即只能捕获到上游异常而无法捕获到下游异常,在使用时注意cache的位置。

看一段示例

fun main() = runBlocking {flow {emit(1)emit(2)throw NullPointerException("Null error")emit(3)}.onStart {println("Flow started emitting values")}.catch {println("Flow catch")emit(-1)}.onCompletion {println("Flow completed")}.collect { value ->println("Received value: $value")}
}
//对应输出
Flow started emitting values
Received value: 1
Received value: 2
Flow catch
Received value: -1
Flow completedProcess finished with exit code 0

需要注意catchonCompletion 的执行顺序和其所处的位置有关,出现异常时谁在上游谁先执行。

上下文切换

Flow 非常适合复杂的异步任务。在大部分的异步任务当中,我们都需要频繁切换工作的线程。对于耗时任务,我们需要线程池当中执行,对于 UI 任务,我们需要在主线程执行。

flowOn可以完美往我们解决这一问题

fun main() = runBlocking {flow {emit(1)println("emit 1 in thread ${Thread.currentThread().name}")emit(2)println("emit 2 in thread ${Thread.currentThread().name}")emit(3)println("emit 3 in thread ${Thread.currentThread().name}")}.flowOn(Dispatchers.IO).collect {println("Collected $it in thread ${Thread.currentThread().name}")}
}
//对应输出
emit 1 in thread DefaultDispatcher-worker-2
emit 2 in thread DefaultDispatcher-worker-2
emit 3 in thread DefaultDispatcher-worker-2
Collected 1 in thread main
Collected 2 in thread main
Collected 3 in thread mainProcess finished with exit code 0

默认不使用flowOn的情况下,Flow中所有代码都是执行在主线程调度器上的,当使用flowOn切换上下文环境后,flowOn 上游代码将执行在其所指定的上下文环境中,同cache操作符一样flowOn也与其位置是强相关的。

launchIn
用于启动流的收集操作的操作符

launchIn 操作符的语法如下:

flow.launchIn(scope)

其中,flow 是待收集的流,scope 是用于启动流收集的协程作用域。

下面通过两段示例感受下launchIn的作用:
示例1:

fun main() = runBlocking {val flow = flow {emit(1)emit(2)emit(3)}val job = launch(Dispatchers.Default) {flow.collect { value ->println("Collecting $value in thread ${Thread.currentThread().name}")}}delay(1000)job.cancel()
}

示例2:

fun main() = runBlocking(Dispatchers.Default) {val flow = flow {emit(1)emit(2)emit(3)}flow.flowOn(Dispatchers.IO).onEach {println("Flow onEach $it in thread ${Thread.currentThread().name}")}.launchIn(this)delay(1000)
}

launchIn源码

public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {collect() // tail-call
}

上面代码中的onEach操作符的作用是对流中的每个元素进行处理,而不改变流中的元素。它类似于其他编程语言中的 forEach 或 map 操作,但 onEach 不会返回修改后的流,而是继续返回原始流。

由于launchIn调用了collect(),所以它也是一个终止操作符,上面两种方法都可以切换collect中的上下文环境,看起来是感觉怪怪的哈,因为是在协程作用域中使用withContext{}不更方便吗?不过launchIn更大的作用是让其他操作符如 collect{}filter{}等都运行在指定上下文环境中。

如果上面两个示例不好理解在看下这两个

示例1:

fun main() = runBlocking {val scope = CoroutineScope(Dispatchers.IO)val flow = flow {emit(1)println("Flow emit 1 in thread ${Thread.currentThread().name}")emit(2)println("Flow emit 2 in thread ${Thread.currentThread().name}")emit(3)println("Flow emit 3 in thread ${Thread.currentThread().name}")}flow.filter {println("Flow filter in thread ${Thread.currentThread().name}")it > 1}.onEach {println("Flow onEach $it in thread ${Thread.currentThread().name}")}.collect()delay(1000)
}
//对应输出
Flow filter in thread main
Flow emit 1 in thread main
Flow filter in thread main
Flow onEach 2 in thread main
Flow emit 2 in thread main
Flow filter in thread main
Flow onEach 3 in thread main
Flow emit 3 in thread mainProcess finished with exit code 0

示例2:

//只是把上面collect 换成了launchIn(scope)flow.filter {println("Flow filter in thread ${Thread.currentThread().name}")it > 1}.onEach {println("Flow onEach $it in thread ${Thread.currentThread().name}")}.launchIn(scope)
//对应输出
Flow filter in thread DefaultDispatcher-worker-1
Flow emit 1 in thread DefaultDispatcher-worker-1
Flow filter in thread DefaultDispatcher-worker-1
Flow onEach 2 in thread DefaultDispatcher-worker-1
Flow emit 2 in thread DefaultDispatcher-worker-1
Flow filter in thread DefaultDispatcher-worker-1
Flow onEach 3 in thread DefaultDispatcher-worker-1
Flow emit 3 in thread DefaultDispatcher-worker-1Process finished with exit code 0

这就一目了然了吧

需要注意:由于 Flow 当中直接使用 withContext 是很容易引发其他问题的,因此,withContext 在 Flow 当中是不被推荐的,即使要用,也应该谨慎再谨慎。

终止操作符

Flow中的终止操作符包括下面几种

  • collect: 收集流中的元素并执行相应操作。
  • toList, toSet: 将流收集为列表或集合。
  • reduce, fold: 使用给定的累加器函数将流中的元素合并为单个值。

需要注意终止操作符后面不能再点出来其他操作符,只能是Flow的最后一个操作符!

Flow为什么被称为 “冷” 的?与Channel 的区别是什么?

Flow 被称为“冷”的主要原因是它是一种惰性的数据流。冷流意味着当没有收集者订阅该流时,它是不会产生任何数据的。Flow 的执行是由收集者的需求来驱动的,只有当有一个或多个收集者订阅了 Flow,并调用了 collect 等收集操作时,Flow 才会开始发射数据。

Flow与Channel 特性:

  • Flow 是惰性的数据流:Flow 是一种基于协程的异步数据流处理库,在 Kotlin 中引入了响应式编程的思想。与其他响应式流框架(如 RxJava)相比,Flow 是惰性的,只有在有收集者订阅时才会开始发射数据。这使得 Flow 非常适合处理潜在的无限序列或需要异步处理的大量数据。

  • Channel 是热的通道:ChannelKotlin 中用于协程之间进行通信和协同工作的机制。与 Flow 不同,Channel 是热的,即使没有接收者,它仍会持续发射数据。它可以用于多个协程之间传递数据、进行异步消息传递等情况。

区别:

  • Flow 是基于被动订阅的模型,数据的发射是由收集者的需求驱动的。每个收集者独立地订阅 Flow,可以按自己的节奏处理数据。

  • Channel 是主动推送数据的模型,数据的发送和接收是显式进行的。发送者可以将数据放入 Channel,而接收者通过调用 Channelreceive()函数主动获取数据。

适用场景

  • Flow 适合处理异步数据流,例如网络请求结果、数据库查询结果等。它提供了各种操作符(如 mapfiltertransform 等)来转换和处理数据流,同时支持背压(backpressure)处理,以避免生产者与消费者之间的压力失衡。

  • Channel 适合多个协程之间的通信和协同工作。它允许协程之间异步地发送和接收数据,可以用于实现生产者-消费者模型、事件驱动模型等。


感谢:朱涛 · Kotlin 编程第一课

由于是初学者,对协程方面见解不深,如有描述错误的地方,欢迎批评指正,不吝赐教

相关文章:

Kotlin 协程与 Flow

简介 Kotlin的Flow 是 Kotlin 在异步编程方面的一个重要组件&#xff0c;它提供了一种声明式的、可组合的、基于协程的异步编程模型。Flow 的设计灵感来自于 Reactive Streams、RxJava、Flux 和其他异步编程库&#xff0c;但它与 Kotlin 协程无缝集成&#xff0c;并提供了一种更…...

设备管理系统与物联网的融合:实现智能化设备监控和维护

在数字化时代&#xff0c;设备管理系统和物联网技术的融合为工业企业带来了巨大的变革和创新。本文将探讨设备管理系统与物联网的融合&#xff0c;重点介绍设备健康管理平台在实现智能化设备监控和维护方面的关键作用和优势。 一、设备管理系统与物联网的融合 随着物联网技术的…...

三、从官方源码精简出第1个FreeRTOS

1、官方源码下载 (1)进入FreeRTOS官网&#xff1a;FreeRTOS官网 (2)点击下载FreeRTOS。 (3)选择待示例的项目进行下载。 2、删减目录 (1)下载后解压的FreeRTOS文件如下图所示。 (2)删除下图中红框勾选的文件。 (3)删除"FreeRTOSv202212.01\FreeRTOS\Demo"目录下用…...

__call__函数的用法

__call__的用法 在 Python 中&#xff0c;类可以通过定义特殊方法 call 来使其实例对象可调用&#xff0c;就像调用普通的函数一样。当你在实例对象后面加上括号并传递参数时&#xff0c;Python 会自动调用这个对象的 call 方法。这使得你可以将类的实例对象当作函数来使用。 下…...

golang定时任务库cron实践

简介 cron一个用于管理定时任务的库&#xff0c;用 Go 实现 Linux 中crontab这个命令的效果。之前我们也介绍过一个类似的 Go 库——gron。gron代码小巧&#xff0c;用于学习是比较好的。但是它功能相对简单些&#xff0c;并且已经不维护了。如果有定时任务需求&#xff0c;还…...

Julia 流程控制

流程控制语句通过程序设定一个或多个条件语句来实现。在条件为 true 时执行指定程序代码&#xff0c;在条件为 false 时执行其他指定代码。 Julia 提供了大量的流程控制语句&#xff1a; 复合表达式&#xff1a;begin 和 ;。 条件表达式&#xff1a;if-elseif-else 和 ?: (三…...

问题解决方案

前端开发 1、npm安装的时候老是卡住 reify:rxjs: timing reifyNode:node_modules/vue/cli/node_modules 查看当前使用的那个镜像 nrm lsnpm ---------- https://registry.npmjs.org/yarn --------- https://registry.yarnpkg.com/cnpm --------- https://r.cnpmjs.org/taobao …...

kubernetes基于helm部署gitlab-operator

kubernetes基于helm部署gitlab-operator 这篇博文介绍如何在 Kubernetes 中使用helm部署 GitLab-operator。 先决条件 已运行的 Kubernetes 集群负载均衡器&#xff0c;为ingress-nginx控制器提供EXTERNAL-IP&#xff0c;本示例使用metallb默认存储类&#xff0c;为gitlab p…...

ChatGPT在在线客服和呼叫中心中的应用如何?

ChatGPT在在线客服和呼叫中心领域中有广泛的应用潜力&#xff0c;可以帮助企业提供更高效、个性化和满意度更高的客户服务体验。以下是详细的讨论&#xff1a; **1. 自动化客服&#xff1a;** ChatGPT可以用于自动化客服流程&#xff0c;通过自动回复用户的常见问题和查询&…...

C++多线程环境下的单例类对象创建

使用C无锁编程实现多线程下的单例模式 贺志国 2023.8.1 在多线程环境下创建一个类的单例对象&#xff0c;要比单线程环境下要复杂很多。下面介绍在多线程环境下实现单例模式的几种方法。 一、尺寸较小的类单例对象创建 如果待创建的单例类SingletonForMultithread内包含的成…...

“深入解析JVM内部机制:从字节码到垃圾回收“

标题&#xff1a;深入解析JVM内部机制&#xff1a;从字节码到垃圾回收 摘要&#xff1a;本文将从字节码生成、类加载、运行时数据区域和垃圾回收等方面深入解析JVM的内部机制&#xff0c;并通过示例代码展示其工作原理和实践应用。 正文&#xff1a; 一、字节码生成 JVM是基…...

音频系统项目与音频算法研究方向分类

+我V hezkz17进数字音频系统研究开发交流答疑群(课题组) 音频系统项目与音频算法研究方向分类 一 音频系统项目产品分类 1 收音机,数字收音机,复读机 2 耳机,蓝牙耳机,TWS蓝牙耳机, 3 立体声音箱,AI智能音箱, 4 音频功放,车载功放, 5 音响,普通音响,Soundbar音响…...

单例模式和工厂模式

目录 今日良言&#xff1a;关关难过关关过&#xff0c;步步难行步步行 一、单例模式 1.饿汉模式 2.懒汉模式 二、工厂模式 今日良言&#xff1a;关关难过关关过&#xff0c;步步难行步步行 一、单例模式 首先来解释一下&#xff0c;什么是单例模式。 单例模式也就是单个…...

两个镜头、视野、分辨率不同的相机(rgb、红外)的视野校正

文章目录 背景实际效果查找资料资料1资料2 解决方案最终结果 背景 目前在做的项目用到两个摄像头&#xff0c;一个是热成像摄像头、另一个是普通的rgb摄像头。 一开始的目标是让他们像素级重合&#xff0c;使得点击rgb图像时&#xff0c;即可知道其像素对应的温度。但是在尝试…...

kettle 连接jdbc

DM JDBC连接 oracle JDBC连接 PG JDBC连接 SQLSERVER JDBC连接...

PyTorch中加载模型权重 A匹配B|A不匹配B

在做深度学习项目时&#xff0c;从头训练一个模型是需要大量时间和算力的&#xff0c;我们通常采用加载预训练权重的方法&#xff0c;而我们往往面临以下几种情况&#xff1a; 未修改网络&#xff0c;A与B一致 很简单&#xff0c;直接.load_state_dict() net ANet(num_cla…...

@FeignClient指定多个url实现负载均衡

C知道回答的如下&#xff1a; 在使用 FeignClient 调用多个 URL 实现负载均衡时&#xff0c;可以使用 Spring Cloud Ribbon 提供的功能来实现。下面是一个示例代码&#xff1a; 首先&#xff0c;在Spring Boot主类上添加EnableFeignClients注解启用Feign Client功能。 Spring…...

vue diff 双端比较算法

文章目录 双端指针比较策略命中策略四命中策略二命中策略三命中策略一未命中四种策略&#xff0c;遍历旧节点列表新增情况一新增情况二 删除节点双端比较的优势 双端指针 使用四个变量 oldStartIdx、oldEndIdx、newStartIdx 以及 newEndIdx 分别存储旧 children 和新 children …...

初识React: 基础(概念 特点 高效原因 虚拟DOM JSX语法 组件)

1.什么是React? React是一个由Facebook开源的JavaScript库&#xff0c;它主要用于构建用户界面。React的特点是使用组件化的思想来构建界面&#xff0c;使得代码的可复用性和可维护性大大提高。React还引入了虚拟DOM的概念&#xff0c;减少了对真实DOM的直接操作&#xff0c;…...

自监督去噪:Neighbor2Neighbor原理分析与总结

文章目录 1. 方法原理1.1 先前方法总结1.2 Noise2Noise回顾1.3 从Noise2Noise到Neighbor2Neighbor1.4 框架结构2. 实验结果3. 总结 文章链接&#xff1a;https://arxiv.org/abs/2101.02824 参考博客&#xff1a;https://arxiv.org/abs/2101.02824 1. 方法原理 1.1 先前方法总…...

iPhone密码忘记了办?iPhoneUnlocker,iPhone解锁工具Aiseesoft iPhone Unlocker 高级注册版​分享

平时用 iPhone 的时候&#xff0c;难免会碰到解锁的麻烦事。比如密码忘了、人脸识别 / 指纹识别突然不灵&#xff0c;或者买了二手 iPhone 却被原来的 iCloud 账号锁住&#xff0c;这时候就需要靠谱的解锁工具来帮忙了。Aiseesoft iPhone Unlocker 就是专门解决这些问题的软件&…...

家政维修平台实战20:权限设计

目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系&#xff0c;主要是分成几个表&#xff0c;用户表我们是记录用户的基础信息&#xff0c;包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题&#xff0c;不同的角色&#xf…...

五年级数学知识边界总结思考-下册

目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解&#xff1a;由来、作用与意义**一、知识点核心内容****二、知识点的由来&#xff1a;从生活实践到数学抽象****三、知识的作用&#xff1a;解决实际问题的工具****四、学习的意义&#xff1a;培养核心素养…...

苍穹外卖--缓存菜品

1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得&#xff0c;如果用户端访问量比较大&#xff0c;数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据&#xff0c;减少数据库查询操作。 缓存逻辑分析&#xff1a; ①每个分类下的菜品保持一份缓存数据…...

LLM基础1_语言模型如何处理文本

基于GitHub项目&#xff1a;https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken&#xff1a;OpenAI开发的专业"分词器" torch&#xff1a;Facebook开发的强力计算引擎&#xff0c;相当于超级计算器 理解词嵌入&#xff1a;给词语画"…...

Spring AI与Spring Modulith核心技术解析

Spring AI核心架构解析 Spring AI&#xff08;https://spring.io/projects/spring-ai&#xff09;作为Spring生态中的AI集成框架&#xff0c;其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似&#xff0c;但特别为多语…...

ABAP设计模式之---“简单设计原则(Simple Design)”

“Simple Design”&#xff08;简单设计&#xff09;是软件开发中的一个重要理念&#xff0c;倡导以最简单的方式实现软件功能&#xff0c;以确保代码清晰易懂、易维护&#xff0c;并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计&#xff0c;遵循“让事情保…...

回溯算法学习

一、电话号码的字母组合 import java.util.ArrayList; import java.util.List;import javax.management.loading.PrivateClassLoader;public class letterCombinations {private static final String[] KEYPAD {"", //0"", //1"abc", //2"…...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

(一)单例模式

一、前言 单例模式属于六大创建型模式,即在软件设计过程中,主要关注创建对象的结果,并不关心创建对象的过程及细节。创建型设计模式将类对象的实例化过程进行抽象化接口设计,从而隐藏了类对象的实例是如何被创建的,封装了软件系统使用的具体对象类型。 六大创建型模式包括…...