(二)Jetpack Compose 布局模型
前文回顾
(一)Jetpack Compose 从入门到会写-CSDN博客
首先让我们回顾一下上一篇文章中里提到过几个问题:
-
ComposeView的层级关系,互相嵌套存在的问题?
-
为什么Compose可以实现只测量一次?
ComposeView和原生View互相嵌套存在的问题?
Compose 天然就支持被原生 View 嵌套,但也支持嵌套原生 View。
通过demo验证一下:FrameLayout内部嵌套一个ComposeView,ComposeView内部再嵌套一个TextView。
通过LayoutInspector可以看到层级结构如下:
1.层级结构问题
可以看到Column和Text组件并没有出现在布局层级中,跟Compose相关的层级只有ComposeView与AndroidComposeView两个View。
由此可以判断,Compose框架是通过一个ComposeView为入口来加入到Android现有的视图体系中,但是它自己内部的布局和渲染逻辑脱离了View原本的框架体系,因此不能被LayoutInspector捕捉到。
2.刷新时机问题
由于Android原生UI框架是基于事件过程更新,Compose框架基于状态变化更新,所以它们的更新逻辑是互相独立的。这里分两种情况看待。
2.1 在Android自定义View中嵌套ComposeView:
由于ComposeView内部的组合是基于状态变化的,所以只有当ComposeView的状态改变时,它才会重新组合(Composition)。如果Android自定义View的更新与ComposeView的状态无关,那么ComposeView不会自动刷新。
2.2 在Compose中嵌套Android自定义View(AndroidView):
由于Compose是声明式的,所以状态变化会驱动UI的更新。当依赖的状态变化时,AndroidView收到update回调,在这里处理自己是否需要更新。
2.1和2.2的结论,可以通过demo验证:
class ComposeOriginTestActivity : AppCompatActivity() {private var nowindex = 0var nowIndexM by mutableStateOf(0)override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_origin_test)// 创建并添加ComposeViewval composeView = ComposeView(this).apply {/*** 组合策略,解决重组性能问题* ViewCompositionStrategy.Default 会在底层 ComposeView 从窗口分离时释放组合,* 除非它是池容器(例如RecyclerView)的一部分。*/setViewCompositionStrategy(ViewCompositionStrategy.Default)setContent {MyComposable()}}val container = findViewById<FrameLayout>(R.id.container)// 添加到LinearLayoutcontainer.addView(composeView)findViewById<Button>(R.id.test_update).setOnClickListener {//验证case1: AndroidView正常刷新,ComposeView不刷新
// nowindex++
// findViewById<TextView>(R.id.test_tv).setText(nowindex.toString())
// container.postInvalidate()//验证case2:AndroidView不刷新,ComposeView正常刷新nowIndexM ++findViewById<TextView>(R.id.test_tv).setText(nowIndexM.toString())container.postInvalidate()}}@Composablefun MyComposable() {Column {MLog.d("Compose 更新:$nowindex $nowIndexM")Text("Compose Text :$nowindex $nowIndexM")AndroidView(modifier = Modifier.size(150.dp), // Occupy the max size in the Compose UI treefactory = { context ->// Creates viewTextView(context).apply {text = "Android TextView $nowindex $nowIndexM"MLog.d("Android 更新:$nowindex $nowIndexM")}},update = { view ->view.text = "Android TextView $nowindex $nowIndexM"MLog.d("Android 更新:$nowindex $nowIndexM")})}}
}
运行可以看到,Compose的重组刷新仅受mutableState变量影响。表现上来看符合Compose和原生的更新逻辑互相独立的结论。
3.生命周期问题
在上面的代码中可以看到setViewCompositionStrategy(ViewCompositionStrategy.Default) 这一行,
在Android自定义中嵌套ComposeView的场景,为了确保正确同步,需要确保正确处理ComposeView的生命周期,例如使用ViewCompositionStrategy来控制何时释放组合(即什么时候Compose不再跟踪状态)。
ViewCompositionStrategy组合策略 | 功能 |
DisposeOnDetachedFromWindow | 这个策略在ComposeView从窗口中分离时释放组合(Activity销毁时)。在Compose 1.2.0-beta02及更高版本中,这个策略不再作为默认值。 |
DisposeOnDetachedFromWindowOrReleasedFromPool | 这是当前的默认策略,它在ComposeView从窗口分离时释放组合,除非它是一个池容器(如RecyclerView)的一部分。在这种情况下,组合会在池容器与窗口分离或池已满时释放。 |
DisposeOnLifecycleDestroyed | 这个策略在与ComposeView关联的LifecycleOwner(如Fragment)的生命周期被销毁时释放组合。适用于需要与特定生命周期绑定的情况。 |
DisposeOnViewTreeLifecycleDestroyed | 这个策略在ComposeView所在ViewTreeLifecycleOwner被销毁时释放组合。当LifecycleOwner不确定或需要与更广泛的视图树关联时使用。 |
Compose如何实现禁止多次测量
为了解决多次测量的性能问题,Compose 禁止了多次测量子元素,否则抛出异常 IllegalStateException,使得我们可以进行深层次嵌套而不用担心影响性能。
@Composable
fun MeasureTest() {CustomColumn(content = {Text(text = "哈哈")Text(text = "呵呵")})
}
@Composable
fun CustomColumn(modifier: Modifier = Modifier,content: @Composable () -> Unit
) {Layout(modifier = modifier,content = content) { measurables, constraints ->val placeables = measurables.map { measurable ->measurable.measure(constraints)//如果测量两次,会有异常measurable.measure(constraints)}var yPosition = 0layout(constraints.maxWidth, constraints.maxHeight) {placeables.forEach { placeable ->placeable.placeRelative(x = 0, y = yPosition)yPosition += placeable.height}}}
}
运行这段自定义测量逻辑的代码,可以看到抛出了IllegalStateException异常
检测步骤在各个节点测量过程中实现:
internal class LayoutNodeLayoutDelegate(private val layoutNode: LayoutNode,
) {//1.节点执行测量过程override fun measure(constraints: Constraints): Placeable {if (layoutNode.intrinsicsUsageByParent == LayoutNode.UsageByParent.NotUsed) {layoutNode.clearSubtreeIntrinsicsUsage()}// 防止重复测量if (layoutNode.isOutMostLookaheadRoot()) {lookaheadPassDelegate!!.run {measuredByParent = LayoutNode.UsageByParent.NotUsedmeasure(constraints)}}trackMeasurementByParent(layoutNode)remeasure(constraints)return this}private fun trackMeasurementByParent(node: LayoutNode) {val parent = node.parentif (parent != null) {// 2.检查当前节点是否已经测量过,如果已经测量过抛出IllegalStateException异常。check(measuredByParent == LayoutNode.UsageByParent.NotUsed ||@Suppress("DEPRECATION") node.canMultiMeasure) { MeasuredTwiceErrorMessage }measuredByParent = when (parent.layoutState) {LayoutState.Measuring ->LayoutNode.UsageByParent.InMeasureBlockLayoutState.LayingOut ->LayoutNode.UsageByParent.InLayoutBlockelse -> throw IllegalStateException("Measurable could be only measured from the parent's measure or layout" +" block. Parents state is ${parent.layoutState}")}} else {measuredByParent = LayoutNode.UsageByParent.NotUsed}}
}
布局模型对比
Android 原生布局模型
首先回顾一下Android的布局流程:
主要分为三个阶段:
-
触发:当某个View的大小,位置发生变化(被动调用requestLayout),或者由于数据改变主动调用requestLayout时。
-
标记:requestLayout会标记自身需要布局,并调用父View的requestLayout,因此从根View开始,所有受影响的View都会被标记为需要重新布局。
-
刷新:在下一次界面更新时,通过递归遍历整个视图树,找到所有被标记为需要重新绘制的 View,分别进行测量、布局和绘制。
@CallSuper
public void requestLayout() {if (isRelayoutTracingEnabled()) {Trace.instantForTrack(TRACE_TAG_APP, "requestLayoutTracing",mTracingStrings.classSimpleName);printStackStrace(mTracingStrings.requestLayoutStacktracePrefix);}if (mMeasureCache != null) mMeasureCache.clear();if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {// Only trigger request-during-layout logic if this is the view requesting it,// not the views in its parent hierarchyViewRootImpl viewRoot = getViewRootImpl();if (viewRoot != null && viewRoot.isInLayout()) {if (!viewRoot.requestLayoutDuringLayout(this)) {return;}}mAttachInfo.mViewRequestingLayout = this;}mPrivateFlags |= PFLAG_FORCE_LAYOUT;mPrivateFlags |= PFLAG_INVALIDATED;if (mParent != null && !mParent.isLayoutRequested()) {//调用父View的requestLayout,递归遍历整个视图树mParent.requestLayout();}if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {mAttachInfo.mViewRequestingLayout = null;}
}
总结一下,原生Android UI是基于事件驱动和面向过程的,View的测量过程遵循严格的自顶向下和自底向上的顺序,需要根据父View的约束和自身的状态来不断调整自身。
面临的问题:
-
原生Android View的生命周期方法,比如onMeasure()和onLayout(),容易受到外部因素的影响,比如触摸事件、动画等。这些因素可能导致View需要多次重新测量和布局,总的来说是因为事件流没有收束。 Android系统对此也采取了一些优化手段,比如上文提到的仅更新被标记过的视图来减少layout次数。
-
但是还是有一些场景存在问题,比如前一篇文章提到过的如果某一层容器的测量(Linelayout)需要多次,整体测量次数就会指数级递增。
Compose 布局模型(Doing)
Compose 有 3 个主要阶段:
-
组合:要显示什么样的界面。Compose 运行可组合函数并创建界面说明。
-
布局:要放置界面的位置。该阶段包含两个步骤:测量和放置。对于布局树中的每个节点,布局元素都会根据 2D 坐标来测量并放置自己及其所有子元素。
-
绘制:渲染的方式。界面元素会绘制到画布(通常是设备屏幕)中。
后两个过程与传统视图的渲染过程相近,唯独组合是 Compose 所特有的。
与原生布局模型的最大区别在于,这些阶段通常会以相同的顺序执行,让数据能够沿一个方向(从组合到布局,再到绘制)生成帧,将所有可能影响的因素收束处理(单向数据流)。
Composition概念
Composition可以理解为UI的构造过程,它将UI元素组合在一起形成一个完整的视图层次。在Compose中,你创建一系列的Composable函数,每个函数定义了一个UI组件,比如按钮、文本、图片等。然后在这些Composable函数之间进行组合,形成复杂的UI结构。
当你调用一个Composable函数时,实际上就是在向Composition中添加一个UI元素。Compose会自动跟踪这些元素的状态,并在状态改变时重新绘制相应的部分,实现了高效的UI更新。
Composition在Compose框架中具体实现为Composition接口,我们首先来看一下Composition接口的实现类:
internal class CompositionImpl(private val parent: CompositionContext,/*** 负责维护LayoutNode布局树*/private val applier: Applier<*>,recomposeContext: CoroutineContext? = null
) : ControlledComposition, ReusableComposition, RecomposeScopeOwner, CompositionServices {/*** 状态树*/@Suppress("MemberVisibilityCanBePrivate") // published as internalinternal val slotTable = SlotTable()
}
可以看到Composition 中存在两棵树:
一棵是 LayoutNode 树,这是真正执行渲染的树,LayoutNode 可以像 View 一样完成 measure/layout/draw 等具体渲染过程,通过applier维护;
而另一棵树是 SlotTable状态树,它记录了 Composition 中的各种数据状态。
为什么Composition需要两棵树?
在Jetpack Compose中,使用了被称为“节点树”(Node Tree)的数据结构来描述UI的结构。
这个数据结构通常被分为两部分:状态树(State Tree)和布局树(Layout Tree)。这种设计是为了优化性能和实现响应式UI。
-
状态树(State Tree):它代表了UI组件的状态和依赖关系。当状态改变时,受影响的UI元素会自动更新。状态树跟踪这些变化,使得只有真正发生变化的部分需要重新构建和绘制,而不是整个UI。
-
布局树(Layout Tree):在状态树中的状态更新后,会触发布局计算,生成布局树。布局树描述了UI元素的几何形状、大小和位置信息,用于确定屏幕上的元素如何排列。这个过程是独立于状态更新的,因为它只关心元素的物理属性,而不涉及它们的内容或行为。
之所以需要这两棵树,是因为它们各自负责不同的职责:
- 状态树关注逻辑和数据驱动的变化,确保UI能够根据数据的实时变化做出反应。
- 布局树则专注于计算和优化视图的物理布局,以适应屏幕尺寸和设备特性。
这种分离的设计使得Jetpack Compose能够高效地处理复杂的UI更新,同时保持良好的性能。通过只重新构建和绘制必要的部分,它可以避免不必要的重绘操作,提高用户体验。
Jetpack Compose使用状态树和布局树来分别处理UI的状态变化和布局计算,这种分离的设计提高了性能,实现了响应式UI,并确保了只有实际变化的部分会被更新。
如何触发Composition?
以下场景均能触发Composition:
状态树(SlotTable)如何生成
框架是如何识别@Composable函数,继而将其组合为状态树的呢?
Compose也通过编译器插桩实现了很多样本代码,由于Compose 是一个 Kotlin Only 框架,所以 Compose Compiler 的本质是一个 KCP(Kotlin Compiler Plugin)
通过KCP可以将@Composable函数转化为Group
(对这部分实现有兴趣的同学可以深入看下源码:https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt)
我们首先实现一个可以点击+1的组件。
@Composable fun ClickText() {var text by remember { mutableStateOf(1) }Button(onClick = { text += 1 }) {Text("$text")}
}
对上面的代码进行反编译后:
// 上面的 ClickText 函数签名经过 compose.compiler 编译后会变成这样
@Composable
public static final void ClickText1(@Nullable Composer $composer, int $changed) {// Composer 类似于上下文,通过KCP插桩实现,用来记录节点关系。Composer $composer2 = $composer.startRestartGroup(-1679608079); //编译期生成的固定keyComposerKt.sourceInformation($composer2, "C(ClickText1)21@424L6:ClickText.kt#a1gac0");if ($changed != 0 || !$composer2.getSkipping()) {ClickText2($composer2, 0);} else {$composer2.skipToGroupEnd();}ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();if (endRestartGroup != null) {endRestartGroup.updateScope(new ClickText1.1($changed));}
}
可以看到代码中穿插着了一些 startXXX/endXXX ,这样的成对调用就好似对一棵树进行深度遍历时的压栈/出栈。
再来看看Composer#startRestartGroup和endRestartGroup方法:
internal class ComposerImpl(//用于存储组合数据的槽表private val slotTable: SlotTable,internal var insertTable = SlotTable()private var writer: SlotWriter = insertTable.openWriter().also { it.close() }internal var reader: SlotReader = slotTable.openReader().also { it.close() }@ComposeCompilerApioverride fun startRestartGroup(key: Int): Composer {//开始重组start(key, null, GroupKind.Group, null)addRecomposeScope()return this}private fun addRecomposeScope() {if (inserting) {//如果正在插入,则先压栈记录val scope = RecomposeScopeImpl(composition as CompositionImpl)//压栈invalidateStack.push(scope)updateValue(scope)scope.start(compositionToken)} else {val invalidation = invalidations.removeLocation(reader.parent)//读取slotval slot = reader.next()val scope = if (slot == Composer.Empty) {//当先前未激活的区域变为活动区域时,执行val newScope = RecomposeScopeImpl(composition as CompositionImpl)updateValue(newScope)newScope} else slot as RecomposeScopeImplscope.requiresRecompose = invalidation != null || scope.forcedRecompose.also { forced ->if (forced) scope.forcedRecompose = false}//压栈invalidateStack.push(scope)scope.start(compositionToken)}}/*** 将槽位表中的当前值更新为[value]。 */@PublishedApi@OptIn(InternalComposeApi::class)internal fun updateValue(value: Any?) {if (inserting) {writer.update(value)} else {val groupSlotIndex = reader.groupSlotIndex - 1changeListWriter.updateValue(value, groupSlotIndex)}}}
可以看到,addRecomposeScope这里主要是创建 RecomposeScopeImpl 并存入 SlotTable 。
SlotTable 的数据存储在 Slot 中,一个或多个 Slot 又归属于一个 Group。可以将 Group 理解为树上的一个个节点。
Compose 中节点分两种:
- Group 代表一个组合范围,属于重组的最小单位,用于构建树的结构,识别结构的变化
- LayoutNode 是最终组成渲染树的节点,可以完成测量布局绘制等渲染过程
internal class SlotTable : CompositionData, Iterable<CompositionGroup> {/*** 用于存储组信息的数组,存储为[Group_Fields_Size]组。数组的元素。[groups]数组可以看作是内联的数组结构体。*/var groups = IntArray(0)private set/*** An array that stores the slots for a group. The slot elements for a group start at the* offset returned by [dataAnchor] of [groups] and continue to the next group's slots or to* [slotsSize] for the last group. When in a writer the [dataAnchor] is an anchor instead of* an index as [slots] might contain a gap.*/var slots = Array<Any?>(0) { null }private set}
SlotTable 有两个数组成员,groups 数组存储 Group 信息,slots 存储 Group 所辖的数据(比如上文中的RecomposeScopeImpl)。用数组替代结构化存储的好处是可以提升对“树”的访问速度(不用每次查询都深度遍历)。
Composable 在编译期会生成多种不同类型的 startXXXGroup,它们在 SlotTable 中插入 Group 的同时,会存入辅助信息以实现不同的功能:
startXXXGroup | 说明 | 使用场景 |
startNode /startResueableNode | 插入一个包含 Node 的 Group。 | 。。。 |
startRestartGroup | 插入一个可重复执行的 Group,它可能会随着重组被再次执行,因此 RestartGroup 是重组的最小单元。 | |
startReplacableGroup | 插入一个可以被替换的 Group,例如一个 if/else 代码块就是一个 ReplaceableGroup,它可以在重组中被插入后者从 SlotTable 中移除。 | |
startMovableGroup | 插入一个可以移动的 Group,在重组中可能在兄弟 Group 之间发生位置移动。 | |
startReusableGroup | 插入一个可复用的 Group,其内部数据可在 LayoutNode 之间复用,例如 LazyList 中同类型的 Item。 | 。。。 |
为什么在结点信息Slot的基础上,需要一个Group的概念呢?
编译期插入 startXXXGroup 代码时会基于代码位置生成可识别的 $key(parent 范围内唯一)。
在首次组合时 $key 会随着 Group 存入 SlotTable,在重组中,Composer 基于 $key 的比较可以识别出 Group 的增、删或者位置移动。换言之,SlotTable 中记录的 Group 携带了位置信息,
这种机制也被称为 Positional Memoization。Positional Memoization 可以发现 SlotTable 结构上的变化,最终转化为 LayoutNode 树的更新。
总结一下整个过程就是:
Composable 源码在编译期会被插入 startXXXGroup/endXXXGroup 模板代码,存储节点信息Slot与位置信息Group到SlotTable中,用于对 SlotTable 的树形遍历。
UI树如何刷新
经过上面的过程,SlotTable中已经存储了UI树的节点信息Slot和位置信息Group。但是SlotTable 结构的变化是如何反映到 LayoutNode 树上的呢?
通过分析源码:
ComposeView#setContent -> ComposeView#createComposition -> ComposeView#ensureCompositionCreated -> AbstractComposeView#setContent -> doSetContent过程:
可以看到,创建组合的时候,Compositoin 内部通过 Applier 维护着 LayoutNode 树并执行具体渲染。
像 View 一样,LayoutNode 通过 measure/layout/draw 等一系列方法完成具体渲染。此外它还提供了 insertAt/removeAt 等方法实现子树结构的变化。这些方法会在 UiApplier 中调用:
internal class UiApplier(root: LayoutNode
) : AbstractApplier<LayoutNode>(root) {override fun insertTopDown(index: Int, instance: LayoutNode) {// Ignored}override fun insertBottomUp(index: Int, instance: LayoutNode) {current.insertAt(index, instance)}override fun remove(index: Int, count: Int) {current.removeAt(index, count)}override fun move(from: Int, to: Int, count: Int) {current.move(from, to, count)}override fun onClear() {root.removeAll()}}
UiApplier 用来更新和修改 LayoutNode 树:
-
down()/up() 用来移动 current 的位置,完成树上的导航。
-
insertXXX/remove/move 用来修改树的结构。其中 insertTopDown 和 insertBottomUp 都用来插入新节点,只是插入的方式有所不同,一个是自下而上一个是自顶而下,针对不同的树形结构选择不同的插入顺序有助于提高性能。例如 Android 端的 UiApplier 主要依靠 insertBottomUp 插入新节点,因为 Android 的渲染逻辑下,子节点的变动会影响父节点的重新 measure,自此向下的插入可以避免影响太多的父节点,提高性能,因为 attach 是最后才进行。
受限于篇幅,本文仅仅只分析了@Composable函数 -> SlotTable -> LayoutNode的过程,具体的布局测量(固有特性测量)以及渲染过程部分源码分析会在后续文章持续补充,感兴趣的同学可以在LayoutNode#insertAt方法这里继续分析下去。
总结
本文主要是对Compose的布局过程,结合源码探索了各个模块具体如何实现,以及继续深挖了上一篇文章中提到的一些问题。阅读之后可以对Compose的布局流程和框架实现思想有一个大体的了解,但是具体的实现细节由于对应部分源码太多,在一篇文章中完全讲解清楚工作量巨大,所以仅仅只是贴出了对应的实现和源码位置,感兴趣的同学们可以基于这些瞄点继续进行深入学习。
下一篇文章预计会补充固有特性测量使用&实现相关的内容,希望感兴趣的同学可以一起加入进来,共同进步。
相关文章:

(二)Jetpack Compose 布局模型
前文回顾 (一)Jetpack Compose 从入门到会写-CSDN博客 首先让我们回顾一下上一篇文章中里提到过几个问题: ComposeView的层级关系,互相嵌套存在的问题? 为什么Compose可以实现只测量一次? ComposeView和…...
【Oracle impdp导入dmp文件(windows)】
Oracle impdp导入dmp文件(windows) 1、连接数据库2、创建与导出的模式相同名称的用户WIRELESS2,并赋予权限3、创建directory 的物理目录f:\radio\dmp,并把.dmp文件放进去4、连接新用户WIRELESS25、创建表空间的物理目录F:\radio\t…...

代数结构:5、格与布尔代数
16.1 偏序与格 偏序集:设P是集合,P上的二元关系“≤”满足以下三个条件,则称“≤”是P上的偏序关系(或部分序关系) (1)自反性:a≤a,∀a∈P; (2…...

如何使用DEEPL免费翻译PDF
如何使用DEEPL免费翻译PDF 安装DEEPL取消PDF限制 安装DEEPL 安装教程比较多,这里不重复。 把英文pdf拖进去,点翻译,在下面的框中有已经翻译完毕的文档。 但是存在两个问题 问题1:这些文档是加密的。 问题2:带有DeepL标…...
Spring-全面详解
Spring,就像是软件开发界的一个超级英雄,它让编写Java程序变得更简单、更灵活。想象一下,如果你要盖一栋大楼,Spring就是那个提供各种工具、框架和最佳实践的建筑大师,帮助你高效、优雅地搭建起整个项目。 Spring是啥&…...

QT自适应界面 处理高DPI 缩放比界面乱问题
1.pro文件添加 必须添加要不找不到 QT版本需要 5。4 以上才支持 QT widgets 2.main界面提前处理 // 1. 全局缩放使能QApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true);// 2. 适配非整数倍缩放QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::High…...

序列到序列模型在语言识别Speech Applications中的应用 Transformer应用于TTS Transformer应用于ASR 端到端RNN
序列到序列模型在语言识别Speech Applications中的应用 A Comparative Study on Transformer vs RNN in Speech Applications 序列到序列(Seq2Seq)模型在语音识别(Speech Applications)中有重要的应用。虽然Seq2Seq模型最初是为了解决自然语言处理中的序列生成问题而设计的…...

【Linux】- Linux环境变量[8]
目录 环境变量 $符号 自行设置环境变量 环境变量 环境变量是操作系统(Windows、Linux、Mac)在运行的时候,记录的一些关键性信息,用以辅助系统运行。在Linux系统中执行:env命令即可查看当前系统中记录的环境变量。 …...

前端笔记-day04
文章目录 01-后代选择器02-子代选择器03-并集选择器04-交集选择器05-伪类选择器06-拓展-超链接伪类07-CSS特性-继承性08-CSS特性-层叠性09-CSS特性-优先级11-Emmet写法12-背景图13-背景图平铺方式14-背景图位置15-背景图缩放16-背景图固定17-background属性18-显示模式19-显示模…...

计算机字符集产生的历史与乱码
你好,我是 shengjk1,多年大厂经验,努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注!你会有如下收益: 了解大厂经验拥有和大厂相匹配的技术等 希望看什么,评论或者私信告诉我! 文章目录 一…...

Rerank进一步提升RAG效果
RAG & Rerank 目前大模型应用中,RAG(Retrieval Augmented Generation,检索增强生成)是一种在对话(QA)场景下最主要的应用形式,它主要解决大模型的知识存储和更新问题。 简述RAG without R…...

使用train.py----yolov7
准备工作 在训练之前,数据集的工作和配置环境的工作要做好 数据集:看这里划分数据集,训练自己的数据集。_划分数据集后如何训练-CSDN博客 划分数据集2,详细说明-CSDN博客 配置环境看这里 从0开始配置环境-yolov7_gpu0是inter g…...

机器学习第37周周报 GGNN
文章目录 week37 GGNN摘要Abstract一、文献阅读1. 题目2. abstract3. 网络架构3.1 数据处理部分3.2 门控图神经网络3.3 掩码操作 4. 文献解读4.1 Introduction4.2 创新点4.3 实验过程4.3.1 传感器设置策略4.3.2 数据集4.3.3 实验设置4.3.4 模型参数设置4.3.5 实验结果 5. 结论 …...

Baidu Comate:释放编码潜能,革新软件开发
Baidu Comate Baidu Comate,智能代码助手,凭借着文心大模型的强大支撑,结合了百度多年的编程实战数据和丰富的开源资源,形成了一款崭新的编码辅助利器。它不仅具备着高智能、多场景、价值创造的特质,更可广泛应用于各…...

MATLAB的Bar3函数调节渐变色(内附渐变色库.mat及.m文件免费下载链接)
一. colormap函数 可以使用colormap函数: t1[281.1,584.6, 884.3,1182.9,1485.2; 291.6,592.6,896,1197.75,1497.33; 293.8,596.4,898.6,1204.4,1506.4; 295.8,598,904.4,1209.0,1514.6];bar3(t1,1) set(gca,XTickLabel,{300,600,900,1200,1500},FontSize,10) set…...
使用 TensorFlow.js 和 OffscreenCanvas 实现实时防挡脸弹幕
首先,要理解我们的目标,我们将实时获取视频中的面部区域并将其周围的内容转为不透明以制造出弹幕的“遮挡效应”。 步骤一:环境准备 我们将使用 TensorFlow.js 的 Body-segmentation 库来完成面部识别部分,并使用 OffscreenCanv…...

【计算机网络篇】数据链路层(10)在物理层扩展以太网
文章目录 🍔扩展站点与集线器之间的距离🛸扩展共享式以太网的覆盖范围和站点数量 🍔扩展站点与集线器之间的距离 🛸扩展共享式以太网的覆盖范围和站点数量 以太网集线器一般具有8~32个接口,如果要连接的站点数量超过了…...

conan2 基础入门(03)-使用(msvc为例)
conan2 基础入门(03)-使用(msvc为例) 文章目录 conan2 基础入门(03)-使用(msvc为例)⭐准备生成profile文件预备文件和Code ⭐使用指令预览正确执行结果可能出现的问题 ⭐具体讲解conanconanfile.txt执行 install cmakeCMakeLists.txt生成项目构建 END ⭐准备 在阅读和学习本文…...
uniapp this 作用域保持的方法
在 UniApp(或任何基于 Vue.js 的框架)中,this 关键字通常用于引用当前 Vue 实例的上下文。然而,当你在回调函数、定时器、Promise、异步函数等中使用 this 时,你可能会发现 this 的值不再指向你期望的 Vue 实例&#x…...
vue2 与vue3的差异汇总
Vue 2 与 Vue 3 之间存在多方面的差异,这些差异主要体现在性能、API设计、数据绑定、组件结构、以及生命周期等方面。以下是一些关键差异的汇总: 数据绑定与响应式系统 Vue 2 使用 Object.defineProperty 来实现数据的响应式,这意味着只有预…...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...
变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析
一、变量声明设计:let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性,这种设计体现了语言的核心哲学。以下是深度解析: 1.1 设计理念剖析 安全优先原则:默认不可变强制开发者明确声明意图 let x 5; …...
内存分配函数malloc kmalloc vmalloc
内存分配函数malloc kmalloc vmalloc malloc实现步骤: 1)请求大小调整:首先,malloc 需要调整用户请求的大小,以适应内部数据结构(例如,可能需要存储额外的元数据)。通常,这包括对齐调整,确保分配的内存地址满足特定硬件要求(如对齐到8字节或16字节边界)。 2)空闲…...
Nginx server_name 配置说明
Nginx 是一个高性能的反向代理和负载均衡服务器,其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机(Virtual Host)。 1. 简介 Nginx 使用 server_name 指令来确定…...
【HTML-16】深入理解HTML中的块元素与行内元素
HTML元素根据其显示特性可以分为两大类:块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...
大学生职业发展与就业创业指导教学评价
这里是引用 作为软工2203/2204班的学生,我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要,而您认真负责的教学态度,让课程的每一部分都充满了实用价值。 尤其让我…...
Python Einops库:深度学习中的张量操作革命
Einops(爱因斯坦操作库)就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库,用类似自然语言的表达式替代了晦涩的API调用,彻底改变了深度学习工程…...

【p2p、分布式,区块链笔记 MESH】Bluetooth蓝牙通信 BLE Mesh协议的拓扑结构 定向转发机制
目录 节点的功能承载层(GATT/Adv)局限性: 拓扑关系定向转发机制定向转发意义 CG 节点的功能 节点的功能由节点支持的特性和功能决定。所有节点都能够发送和接收网格消息。节点还可以选择支持一个或多个附加功能,如 Configuration …...

stm32wle5 lpuart DMA数据不接收
配置波特率9600时,需要使用外部低速晶振...

数据结构第5章:树和二叉树完全指南(自整理详细图文笔记)
名人说:莫道桑榆晚,为霞尚满天。——刘禹锡(刘梦得,诗豪) 原创笔记:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 上一篇:《数据结构第4章 数组和广义表》…...