【Android14 ShellTransitions】(六)SyncGroup完成

在这里插入图片描述

这一节的内容在WMCore中,回想我们的场景,是在Launcher启动某一个App,那么参与动画的就是该App对应Task(OPEN),以及Launcher App对应的Task(TO_BACK)。在确定了动画的参与者后,接下来我们就需要等待动画参与者绘制完成。一个基本的逻辑是,参与动画的主体要绘制出来了才能开始动画,不然动画都开始执行了,窗口还没有绘制出来,那对于用户来说屏幕上是没有任何变化的,这不就有点尴尬了。

ShellTransitions之前,检查动画是否可以开始的逻辑是在AppTransitionController.handleAppTransitionReady中,通过调用transitionGoodToGo来检查窗口是否绘制完成的,现在则是在BLASTSyncEngine.onSurfacePlacement中,通过调用BLASTSyncEngine.SyncGroup.tryFinish不断检查所有动画参与者是否已经全部同步完毕。一旦所有的动画参与者完成同步,则视为SyncGroup完成,或者说Transition就绪,就会调用BLASTSyncEngine.SyncGroup.finishNow,最终走到Transition.onTransactionReady,具体的调用堆栈为:

RootWindowContainer.performSurfacePlacementNoTrace

-> BLASTSyncEngine.onSurfacePlacement

-> BLASTSyncEngine.SyncGroup.tryFinish

-> BLASTSyncEngine.SyncGroup.finishNow

-> Transition.onTransactionReady

我们这一节先分析检查SyncGroup是否完成(BLASTSyncEngine.SyncGroup.tryFinish)以及SyncGroup完成(BLASTSyncEngine.SyncGroup.finishNow)的内容,Transition就绪的内容即Transition.onTransactionReady放到单独一章分析。

1 BLASTSyncEngine.onSurfacePlacement

BLASTSyncEngine.onSurfacePlacement由RootWindowContainer.performSurfacePlacementNoTrace调用,代码为:

在这里插入图片描述

BLASTSyncEngine的成员变量mActiveSyncs之前已经介绍过了,当我们创建Transition的时候,也会创建一个SyncGroup,来收集参与动画的WindowContainer,创建的SyncGroup则保存在了BLASTSyncEngine.mActiveSyncs。

这里则是从BLASTSyncEngine.mActiveSyncs中拿出SyncGroup,调用SyncGroup.tryFinish来检查SyncGroup是否完成。

2 SyncGroup.tryFinish

在这里插入图片描述

1)、如果SyncGroup.mReady为false,则直接返回。之前我们分析Transition的启动流程时,知道了只有当WMShell侧创建了一个ActiveTransition后切换回WMCore并且调用Transition.start,Transition才算正式启动,正是在Transition.start中,调用了SyncGroup.setReady方法将SyncGroup的mReady设置为了true。从这里我们就看到了SyncGroup.mReady的作用,如果Transition没有启动,那么这里是不会去检查其对应的SyncGroup是否完成了的,而是直接返回false,也就是说如果SyncGroup没有ready,那么Transition将无法走到下一个阶段。

2)、SyncGroup.mRootMembers则保存了参与动画的WindowContainer,我们这里则是为每一个WindowContainer调用WindowContainer.isSyncFinished来检查这个WindowContainer是否完成同步/绘制,只要有一个没有完成同步,那么就直接返回false,不需要往下走了,等待下一次RootWindowContainer.performSurfacePlacementNoTrace到来的时候再检查看看有没有完成同步。

3)、如果所有参与动画的WindowContainer都已经完成同步了,那么就继续调用SyncGroup.finishNow来将当前SyncGroup结束掉。

我们先分析WindowContainer.isSyncFinished,再去分析SyncGroup.finishNow。

2.1 WindowContainer.isSyncFinished

首先再回顾一下,当把WindowContainer添加SyncGroup的时候,会为每一个WindowContainer调用prepareSync方法,结果是:

1)、WindowState类型的WindowContainer,其mSyncState被置为SYNC_STATE_WAITING_FOR_DRAW。

2)、非WindowState类型的WindowContainer,其mSyncState被置SYNC_STATE_READY。

再看WindowContainer.isSyncFinished的内容:

在这里插入图片描述

1)、首先一个基本的规则是,如果一个WindowContainer请求的是不可见的,那么将其视为同步完成。这个其实也很好理解,如果这个WindowContainer在完成动画的时候是不希望被显示的,那么就不需要等待它绘制完成了。

2)、对于非WindowState类型的WindowContainer,比如Task或者ActivityRecord,由于它们的mSyncState一开始就被设置为了SYNC_STATE_READY,因此它们主要是检查它们的mChildren是否同步完成,最终检查的就是其中的WindowState是否完成同步/绘制。一旦有一个WindowState:

  • 同步完成。
  • 请求可见。
  • 和父容器大小相等。

只要找到一个符合以上条件的WindowState,那么就可以认为这个WindowContainer已经完成了同步。

3)、对于WindowState,则只有一个标准,即其mSyncState是否是SYNC_STATE_READY(暂不考虑子窗口的情况)。

2.2 WindowContainer.onSyncFinishedDrawing

再看下什么时候WindowState的mSyncState什么被设置为SYNC_STATE_READY。

WindowState的mSyncState被设置为SYNC_STATE_READY的地方只有一处,在WindowContainer.onSyncFinishedDrawing:

在这里插入图片描述

从注释也能看出,当WindowContainer完成绘制其内容的时候,这个方法会被调用。

具体调用的地方则是:

在这里插入图片描述

很明显,当窗口完成绘制时,会调用WindowState.finishDrawing,进而将WindowState的mSyncState设置为SYNC_STATE_READY。

而一旦Task/ActivityRecord中的WindowState绘制完成,那么该Task/ActivityRecord就会被视为同步完成。

这部分的内容之前分析WindowContainerTransaction的文章有过更加详细的介绍:

4【Android 12】【WCT的同步】BLASTSyncEngine - 掘金 (juejin.cn)

更多的详细内容可以看下当时的分析。

3 SyncGroup.finishNow

一旦SyncGroup中所有的动画参与者都同步完成,那么就调用SyncGroup.finishNow来结束掉这个SyncGroup。

这个方法是我们本篇文章的重点,且内容较多,我们分段去看。

3.1 合并Transaction

在这里插入图片描述

这里从对象工厂中拿到一个Transaction对象,局部变量merged,对于所有参与动画的WindowContainer,将它们在动画期间发生的同步操作都合并到这个局部变量merged中。

这一点主要是通过对所有参与动画的WindowContainer调用WindowContainer.finishSync方法来完成的,indowContainer.finishSync方法内容为:

在这里插入图片描述

1)、将WindowContainer.mSyncTransaction中收集到的对当前WindowContainer对应的SurfaceControl的修改(同步操作)合并到传参outMergedTransaction中,即我们上面提到的SyncGroup.finishNow中的局部变量merged。

2)、递归调用所有子WindowContainer的finishSync方法,最终的结果是将这个WindowContainer以及所有子WindowContainer的同步操作都收集到传参outMergedTransaction中。

3)、最后由于同步工作已经结束,那么将这个WindowContainer的mSyncState以及mSyncGroup之类的成员变量进行重置。

这里所说的同步操作主要是针对WindowContainer.mSyncTransaction来说的,其实之前也看到过“sync”这个字眼了,我们这里来大致说明一下“同步”这个概念。

3.1.1 “同步”的概念

其实最早的时候,BLASTSyncEngine以及SyncGroup并不是用于动画,而是和WindowContainerTransaction一起结合使用,主要是用于分屏。

分屏由于将屏幕一分为二以供两个App同时显示,那么一旦分屏发生变化(进退分屏,调整分割线位置等),那么最起码就会有两个可见的SurfaceControl参与了变化,即参与分屏的这两个App下的SurfaceControl。

那么一个很明显能够想到的问题是,如何做到这两个分屏的App界面能够一起改变呢,比如我调整分屏分割线的位置,我肯定不希望看到上分屏的App界面改变之后,下分屏的App界面没有跟着一起改变,而是又过了几百毫秒才开始变化。这是一个肯定会遇到的问题,App界面改变是在窗口绘制完成之后,而WMS无法控制窗口绘制的时间,因此如果WMS不加以控制,那么就会出现由于两个窗口的绘制时间不同,导致用户看到的两个界面先后进行了改变(异步),而非同一时间进行了改变(同步)。

因此当时引出了BLASTSyncEngine以及SyncGroup的概念,这套机制最开始就是用来保证使用WindowContainerTransaction的模块(比如分屏)可以做到SurfaceControl的同步。

同步的大致做法,则是创建一个统一的Transaction对象(即SyncGroup.finishNow中创建的那个Transaction类型的局部变量merged),来收集所有参与到分屏的SurfaceControl的变化,并且只有等到所有参与分屏的窗口都绘制完成后,才对这个Transaction对象调用apply方法,这样就保证了所有的SurfaceControl变化在一次Transaction.apply中进行了提交。

从以上介绍可以看到要实现同步,有两个比较重要点,一是有一个统一的Transaction来收集所有SurfaceControl的变化,二是当所有参与同步的窗口绘制完成后再调用Transaction.apply。

更多的内容可以去看下之前分析WindowContainerTransaction的系列文章。

3.1.2 WindowContainer.mSyncTransaction

回到现在Android14的ShellTransitions中来,现在是动画也开始用BLASTSyncEngine这一套逻辑了,接下来看下现在动画是如何实现同步的。

从上面的介绍中,我们知道同步的两个重要的点是:

1)、有一个统一的Transaction来收集所有SurfaceControl的变化。

2)、当所有参与同步的窗口绘制完成后再调用这个统一的Transaction对象的Transaction.apply方法。

可知和这个Transaction是有很大的关系。

第二点放到后面再说,这一节我们分析一下第一点,即有一个统一的Transaction来收集所有SurfaceControl的变化。

之前看SyncGroup.finishNow,我们知道了这个统一的Transaction就是这里创建的Transaction类型的局部变量merged,它合并了所有参与动画的WindowContainer的mSyncTransaction中收集的内容,那么WindowContainer.mSyncTransaction又是什么?

再举一个例子,来看一个经典的对SurfaceControl进行显示的操作,为WindowSurfaceController.showRobustly:

在这里插入图片描述

根据以往的系列文章知WindowSurfaceController.mSurfaceControl对应的是SurfaceFlinger侧的BufferStateLayer。

调用Transaction.show的时候,只是将对SurfaceControl的操作暂存在了Transaction中(更准确的说,是native层的layer_state_t结构体中),只有当调用Transaction.apply的时候,这个对SurfaceControl的操作才算真正提交到了SurfaceFlinger端,进而作用到了Layer上。

那么我们再看下这个传参Transaction对象是从哪里拿到的,一般来说,调用堆栈为:

WindowState.prepareSurfaces

-> WindowStateAnimator.prepareSurfaces

-> WindowSurfaceController.showRebustly

看到是在WindowState.prepareSurfaces中,通过WindowContainer.getSyncTransaction拿到的:

在这里插入图片描述

WindowContainer.getSyncTransaction为:

在这里插入图片描述

如果WindowContainer.mSyncTransactionCommitCallbackDepth大于0,或者WindowContainer.mSyncTransaction不为SYNC_STATE_NONE,说明此时WindowContainer仍然处于需要同步的场景中,因此返回WindowContainer.mSyncTransaction,否则返回WindowContainer.getPendingTransaction。

WindowContainer.getPendingTransaction为:

在这里插入图片描述

一般返回的是DisplayContent的mPendingTransaction。

再看下mSyncTransaction和mPendingTransaction的定义以及初始化:

在这里插入图片描述

看到mSyncTransaction和mPendingTransaction其实都是一个普通的Transaction对象,本质上没有区别,区别在于它们的使用方式:

1)、pendingTransaction基本上每次RootWindowContainer.performSurfacePlacementNoTrace就apply一次:

在这里插入图片描述

可以认为是使用pendingTransaction对SurfaceControl操作后,很快就会调用Transaction.apply,也就是说使用pendingTransaction对SurfaceControl进行的操作很快就能见到效果。

2)、syncTransaction的apply方法的调用时机则是和Transition的流程密切相关,只有走到特定的阶段才会调用Transaction.apply方法,以后的分析中我们会看到。

最后一句话总结一下pendingTransaction和syncTransaction的区别就是,需要WindowContaienr同步的场景使用syncTransaction,不需要WindowContainer同步的场景则使用pendingTransaction…怎么有点像废话呢

3.2 注册TransactionCommittedListener回调以及超时处理

在这里插入图片描述

主要内容是:

1)、为所有参与到动画的WindowContainer调用waitForSyncTransactionCommit方法.

2)、定义一个CommitCallback的类,这个类有一个自定义的onCommitted方法,以及复写Runnable的run方法。

3)、创建一个CommitCallback类的对象,callback。

4)、调用Transaction.addTransactionCommittedListener方法注册TransactionCommittedListener回调,回调触发的时候执行这个callback的onCommitted方法。

5)、Handler.postDelayed将这个callback添加到了MessageQueue中,5000ms超时之后执行这个callback的run方法。

接下来分别介绍,并不一定按照顺序。

3.2.1 注册TransactionCommittedListener回调

            CommitCallback callback = new CommitCallback();
            merged.addTransactionCommittedListener(Runnable::run,
                    () -> callback.onCommitted(new SurfaceControl.Transaction()));

看到这里为merged调用了Transaction.addTransactionCommittedListener方法:

在这里插入图片描述

从注释来看,TransactionCommittedListener的onTransactionCommitted回调方法会在Transaction被apply的时候调用,另外这个回调被执行的时候也说明当前Transaction将不会被一个新的Transaction对象复写。

那么再结合SyncGroup.finishNow的代码,也就是说,当merged这个Transaction对象被apply后,Transaction.addTransactionCommittedListener这段代码将被执行:

executor.execute(listener::onTransactionCommitted)

也就是异步执行传参listener的onTransactionCommitted方法,即SyncGroup.finishNow中的这段代码:

callback.onCommitted(new SurfaceControl.Transaction())

即当merged这个Transaction对象被apply后,这里定义的CommitCallback类的onCommitted方法将会被执行。

分析了注册TransactionCommittedListener回调后,我们可以再回过头来看CommitCallback类的定义,即它的onCommitted方法和run方法。

3.2.2 CommitCallback.onCommitted

先看onCommitted方法,从上面的分析我们知道这个方法将会在merged被apply的时候调用,作用为:

1)、将CommitCallback从MessageQueue中移除,即merged在规定的5000ms内得到apply了,那么就不需要触发超时了。

2)、将ran这个变量置为true,因为Transaction有可能在5000ms超时后才apply,那么onCommitted方法就有可能走两次。

3)、调用WindowContainer的onSyncTransactionCommitted方法,onSyncTransactionCommitted方法要和waitForSyncTransactionCommit结合着来看:

在这里插入图片描述

正好WindowContainer.waitForSyncTransactionCommit方法也是在上面被调用了,感觉这两个方法主要是对mSyncTransactionCommitCallbackDepth这个成员变量进行操作,而mSyncTransactionCommitCallbackDepth作用的地方也在我们之前看过的WindowContainer.getSyncTransaction中:

在这里插入图片描述

我自己的看法是,这个变量用来继续延长WindowContainer的同步状态。

如我们之前第一节合并Transaction中提到的,这里会为所有参与同步的WindowContainer调用WindowContainer.finishSync方法,这将会使得WindowContainer的mSyncState重置为SYNC_STATE_NONE,那么假如没有mSyncTransactionCommitCallbackDepth,此时调用WindowContainer.getSyncTransaction将会返回pendingTransaction,而非syncTransaction,也就是说WindowContainer的同步状态在走到SyncGroup.finishNow的时候就结束了。

而加入了mSyncTransactionCommitCallbackDepth之后,WindowContainer的同步状态的结束将会被延迟到merged被apply的时候。

为什么要这么做呢,因为此时SyncGroup.finishNow距离merged被apply还有一段时间,而且这个时间其实可能会超过5000ms,即上面规定的超时时限。假如在merged被apply之前,WindowContainer又发生了变化,那么如果没有mSyncTransactionCommitCallbackDepth的存在,此时WindowContainer将使用pendingTransaction,并且pendingTransaction如果再在merged被apply之前就apply,就会出现新的Transaction(pendingTransaction)的内容被旧的Transaction(syncTransaction)内容覆盖的情况。

4)、调用WindowContainer.onSyncTransactionCommitted,将所有参与动画的WindowContainer.mSyncTransaction收集到Transaction类型传参t中,集中进行一次apply。

如之前所说,此时距离merged被apply还有一段时间,在这段时间内参与到动画的WindowContainer是有可能继续发生变化的,而syncTransaction合并到merged的操作已经结束了,为了让这个时间段的变化也能够被应用,所以这里调用WindowContainer.mSyncTransaction,将收集到变化的syncTransaction都合并到一个Transaction中,然后调用apply。

但是这样不就是后发生变化的WindowContainer的Transaction先被apply了吗,这样不是还会出现上面提到的Transaction被覆盖的情况?目前我暂时没有碰到过这种情况,但是这个逻辑我感觉是有问题的。

3.2.3 CommitCallback.run

根据我们的分析,我们知道了这个方法将会在5000ms超时后调用,主要的内容是:

1)、调用TransactionReadyListener.onTransactionCommitTimeout,通知关心方超时的情况。

2)、调用CommitCallback.onCommitted方法,应该是想让syncTransaction收集到的变化得到应用,但是之前合并到merged那部分变化则是永久丢失掉了,这部分应该才是最重要的。

3)、这里打印了一条log:

                    Slog.e(TAG, "WM sent Transaction to organized, but never received" +
                           " commit callback. Application ANR likely to follow.");

打印了这个条log的时候,我们已经知道是处于5000ms超时的情况了,那么可能会出现本来应该显示的Layer,在5000ms的时间内得不到显示,那么屏幕上就可能会出现没有任何一个输入窗口可以作为焦点窗口的情况(输入窗口能够接收焦点,需要其Layer为可见),如果此时再来一个KeyEvent事件,那么就会发生无焦点窗口的ANR。

3.3 调用Transition.onTransactionReady

只剩下最后一点内容了,一起来看下:

在这里插入图片描述

1)、调用TransactionReadyListener类型的mListener的onTransactionReady方法。

2)、将当前SyncGroup从BLASTSyncEngine.mActiveSyncs中移除。

3)、将其成员变量mOnTimeout从MessageQueue中移除。

有关其成员变量mListener以及mOnTimeout的部分,在之前创建SyncGroup对象的时候漏说了,现在大概过一遍。

首先看下SyncGroup的构造方法:

在这里插入图片描述

1)、int类型的mSyncId成员变量保存该SyncGroup的ID。

2)、TransactionReadyListener类型的成员变量mListener保存与该SyncGroup一一对应的Transition对象,TransactionReadyListener定义为:

在这里插入图片描述

用来通知Transition同步完成以及Transaction提交超时。

所以这里调用的是Transition.onTransactionReady,Transition.onTransactionReady的内容比较多,需要单独开一篇分析。

3)、mOnTimeout则是一个Runnable,用来在超时的时候触发BLASTSyncEngine.onTImeout方法:

在这里插入图片描述

主要内容就是遍历一下参与同步的WindowContainer,看下是哪个WindowContainer没有同步完成,以及在方法的最终调用SyncGroup.finishNow,这个一点也很好理解,毕竟我们不能无限等待某一个WindowState绘制完成。

注意区分一下这里的超时和我们上面提到的超时。

这里的超时是在SyncGroup刚刚创建,或者说Transition刚开始收集的时候,开始计时的,防止某一个窗口迟迟没有完成绘制,从而无限等待这个窗口绘制完成的情况。

上面提到的超时是在SyncGroup.finishNow的时候开始计时的,防止merged这个Transaction迟迟没有得到apply(Transition没有走到下一个阶段),从而syncTransaction的收集的变化内容无法被apply的情况。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python武器库开发-武器库篇之Redis未授权漏洞扫描器(五十七)

Python武器库开发-武器库篇之Redis未授权漏洞扫描器(五十七) Redis未授权访问漏洞简介以及危害 Redis是一个开源的内存数据库,具有高性能和可扩展性。然而,由于配置不当或者默认设置,Redis服务器可能会存在未授权访问的漏洞。 未授权访问漏…

用Vite基于Vue3+ts+DataV+ECharts开发数据可视化大屏,即能快速开发又能保证屏幕适配

数据可视化大屏 基于 Vue3、Typescript、DataV、ECharts5 框架的大数据可视化(大屏展示)开发。此项目vue3实现界面,采用新版动态屏幕适配方案,全局渲染组件封装,支持数据动态刷新渲染、内部DataV、ECharts图表都支持自…

【目标检测】图解 DETR 系统框图

简略版本 Backbone:CNN backbone 学习图像的 2D 特征Positional Encoding:将 2D 特征展平,并对其使用位置编码(positional encoding)Encoder:经过 Transformer 的 encoderDecoder:encoder 的输出…

Python用于解析 XML 数据之untangle使用详解

概要 在处理 XML 数据时,解析和提取数据是一个常见的需求。虽然 Python 提供了多种处理 XML 的库,如 xml.etree.ElementTree 和 lxml,但它们通常需要编写较多的代码来解析和处理 XML 数据。untangle 库是一个轻量级的 Python 库,它提供了一种简单而直观的方式来解析 XML 数…

快速又不失灵活性的JeecgBoot框架

简介JeecgBoot 开源界 "小普元" 超越传统商业平台。引领低代码开发模式 (OnlineCoding-> 代码生成器 -> 手工 MERGE),低代码开发同时又支持灵活编码, 可以帮助解决 Java 项目 70% 的重复工作,让开发更多关注业务。既能快速提…

解决数据丢失问题的MacOS 数据恢复方法

每个人都经历过 Mac 硬盘或 USB 驱动器、数码相机、SD/存储卡等数据丢失的情况。我们中的一些人可能认为已删除或格式化的数据将永远丢失,因此就此作罢。对于 macOS 用户来说,当文件被删除时,垃圾箱已被清空,他们可能不知道如何恢…

利用GD32F470的定时器实现频率和占空比测试

1&#xff09;main函数代码如下&#xff1a; #include "gd32f4xx.h" #include <stdio.h> #include "gd32f470i_eval.h" #include "systick.h"void TIM_PwmInit(void) {rcu_periph_clock_enable(RCU_GPIOA);/* PWM输出管脚为复用推挽模式 …

一次压测引发的数据库 CPU 飙升

作者&#xff1a;昀鹤 一次压测过程中&#xff0c;当数据库的 qps 和 tps 都正常时&#xff0c;如果 cpu 利用率异常的高&#xff0c;应该如何排查&#xff1f;希望通过这篇文章&#xff0c;给你一些启发... 一、业务背景 业务需要控制频道内兑换现金的数量&#xff0c;于是在…

肆拾玖坊三级众筹模式玩法揭秘,白酒体验馆运作模式

发展至今&#xff0c;肆拾玖坊已积累了数百万忠实用户&#xff0c;拥有100多家分销商、5000多个新零售终端&#xff0c;覆盖全国34个省级行政区域、200余地市、1500个县区。成为中国创业界和酒行业的“现象级”企业。 今天&#xff0c;我们就来深入解析肆拾玖坊的营销模式&…

智能猫砂盆效果这么惊艳吗?绝对不踩雷的智能猫砂盆合集来啦

身为一个铲屎官&#xff0c;我深受“天天铲屎”的困扰。想要片刻放松都不行&#xff0c;因为猫砂盆一旦堆积屎尿&#xff0c;尤其在夏天&#xff0c;会迅速发臭&#xff0c;滋生细菌。对猫而言&#xff0c;不清理猫砂盆会让它们感到不适&#xff0c;可能引发疾病或拒绝使用猫砂…

C#唯一进程的处理Winform/WPF

C#唯一进程的处理 1.使用进程&#xff08;Process&#xff09;判断winformWPF执行效果&#xff1a; 2.使用互斥体&#xff08;Metux&#xff09;实现winformWPF实现效果&#xff1a; 在C#客户端&#xff08;Winform/WPF&#xff09;开发过程中&#xff0c;有的情况需要确保程序…

ubuntu22.04安装onlyoffice社区版

安装unbuntu22.04 https://blog.csdn.net/qq_36437991/article/details/135915360 安装onlyoffice sudo apt-get update sudo apt-get upgradepostgresql sudo apt-get install postgresql创建用户和数据库 sudo -i -u postgres psql -c "CREATE USER onlyoffice WIT…

ERP系统品牌大比拼:哪款产品更适合您的企业?

ERP集成了企业的销售、采购、生产、财务等各个环节&#xff0c;实现了资源的优化配置和信息的实时共享。然而&#xff0c;面对市场上琳琅满目的ERP系统产品&#xff0c;许多企业却陷入了选择的困境。 “哪款ERP系统更适合我的企业呢&#xff1f;”这或许是每一位企业决策者心中…

【STM32】SysTick系统滴答定时器

1.SysTick简介 CM4内核的处理和CM3一样&#xff0c;内部都包含了一个SysTick定时器&#xff0c;SysTick 是一个24 位的倒计数定时器&#xff0c;当计到0 时 &#xff0c;将 从RELOAD 寄存器中自动重装载定时初值。只要不把它在SysTick 控制及状态寄存器中的使能位清除&#xf…

免费ai写作?这三款软件是你的好帮手!

在信息爆炸的今天&#xff0c;自媒体已成为越来越多人展现自我、分享知识的平台。然而&#xff0c;对于许多自媒体创作者来说&#xff0c;写作过程中的灵感枯竭、文笔不畅等问题常常困扰着他们。幸运的是&#xff0c;随着人工智能技术的飞速发展&#xff0c;免费AI写作软件应运…

MyBatisPlus基础学习

一、简介 二、集成MP 三、入门HelloWorld 四、条件构造器EntityWrapper 五、ActiveRecord(活动记录 ) 六、代码生成器 七、插件扩展 八、自定义全局操作 九、公共字段自动填充 十、Oracle主键Sequence 十一、Idea快速开发插件 十二、mybatis-plus实践及架构原理

一文带你全面详细了解安全运维

一、安全运维-网络 1、IP地址相关 IP地址属于网络层地址&#xff0c;用于标识网络中的节点设备。 IP地址由32bit构成&#xff0c;每8bit一组&#xff0c;共占用4个字节。 IP地址由两部分组成&#xff0c;网络位和主机位。 IP地址分类&#xff1a; 类别网络位子网掩码私有地…

【OceanBase诊断调优】 —— DDL时报磁盘不足问题排查

1. 背景 由于在4.x的部分版本中&#xff0c;我们对于一些ddl操作还存在磁盘空间放大问题&#xff0c;本文主要介绍了这一类问题的排查。 2. 问题排查 2.1 整体排查链路 2.2 问题现象 DDL过程中报磁盘空间不足&#xff0c;需要确认是否符合预期&#xff0c;如果是符合预期&a…

告别数据孤岛,Xinstall助力App广告投放实现全渠道归因!

在移动互联网时代&#xff0c;App的推广和运营已成为企业不可或缺的一部分。然而&#xff0c;面对五花八门的广告渠道和繁杂多样的投放方式&#xff0c;如何有效追踪广告和渠道效果&#xff0c;如何甄别和选择流量渠道&#xff0c;成为了众多App开发商和运营者头疼的问题。幸运…

Qt制作程序启动界面类QSplashScreen实例测试详解

目录 一、QSplashScreen的概述 二、QSplashScreen静态图片加载 1、主程序实现 2、mainwindow.h实现 3、mainwindows.cpp实现 三、QSplashScreen动态图片加载 1、主程序实现 2、mainwindow.h实现 3、mainwindows.cpp实现 一、QSplashScreen的概述 QSplashScreen&#x…