(二)Jetpack Compose 布局模型

news2024/9/21 3:24:43

前文回顾

(一)Jetpack Compose 从入门到会写-CSDN博客

首先让我们回顾一下上一篇文章中里提到过几个问题:

  1. ComposeView的层级关系,互相嵌套存在的问题?

  2. 为什么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 = 0
  var nowIndexM by mutableStateOf(0)

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_origin_test)

    // 创建并添加ComposeView
    val composeView = ComposeView(this).apply {
      /**
       * 组合策略,解决重组性能问题
       * ViewCompositionStrategy.Default 会在底层 ComposeView 从窗口分离时释放组合,
       * 除非它是池容器(例如RecyclerView)的一部分。
       */
      setViewCompositionStrategy(ViewCompositionStrategy.Default)
      setContent {
        MyComposable()
      }
    }
    val container = findViewById<FrameLayout>(R.id.container)
    // 添加到LinearLayout
    container.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()
    }
  }

  @Composable
  fun 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 tree
        factory = { context ->
          // Creates view
          TextView(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 = 0
        layout(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.NotUsed
                    measure(constraints)
                }
            }
            trackMeasurementByParent(layoutNode)
            remeasure(constraints)
            return this
        }

    private fun trackMeasurementByParent(node: LayoutNode) {
            val parent = node.parent
            if (parent != null) {
                // 2.检查当前节点是否已经测量过,如果已经测量过抛出IllegalStateException异常。
                check(
                    measuredByParent == LayoutNode.UsageByParent.NotUsed ||
                        @Suppress("DEPRECATION") node.canMultiMeasure
                ) { MeasuredTwiceErrorMessage }
                measuredByParent = when (parent.layoutState) {
                    LayoutState.Measuring ->
                        LayoutNode.UsageByParent.InMeasureBlock

                    LayoutState.LayingOut ->
                        LayoutNode.UsageByParent.InLayoutBlock

                    else -> 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 hierarchy
        ViewRootImpl 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的约束和自身的状态来不断调整自身。

面临的问题:

  1. 原生Android View的生命周期方法,比如onMeasure()和onLayout(),容易受到外部因素的影响,比如触摸事件、动画等。这些因素可能导致View需要多次重新测量和布局,总的来说是因为事件流没有收束。 Android系统对此也采取了一些优化手段,比如上文提到的仅更新被标记过的视图来减少layout次数。

  2. 但是还是有一些场景存在问题,比如前一篇文章提到过的如果某一层容器的测量(Linelayout)需要多次,整体测量次数就会指数级递增。

Compose 布局模型(Doing)

Compose 有 3 个主要阶段:

  1. 组合:要显示什么样的界面。Compose 运行可组合函数并创建界面说明。

  2. 布局:要放置界面的位置。该阶段包含两个步骤:测量和放置。对于布局树中的每个节点,布局元素都会根据 2D 坐标来测量并放置自己及其所有子元素。

  3. 绘制:渲染的方式。界面元素会绘制到画布(通常是设备屏幕)中。

后两个过程与传统视图的渲染过程相近,唯独组合是 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 internal
    internal 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); //编译期生成的固定key
    ComposerKt.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() }

    @ComposeCompilerApi
    override 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)
            //读取slot
            val slot = reader.next()
            val scope = if (slot == Composer.Empty) {
                //当先前未激活的区域变为活动区域时,执行
                val newScope = RecomposeScopeImpl(composition as CompositionImpl)
                updateValue(newScope)
                newScope
            } else slot as RecomposeScopeImpl
            scope.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 - 1
            changeListWriter.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的布局流程和框架实现思想有一个大体的了解,但是具体的实现细节由于对应部分源码太多,在一篇文章中完全讲解清楚工作量巨大,所以仅仅只是贴出了对应的实现和源码位置,感兴趣的同学们可以基于这些瞄点继续进行深入学习。

下一篇文章预计会补充固有特性测量使用&实现相关的内容,希望感兴趣的同学可以一起加入进来,共同进步。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1666608.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

加密与CA证书

文章目录 加密与CA证书http协议是不安全的使用对称秘钥进行数据加密非对称秘钥加密CA证书应用补充 加密与CA证书 CA 证书是什么&#xff0c;证书的目的是什么 首先明确一点&#xff0c;CA证书是数字时代中确保身份和数据安全的重要工具&#xff0c;为用户提供了安心、便捷和可…

vlock工具:锁定Linux终端的安全智能方法

虚拟控制台是 Linux 非常重要的功能&#xff0c;它们为系统用户提供 shell 提示&#xff0c;以非图形设置方式使用系统&#xff0c;该设置只能在物理机上使用&#xff0c;而不能远程使用。 用户只需从一个虚拟控制台切换到另一个虚拟控制台即可同时使用多个虚拟控制台会话。 …

[机器学习-05] Scikit-Learn机器学习工具包进阶指南:协方差估计和交叉分解功能实战【2024最新】

&#x1f3a9; 欢迎来到技术探索的奇幻世界&#x1f468;‍&#x1f4bb; &#x1f4dc; 个人主页&#xff1a;一伦明悦-CSDN博客 ✍&#x1f3fb; 作者简介&#xff1a; C软件开发、Python机器学习爱好者 &#x1f5e3;️ 互动与支持&#xff1a;&#x1f4ac;评论 &…

QT自适应界面 处理高DPI 缩放比界面乱问题

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

EPAI手绘建模APP工程图顶部工具栏

7、工程图 图 302 工程图 工程图包括顶部常用工具栏、右侧工程图工具栏、左侧模型列表栏、中间的工程图。 (1) 常用工具栏 ① 删除&#xff0c;选中场景中工程图元素后&#xff0c;删除。可以选择多个工程图元素同时删除。 ② 设置&#xff0c;打开工程图设置页面&#xff0…

科林Linux_4 信号

#include <signal.h> 信号signal&#xff1a;Linux或Unix系统支持的经典的消息机制&#xff0c;用于处置进程&#xff0c;挂起进程或杀死进程 kill -l #查看系统支持的信号 1~31 Unix经典信号&#xff08;软件开发工程师&#xff09; 34~64 自定义信号/实时信号&am…

pycharm 将项目连同库一起打包及虚拟环境的使用

目录 一、创建虚拟环境 1、用 anaconda 创建 2、Pycharm 直接创建 二、虚拟环境安装第三方库 1、创建项目后&#xff0c;启动终端(Alt F12)&#xff0c;或者点击下方标记处。 2、使用 pip 或者 conda 来进行三方库的安装或卸载 3、将项目中的库放入文档&#xff0c;便于…

Python深度学习基于Tensorflow(1)Numpy基础

文章目录 数据转换和数据生成存取数据数据变形和合并算数计算广播机制使用Numpy实现回归实例 numpy的重要性不言而喻&#xff0c;一般不涉及到GPU/TPU计算&#xff0c;都是用numpy&#xff0c;常见的np就是这个玩意。其特点就是快&#xff01;其实如果不涉及到深度学习&#xf…

【挑战30天首通《谷粒商城》】-【第一天】01、简介-项目介绍

文章目录 课程介绍一、 项目介绍1、项目背景A、电商模式1、B2B 模式2、B2C 模式3、C2B 模式4、C2C 模式5、O2O 模式 1.2、项目架构图1.3、项目技术 & 特色1.4、项目前置要求二、分布式基础概念(略)三、环境撘建(略) one more thing 课程介绍 1.分布式基础(全栈开发篇)2.分…

音视频开发6 音视频录制原理和播放原理

音视频录制原理 音视频播放原理

6818Linux内核--Bootloader应用分析

Bootloader应用分析 一个嵌入式 Linux 系统从软件的角度看通常可以分为四个层次&#xff1a; 引导加载程序。包括固化在固件( firmware )中的 boot 代码(可选)&#xff0c;和 Boot Loader 两大部分。 Linux 内核。特定于嵌入式板子的定制内核以及内核的启动参数。 文件系统…

手把手教你用 spacy3 训练中文NER

文章目录 模型文件下载训练模型准备数据转化成 doc_bin 格式模型训练配置生成初始配置补全完整配置 开始训练 测试模型参考文献 模型文件下载 https://github.com/explosion/spacy-models/releases?qzh&expandedtrue 简单测试一下ner效果&#xff0c;发现根本不能用 训…

shell进阶之计算编译前后时间(十八)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

Ubuntu 下使用 Scons 交叉编译嘉楠堪智 CanMV K230 大小核 Coremark 程序

在 Ubuntu 下使用 SCons 进行交叉编译嘉楠堪智 CanMV K230 大小核&#xff08;不同的玄铁 C908 核心&#xff09;的 C 程序&#xff0c;以 Coremark 程序为例&#xff0c;顺便测试一下大小核和编译器的性能。 2024年3月14日&#xff0c;嘉楠科技宣布推出了全球首款支持 RISC-V…

掌握未来搜索的钥匙:深入解析 Milvus 向量搜索引擎的终极指南!

在大数据时代&#xff0c;向量搜索技术愈发重要。作为一个开源的向量相似性搜索引擎&#xff0c;Milvus 提供了基于向量的相似性搜索功能&#xff0c;广泛应用于机器学习、人工智能等领域。本文将深入介绍 Milvus 的基本概念&#xff0c;包括其介绍、主要作用、使用方法及注意事…

【iOS开发】—— 初识锁

【iOS开发】—— 初识锁 线程安全锁的种类自旋锁定义原理自旋锁缺点OSSpinLock&#xff08;自旋锁&#xff09; 互斥锁os_unfair_lockpthread_mutexNSLockNSRecusiveLockSemaphore信号量synchronized 总结两种之间的区别和联系&#xff1a; 线程安全 当一个线程访问数据的时候…

深入理解WPF的ResourceDictionary

深入理解WPF的ResourceDictionary 介绍 在WPF中&#xff0c;ResourceDictionary用于集中管理和共享资源&#xff08;如样式、模板、颜色等&#xff09;&#xff0c;从而实现资源的重用和统一管理。本文详细介绍了ResourceDictionary的定义、使用和合并方法。 定义和用法 Res…

Android Hanlder 揭密之路- 深入理解异步消息传递机制Looper、Handler、Message三者关系

在Android开发中&#xff0c;Handler作为实现线程间通信的桥梁&#xff0c;扮演着至关重要的角色。无论是在主线程执行UI操作&#xff0c;还是在子线程进行耗时任务&#xff0c;Handler都可以高效地将异步消息分派到对应的线程中执行。 本文将全方位解析Handler的工作原理及实现…

2024数维杯数学建模C题思路代码

2024年数维杯&电工杯思路代码在线文档​https://www.kdocs.cn/l/cdlol5FlRAdE 这道题想要做出好的结果&#xff0c;必须要结合插值法和分布函数来做&#xff0c;主要还是因为勘探点太少&#xff0c;直接用插值法效果不太好&#xff0c;以下是我做的&#xff0c;函数分布可…

Python的while循环

目录 while循环的结构 示例 关键字 break continue while循环的结构 while condition&#xff08;循环条件&#xff09;: # 循环的内容 循环内容的执行与结束需要通过循环条件控制。 在执行循环之前需要设立一个循环条件的初始值&#xff0c;以便while循环体判断循环条件。…