Compose 实践与探索二 —— 状态订阅与自动更新1
1、自定义 Composable
为什么所有组件都要加 @Composable 注解才可以使用?
这是因为 Compose 需要通过 Compose 的编译器插件(Compose Compiler Plugin)在组件函数中增加一些参数,这些参数在调用时有用。通过编译器增加这些参数,既可以在调用时可以被程序正确使用,又不用使用者手动添加(省事了),一举两得。
这实际上是一种面向切面编程(AOP)的实例,通常 AOP 会借助以下两者之一:
- Annotation Processor
- 修改字节码(字节码插桩)
Compose 没有使用以上两种技术方案,主要是因为 Compose 要跨平台,而上述两种技术方案都是针对 JVM 的,在其他平台上无效。其次,编译器插件的功能要比 Annotation Processor 更强大。
说回来,那么 Compose 编译器插件是如何知道应该修改哪些函数的呢?就是通过 @Composable 注解,该注解起到了一个识别符的作用。
总结一下,由于 Compose 编译器插件要对组件函数进行修改,注解可以帮助编译器正确识别那些需要被修改的函数。
被 @Composable 修饰的函数,只能在同样被 @Composable 修饰的函数中调用,这样到了最上层的 setContent,我们能看到,该函数虽然自身没有被 @Composable 修饰,但是其将闭包内容封装为 @Composable 函数进行调用也是可以的:
public fun ComponentActivity.setContent(parent: CompositionContext? = null,content: @Composable () -> Unit
) {...
}
在执行这个根部的 @Composable 函数时,实际上是将其强转为一个 Function2 类型的函数 invokeComposable(),然后直接调用(后续讲 setContent 函数时会详解)。
@Composable 函数只能在 @Composable 函数中被调用,这一点与挂起函数只能在协程或其他挂起函数中调用的规则很相似。前者是通过 Composer 编译器插件实现的,后者是 Kotlin 编译器实现的。
加了 @Composable 注解的函数会被简称为 Composable 或 Composable 函数。
2、MutableState 和 mutableStateOf()
Compose 对 UI 进行一次声明,后续当数据发生变化时,会自动更新到 UI 上,而无需像传统 View 体系那样,以命令式的方式让 UI 更新数据。
上述描述隐含着一个条件,就是只有可以被监听的数据,在其发生变化时,监听者才能拿到变化后的数据去更新 UI。所以数据不能是普通的变量,而需要用 mutableStateOf() 来创建一个 MutableState:
val name = mutableStateOf("James")
通过 value 获取属性值:
Text(name.value)
接下来探查自动订阅是如何形成的。
mutableStateOf() 的返回值类型是 MutableState 这个接口类型:
@Stable
interface MutableState<T> : State<T> {override var value: Toperator fun component1(): Toperator fun component2(): (T) -> Unit
}
而提供 MutableState 的具体类型的是 createSnapshotMutableState():
fun <T> mutableStateOf(value: T,policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)
createSnapshotMutableState() 返回的是 MutableState 的子接口 SnapshotMutableState 的实现类的子类对象 ParcelableSnapshotMutableState:
internal actual fun <T> createSnapshotMutableState(value: T,policy: SnapshotMutationPolicy<T>
): SnapshotMutableState<T> = ParcelableSnapshotMutableState(value, policy)
ParcelableSnapshotMutableState 继承了 SnapshotMutableStateImpl 也实现了 Parcelable,对 MutableState 内 value 属性的实现就来自于 SnapshotMutableStateImpl:
internal open class SnapshotMutableStateImpl<T>(value: T,override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {@Suppress("UNCHECKED_CAST")override var value: Tget() = next.readable(this).valueset(value) = next.withCurrent {if (!policy.equivalent(it.value, value)) {next.overwritable(this, it) { this.value = value }}}
}
后续关注 value 的 getter 和 setter。
Compose 的界面刷新包括组合(Composition)、布局、绘制三个过程。其中组合是指利用 Composable 函数拼凑出页面实际内容(各个组件的 LayoutNode)的过程。Composable 函数本身并不是界面的元素,而是用于生成界面元素的。
2.1 getter
MutableState -> StateObject -> StateRecord -> Compose 支持事务功能(简单理解为可以批量进行、可以撤销、并发进行并且可以进行事后合并)
事务功能包含撤销,撤销需要保持旧值,因此 Compose 的变量管理需要对同一个变量保存多个新旧值。
Compose 使用链表来保存 StateRecord,只需在 StateRecord 中保存 StateRecord 链表的头节点 firstStateRecord 即可获取整个链表。
分析 value 的 get() 都做了哪些事:
override var value: T// 订阅,取值返回get() = next.readable(this).value
首先,这个 next 的类型是 StateStateRecord,该类型是 StateRecord 的子类:
internal open class SnapshotMutableStateImpl<T>(value: T,override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {private var next: StateStateRecord<T> = StateStateRecord(value)// 链表头的取值为 nextoverride val firstStateRecord: StateRecordget() = next// StateStateRecord 是 StateRecord 的子类private class StateStateRecord<T>(myValue: T) : StateRecord() {override fun assign(value: StateRecord) {@Suppress("UNCHECKED_CAST")this.value = (value as StateStateRecord<T>).value}override fun create(): StateRecord = StateStateRecord(value)var value: T = myValue}
}
在 next 上调用的 readable() 是以 StateRecord 为上限的类型的扩展函数,虽然最终返回的就是该扩展类型的对象,但是在返回前做了一些事:
/**
* 返回当前快照的当前可读状态记录。假定[this]是[state]的第一个记录。
*/
fun <T : StateRecord> T.readable(state: StateObject): T {val snapshot = Snapshot.current// 这行代码的逻辑很深,暂且不深入追溯,先只说说它做了什么。当外界访问 value 的值时会调用其 get()// 进而执行到 readable(),此时要记录(实际上是订阅)下这个 value 所属的 state 被访问/使用过。// 这些被访问过的 state 被标记为失效,在下一帧画面要刷新时会进行重组(Recompose)。snapshot.readObserver?.invoke(state)// 三参数 readable() 会拿到可用的中最新的 StateRecordreturn readable(this, snapshot.id, snapshot.invalid) ?: sync {// 当全局快照已被另一个线程推进,并且在该线程暂停期间写入对象的状态被覆盖时,可读状态可能// 返回 null。在这里重复读取是有效的,因为要么这将返回与上一次调用相同的结果,要么将找到// 一个有效记录。在 sync 块中阻止其他线程写入该状态对象,直到读取完成。val syncSnapshot = Snapshot.currentreadable(this, syncSnapshot.id, syncSnapshot.invalid)} ?: readError()
}
在这个过程中,需要注意的一件事是:mutableStateOf() 得到一个 ParcelableSnapshotMutableState 对象,该对象继承了 SnapshotMutableStateImpl,这个实现类实现了 SnapshotMutableState 接口:
/**
* 一个可变的值持有者,在[Composable]函数执行期间对[value]属性的读取,当前的[RecomposeScope]将订阅
* 该值的更改。当[value]属性被写入并更改时,任何订阅的[RecomposeScope]将被安排重新组合。对其的写入作为
* [Snapshot]系统的一部分进行事务处理。
*/
interface SnapshotMutableState<T> : MutableState<T> {/*** 控制在可变快照中如何处理更改的策略。*/val policy: SnapshotMutationPolicy<T>
}
也实现了 StateObject 接口:
/**
* 所有快照感知状态对象都实现的接口。被该模块用于维护状态对象的状态记录。
*/
@JvmDefaultWithCompatibility
interface StateObject {/*** 状态记录链表中的第一个状态记录。*/val firstStateRecord: StateRecord/*** 在列表的开头添加一个新的状态记录。调用此方法后,[firstStateRecord]应为[value]。*/fun prependStateRecord(value: StateRecord)/*** 基于冲突的状态更改生成合并的状态。** 该方法不得修改接收到的任何记录,并应将状态记录视为不可变的,即使是[applied]记录也是如此。** @param previous 用于创建[applied]记录的状态记录,也是产生[current]记录的状态(虽然间接)。** @param current 父快照或全局状态的状态记录。** @param applied 正在应用的父快照或全局状态的状态记录。** @return 修改后的状态,如果值无法合并则为null。如果无法合并状态,则当前应用将失败。* 任何参数都可以作为结果返回。* 如果不是参数值之一,则必须通过调用传递的记录之一上的[StateRecord.create]来创建一个新值,* 然后可以修改为合并值后返回。* 如果返回一个新记录,[MutableSnapshot.apply]将更新内部快照ID并调用* [prependStateRecord](如果使用了该记录)。*/fun mergeRecords(previous: StateRecord,current: StateRecord,applied: StateRecord): StateRecord? = null
}
ParcelableSnapshotMutableState 可以被订阅的能力不是来自于 SnapshotMutableState,而是 StateObject。不要被类的名字所迷惑,看起来似乎是由 SnapshotMutableState 提供订阅功能的。但是你看 SnapshotMutableState 及其父接口 MutableState 都没有提供这种能力。
总结:为什么 mutableStateOf() 返回的对象可以被订阅?因为它内部的 value 属性的 get 函数被定制为每次取值时先进行记录操作,记录下这个值在哪里被取用了,然后从保存的众多值当中取到最新的可用值返回。
2.2 setter
再看 setter 的实现:
override var value: Tset(value) = next.withCurrent { // it: StateStateRecord<T>if (!policy.equivalent(it.value, value)) {next.overwritable(this, it) { this.value = value }}}
next 已经知道什么了,直接看它的 withCurrent(),该函数会直接调用它参数上的 block 函数:
// block 函数的参数类型与 withCurrent() 的接收者类型相同
inline fun <T : StateRecord, R> T.withCurrent(block: (r: T) -> R): R =block(current(this))
其中 current() 会取到一个最新的 StateRecord:
@PublishedApi
internal fun <T : StateRecord> current(r: T) =Snapshot.current.let { snapshot ->// 三个参数的 readable(),只取值,不订阅readable(r, snapshot.id, snapshot.invalid) ?: sync {Snapshot.current.let { syncSnapshot ->readable(r, syncSnapshot.id, syncSnapshot.invalid)}} ?: readError()}
因此 set() 的逻辑就是先判断新旧值是否相等,如果不等再调用 overwritable():
internal inline fun <T : StateRecord, R> T.overwritable(state: StateObject,candidate: T,block: T.() -> R
): R {var snapshot: Snapshot = snapshotInitializerreturn sync {snapshot = Snapshot.currentthis.overwritableRecord(state, snapshot, candidate).block()}.also {notifyWrite(snapshot, state)}
}
这一步主要看 overwritableRecord():
internal fun <T : StateRecord> T.overwritableRecord(state: StateObject,snapshot: Snapshot,candidate: T
): T {if (snapshot.readOnly) {// If the snapshot is read-only, use the snapshot recordModified to report it.snapshot.recordModified(state)}val id = snapshot.id// 1.如果 candidate 的 snapshotId 与 snapshot 的 id 匹配,就返回 candidateif (candidate.snapshotId == id) return candidate// 2.如果上一步没返回,就生成一个新的 StateRecord 类型的数据并返回val newData = newOverwritableRecord(state)newData.snapshotId = idsnapshot.recordModified(state)return newData
}
StateRecord 每次修改前后的新旧值都会被存起来串成一个链表,链表上的各个节点都对应了某一时刻 Compose 的整个内部状态。Compose 记录每个变量的每个状态,用的是 StateRecord 的链表。而具体各个链表上的哪些节点属于同一个状态,它也有记录,这个记录就是 SnapShot。
SnapShot 的功能就是对 Compose 内的各个变量做快照。SnapShot 记录整个系统的状态,可以对应多个 StateRecord;而一个 StateRecord 只对应一个 SnapShot。
有了快照功能后,就可以在某些变量值发生变化的时候,不必马上将这个变化应用到界面上,而是在跑完整个 Compose 的流程之后,把所有发生变化的变量一起应用。直接拿着最终结果去进行接下来的布局和绘制,这样性能会好一些。SnapShot 对这种批量应用改变提供了底层的技术可行性支持。
当然,SnapShot 不止有这一个帮助,它还是 Compose 支持多线程同步对界面进行计算的下层技术支持。
- 系统有多个 SnapShot 时,它们是有先后关系的
- 同一个 StateObject 的每个 StateRecord 都有它们对应的 SnapShot 的 id。StateRecord 和 SnapShot 就算不直接对应,只要 StateRecord 的 SnapShot 对另一个是有效的,另一个就能取到这个 StateRecord
overwritable() 内还有一个 notifyWrite():
@PublishedApi
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {snapshot.writeObserver?.invoke(state)
}
该函数会去查找这个 state 在哪里被读取了,然后去读取它的位置将状态标记为失效。
get() 与 set() 并没有完成我们所认为的订阅工作,Compose 需要两套订阅机制同时工作才能完成订阅。
两个订阅过程:
- 对 SnapShot 中读写 StateObject 对象的订阅,分别订阅读和写,所以有两个接收者:readObserver 和 writeObserver。发生时间:订阅是在 SnapShot 创建时,通知是在读和写的时候
- 对每一个 StateObject 的应用做订阅。发生时间:订阅发生在第一个订阅的 readObserver 被调用(通知)的时候;通知发生在 StateObject 新值被应用的时候(后面讲)
最后讲的通过 by 替换 = 的用法(好处是不用每次获取与赋值时都调用 value 了),需要手动导入 getValue 和 setValue 这两个扩展函数。
3、Recompose Scope 和 remember()
Recompose Scope 是重组作用域,作用域越小,其内部包含的与重组无关的代码就越少,无用的工作也就做得少。
remember() 可以防止由于 Recompose 导致的预期之外的某些变量的反复初始化,反复初始化可能会带来意外的结果。
何时使用 remember()?这个问题取决于初始化代码是否会被包进 Recompose 的过程。即便在当前所在的组合函数中,能确保初始化过程不会进入重组,但是你不能确保调用这个组合函数的上级组合函数不会带着它一起进入重组,所以在 Compose 中,没有办法判断初始化是否会在重组过程中执行。既然无法判断,那么解决方案也就简单粗暴了,对所有使用了 mutableStateOf() 进行初始化的变量,都套上 remember() 即可。
remember() 起到缓存作用,防止多次初始化,全部包上就对了。
如果初始化在 Composable 函数外面,由于不会重组了,因此就用不到 remember() 了,并且你想写也写不了,因为它也是个 Composable 函数,不能在 Composable 函数之外的环境中调用。
remember() 是可以传数 key 的:
@Composable
inline fun <T> remember(key1: Any?,key2: Any?,crossinline calculation: @DisallowComposableCalls () -> T
): T
当被 remember() 括起来的内容发生变化时,需要重新计算,但如果不传参数 key,它不会重新计算。因此需要将在括号内参与计算的数据作为 remember() 的 key,这样一旦 key 发生变化,remember() 就会重新计算括号内的数据。
4、无状态、状态提升和单向数据流
4.1 什么是无状态
Compose 官方称其是无状态的(Stateless),这个状态是指组件属性。
比如说 TextView 内保存的文字内容就是一个状态,你可以通过 getText() 与 setText() 获取与设置文字。但在 Compose 中,组件没有状态,也就是其内部不会保存这些数据,在将数据设置到 UI 上之后,它们就被“扔掉了”。
但需要注意的是,无状态作为 Compose 的一个特点,它是允许组件无状态,而不是说组件绝对没有状态。看下面的例子:
@Composable
fun Hello() {var text = "Hello"Text(text)
}
text 变量存在于 Hello 函数中,因此 Hello 是有状态的,而 Text 内部没有保存任何变量,因此 Text 是无状态的。
确切地说,Compose 组件是内部无状态,但是状态可以存在于外部。
4.2 状态提升
那么,在 Compose 中,从外部如何获取某个组件的状态?可以分为两种情况:
- 对于无状态组件,由于组件内部无状态,而这个状态是存在于外部的(就好比上面示例中,Text 内部是无状态的,但是它的状态 text 是在外部提供的),因此可以直接在外部获取这个状态
- 对于有状态组件,由于状态在函数内是一个局部变量,从语言角度上说,你无法在一个函数的外部获取到该函数的局部变量,因此你可以效仿无状态组件,将状态提出到函数之外,将有状态组件变成无状态组件,然后像无状态组件那样获取状态
以上面的 Hello 函数为例,它内部的状态 text 可以提出到外部,在外部获取这个状态:
setContent {var text = "Hello"Hello(text)// 获取状态,也可以修改text = "HaHaHa"
}// 状态提出去之后,作为参数接收这个状态
@Composable
fun Hello(value: String) {Text(value)
}
Hello 在提取状态之后,从有状态组件变成了无状态组件。这种将状态从子组件移动到父组件,以便在整个组件层次结构中共享和管理状态的模式叫做状态提升(State Hoisting)。
状态提升有一个原则:尽量不往上提。因为状态提的越高,能访问该状态的范围就越广,代码出错的概率就越高。因此状态要尽可能地往下放。
对于可以互动的组件,除了提取状态之外,还需要提取交互的函数到外部。
4.3 单项数据流
当应用内的数据来源有多个渠道时,如何安排这些数据呢?
比如一个新闻应用,通过网络 + 本地数据库两种方式来展示数据:
- 第一次打开应用时,本地数据库是空的,从网络获取新闻列表,显示到 UI 上并存到数据库中
- 滑动到底部加载下一页数据,取到新的数据后合并到内存后显示到 UI 上,同时也要存入数据库中
- 杀死应用后重新打开,可以从本地数据库加载数据显示,同时从网络获取数据,获取之后合并到内存后显示,仍需要保存到数据库
第三种情况涉及到数据有效性的问题。通常数据库读取数据要比网络请求数据要快很多,因此先读取数据库的数据显示到 UI 上,然后等拿到网络请求的数据之后再显示网络数据。但是也有极端情况,可能网络数据比读取数据库的速度快,那么可能会出现数据库中较老的数据覆盖了网络请求的较新数据。
像这种双数据来源都需要解决这种数据有效性或数据同步性的问题。较为常用的解决方式是采用单数据来源,让多个数据源串行合并为单个数据源。比如这里我们就可以让网络数据源作为本地数据库的上游,即网络数据先存入数据库,数据库再为 UI 提供单一的数据来源,即单一信息源(Single Source of Truth)。
单一信息源是 Compose 官方建议使用的,当然这种模式在 Compose 之前的 Jetpack 开始就已经被 Google 官方推荐了(ViewModel 的 Repository 也有数据库和网络两个数据源,官方也是建议让网络数据存入数据库,数据库再为 UI 提供数据这种方式)。Compose 建议所有界面中会用到的数据都采用这种形式。
单向数据流(Unidirectional Data Flow)怎么用?把 Composable 函数做好封装,做状态提升时提的完整一点。如果有用户交互,在提状态时应该把与之相关的用户交互也往上提,即把用户事件做成函数类型以函数参数的形式暴露出来,把这个用户事件也交给上层来调用。
对于 Compose 而言,实现了单向数据流,也就实现了单一信息源。
相关文章:
Compose 实践与探索二 —— 状态订阅与自动更新1
1、自定义 Composable 为什么所有组件都要加 Composable 注解才可以使用? 这是因为 Compose 需要通过 Compose 的编译器插件(Compose Compiler Plugin)在组件函数中增加一些参数,这些参数在调用时有用。通过编译器增加这些参数&…...
linux下文件读写操作
Linux下,文件I/O是操作系统与文件系统之间进行数据传输的关键部分。文件I/O操作允许程序读取和写入文件,管理文件的打开、关闭、创建和删除等操作。 1. 文件描述符 在Linux中,每个打开的文件都由一个文件描述符来表示。文件描述符是一个非负…...
嵌入式学习第二十四天--网络 服务器
服务器模型 tcp服务器: socket bind listen accept recv/send close 1.支持多客户端访问 //单循环服务器 socket bind listen while(1) { accept while(1) { recv/send } } close 2.支持多客户端同时访问 (并发能力) 并发服务器 socket bind …...
Uniapp组件 Textarea 字数统计和限制
Uniapp Textarea 字数统计和限制 在 Uniapp 中,可以通过监听 textarea 的 input 事件来实现字数统计功能。以下是一个简单的示例,展示如何在 textarea 的右下角显示输入的字符数。 示例代码 首先,在模板中定义一个 textarea 元素ÿ…...
【Java 面试 八股文】计算机网络篇
操作系统篇 1. 什么是HTTP? HTTP 和 HTTPS 的区别?2. 为什么说HTTPS比HTTP安全? HTTPS是如何保证安全的?3. 如何理解UDP 和 TCP? 区别? 应用场景?3.1 TCP 和 UDP 的特点3.2 适用场景 4. 如何理解TCP/IP协议?5. DNS协议 是什么?说说DNS 完整的查询…...
Webservice创建
Webservice创建 服务端创建 3层架构 service注解(commom模块) serviceimpl(server) 服务端拦截器的编写 客户端拦截器 客户端调用服务端(CXF代理) 客户端调用服务端(动态模式调用&a…...
使用VS Code remote ssh进行远程开发的笔记
本文是在VS Code中使用 remote ssh 进行开发的笔记。 安装插件 打开VS Code,在扩展区找到remote相关插件,安装之。下图中红色框出来的是已经安装了的插件(圆圈处即为Remote Explorer)。 实践 连接服务器 新建连接:…...
C语言每日一练——day_3(快速上手C语言)
引言 针对初学者,每日练习几个题,快速上手C语言。第三天。(会连续更新) 采用在线OJ的形式 什么是在线OJ? 在线判题系统(英语:Online Judge,缩写OJ)是一种在编程竞赛中用…...
Linux基本操作指令4
1、查看Ubuntu的版本 lsb_release -a 2、在 Ubuntu 下安装 OpenGL Library sudo apt-get install libglu1-mesa-dev 3、终止当前运行的进程 Ctrl C//默认情况 Ctrl Shift C//若修改了复制快捷键为CtrlC的情况 4、快速打开终端 CtrlAltT 5、关闭终端 Ctrl Shift W…...
PostgreSQL - Windows PostgreSQL 下载与安装
Windows PostgreSQL 下载与安装 1、PostgreSQL 下载 下载地址:https://www.enterprisedb.com/downloads/postgres-postgresql-downloads 2、PostgreSQL 安装 启动安装程序 -> 点击 【Next】 指定安装路径 -> 点击 【Next】 默认勾选 -> 点击 【Next】 指…...
JVM 的主要组成部分及其作用?
创作内容丰富的干货文章很费心力,感谢点过此文章的读者,点一个关注鼓励一下作者,激励他分享更多的精彩好文,谢谢大家! JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执…...
华为eNSP:配置P2P网络类型
一、什么是P2P网络类型 P2P(Point-to-Point)网络类型 是 OSPF(开放最短路径优先)协议中的一种网络类型,用于描述两个路由器之间直接相连的点对点链路。P2P 网络类型通常用于串行链路(如 PPP 或 HDLC 封装&…...
通过数据集微调LLM后怎么调用
通过数据集微调LLM后怎么调用 1. 导入必要的库 from transformers import AutoTokenizer, AutoModelForCausalLMAutoTokenizer:这是 transformers 库中的一个实用类,它能够根据指定的模型名称或路径自动选择合适的分词器。分词器的主要作用是将输入的文本字符串转换为模型可…...
thinkphp+mysql+cast解决text类型字段的文本型数字排序错误的方法 - 数据库文本字段排序ASC、DESC的失效问题
TP中使用cast order $lists AmdCommonTable::where(..............) ->field(*,CAST(w6 AS UNSIGNED) as sort) ->order(sort, asc) ->select() ->toArray(); 先转换为数字,再order by 效果对比 (1/2) 不ok - 直接order by 某字段 asc - 只能按照文本…...
【Manus资料合集】激活码内测渠道+《Manus Al:Agent应用的ChatGPT时刻》(附资源)
DeepSeek 之后,又一个AI沸腾,冲击的不仅仅是通用大模型。 ——全球首款通用AI Agent的破圈启示录 2025年3月6日凌晨,全球AI圈被一款名为Manus的产品彻底点燃。由Monica团队(隶属中国夜莺科技)推出的“全球首款通用AI…...
C++----红黑树map和set的封装
一、红黑树 1.概念 红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出2倍࿰…...
【报错】微信小程序预览报错”60001“
1.问题描述 我在微信开发者工具写小程序时,使用http://localhost:8080是可以请求成功的,数据全都可以无报错,但是点击【预览】,用手机扫描二维码浏览时,发现前端图片无返回且报错60001(打开开发者模式查看日…...
软考 数据通信基础——信道
信道特性 带宽 在模拟信号里频率的差,表示信道能通过的频率 在数字信号里表示最大传输速率,单位用bit/s 通常用W表示 波特率 即码元速率,码元可看作一个时间周期 码元速率B2W也可写成B1/T 码元种类n和码元信息量个数N存在以下关系 Nl…...
windows 平台如何点击网页上的url ,会打开远程桌面连接服务器
你可以使用自定义协议方案(Protocol Scheme)实现网页上点击URL后自动启动远程桌面连接(mstsc),参考你提供的C代码思路,如下实现: 第一步:注册自定义协议 使用类似openmstsc://协议…...
uni-app开发的App和H5嵌套封装的App,以及原生App有什么区别
uni-app 开发的 App 和 H5 嵌套封装的 App 是两种不同的开发模式,虽然它们都可以实现跨平台开发,但在技术实现、性能、功能支持等方面有显著区别。以下是详细对比: 1. uni-app 开发的 App uni-app 是一个基于 Vue.js 的跨平台开发框架&#…...
Anaconda中虚拟环境安装g++和gcc相同版本
安装torchSDF的时候遇到的,这是g和gcc版本不一致的问题 gcc: fatal error: cannot execute cc1plus: execvp: No such file or directory compilation terminated.查看gcc, g版本 gcc --version | head -n1 g --version | head -n1发现gcc的是anaconda中的&#x…...
Docker数据管理,端口映射与容器互联
1.Docker 数据管理 在生产环境中使用 Docker,往往需要对数据进行持久化,或者需要在多个容器之间进行数据共享,这必然涉及容器的数据管理操作。 容器中的管理数据主要有两种方式: 数据卷(Data Volumns)&a…...
部署前后端项目
部署项目 liunx 软件安装 软件安装方式 在Linux系统中,安装软件的方式主要有四种,这四种安装方式的特点如下: 建议nginx、MySQL、Redis等等使用docker安装,会很便捷,这里只演示JDK、ngxin手动的安装 安装JDK 上述我…...
从零构建逻辑回归: sklearn 与自定义实现对比
文章目录 理论基础1. 逻辑回归模型2. 损失函数3. 梯度推导(1) 计算 ∂ L ∂ y ^ \frac{\partial L}{\partial \hat{y}} ∂y^∂L(2) 计算 ∂ y ^ ∂ z \frac{\partial \hat{y}}{\partial z} ∂z∂y^(3) 计算 ∂ L ∂ z \frac{\partial L}{\partial z} ∂z∂L(4) 计…...
1256:献给阿尔吉侬的花束--BFS多组输入--memset
1256:献给阿尔吉侬的花束--BFS多组输入--memset 题目 解析代码【结构体】用book标记且计步数的代码[非结构体法] 题目 解析 标准的BFS题目,在多组输入中要做的就是先找到这一组的起点和终点,然后将其传给bfs,在多组输入中最易忘记…...
【JavaEE】SpringBoot快速上手,探秘 Spring Boot,搭建 Java 项目的智慧脚手架
1.Spring Boot介绍 在学习SpringBoot之前, 我们先来认识⼀下Spring ,我们看下Spring官⽅的介绍 可以看到,Spring让Java程序更加快速, 简单和安全。 Spring对于速度、简单性和⽣产⼒的关注使其成为世界上最流⾏的Java框架。 Spring官⽅提供了很多开源的…...
【C】初阶数据结构9 -- 直接插入排序
前面我们学习了数据结构二叉树,接下来我们将开启一个新的章节,那就是在日常生活中经常会用到的排序算法。 所谓排序算法就是给你一堆数据,让你从小到大(或从大到小)的将这些数据排成一个有序的序列(这些数据…...
Lottie与LottieFiles:快速为前端Web开发注入精美动画的利器
目录 Lottie与LottieFiles:快速为前端Web开发注入精美动画的利器 一、Lottie是什么?从GIF到JSON的动画技术演进 1、传统动画臃肿的Gif 2、Lottie的突破性创新 二、Lottie的核心组件解析(Lottie的技术架构) 1、Lottie核心三要…...
Spring boot创建时常用的依赖
新建SpringBoot Maven项目中pom常用依赖配置及常用的依赖的介绍 1.springboot项目的总(父)依赖大全 <parent><artifactId>spring-boot-dependencies</artifactId><groupId>org.springframework.boot</groupId><version>2.3.3.RELEASE<…...
音乐API
https://neteasecloudmusicapi.vercel.app/docs/#/https://neteasecloudmusicapi.vercel.app/docs/#/ 使用实例 所有榜单内容摘要 说明 : 调用此接口,可获取所有榜单内容摘要 接口地址 : /toplist/detail 调用例子 : /toplist/detail 获取歌单所有歌曲 说明 : 由于网易云…...
