App卡帧与BlockCanary
作者:图个喜庆
一,前言
app卡帧一直是性能优化的一个重要方面,虽然现在手机硬件性能越来越高,明显的卡帧现象越来越少,但是了解卡帧相关的知识还是非常有必要的。
本文分两部分从app卡帧的原理出发,讨论屏幕刷新机制,handler消息机制,为什么在主线程执行耗时任务会造成卡帧。另一部分讨论BlockCanary的原理,它是如何检测方法耗时的。
二,屏幕刷新机制
(1)卡帧的定义
大家小时候应该都玩过一个玩具,5毛一本的连环画小书,每一页绘制一幅画,用手指快速翻动会产生一个动画效果。
假设这本小书用50页构成一个完整的动画,丢了一页两页,可能看不出来,由于某些原因丢了10页,20页就完全没法看了。
app卡帧就是因为某些原因影响了屏幕绘制,电影每秒24帧,一般的手机是每秒60帧,高刷手机是每秒90帧 ,120帧。
如果发生了卡帧原本一秒内应该出现60张画面,实际只出现了30张画面,用户就会感觉界面不流畅。
用歌词举例卡帧 :
正常歌词【梦美的太短暂,孟克桥上呐喊,这世上的热闹,出自孤单】
卡帧歌词【梦xxx暂,xx桥xx喊,这世xx热闹,xxx单】
(2)屏幕刷新基础概念
在京东随便找几个手机,查看它们的屏幕刷新率参数: 144Hz ,90Hz,60Hz。
代表一秒内屏幕刷新的次数,常见的16.6ms刷新一次的概念来自 60Hz的普通手机 1000 / 60 ≈ 16ms。
60Hz的手机,每秒刷新60次,每次间隔16.6ms
90Hz的手机,每秒刷新90次,每次间隔11.1ms
144Hz的手机,每秒刷新144次,每次间隔6.9ms
手机的展示内容不是固定的,没办法预知提前绘制,所以手机硬件必须以极快的速度计算并绘制好下个瞬间要展示的图像。
60Hz的手机每个16ms就会出一帧图像。为了绘制这帧画有三个硬件参与其中:CPU,GPU,屏幕(display)
CPU负责计算数据,屏幕是由一个个像素点组成的,CPU根据编写的程序,计算这块像素展示什么颜色,形状之类的。
GPU负责把CPU计算好的数据进行渲染,放到缓存中
屏幕把缓存中的数据呈现到屏幕上
手机刷新频率是硬件决定的不会发生改变,CPU和GPU的工作必须在固定时间完成,如果没有按时完成就会产生掉帧。
(3)VSync ,Choreographer,view绘制
以固定频率刷新屏幕这种机制叫做VSync机制。Android系统每隔16ms发出VSYNC信号,触发UI渲染,Vsync机制并不是Android独创的,是一种在PC上很早就广泛使用的技术,Android在4.1版本引入Vsync机制。
这就引发了一个问题,屏幕刷新频率是非常快的,程序中关于view绘制的代码写在onMeasure()``onDraw()``onLayout()
三个方法中,而它们是不会随意调用的,并不与Vsync信号的频率保持一致,只有当主动调用invalidate()
或 requestLayout()
方法才会进行view重绘。
以requestLayout()
为例,看看为什么view的刷新与底层发送Vsync信号的频率不一致。
requestLayout()
方法在view中实现,
(a)调用自身mParent
的requestLayout()
方法,因为每个view都mParent
对象,会不断向上查找,找到根view,DecorView。
(b) DecorView
的parent是ViewRootImpl
,两者在activity启动时绑定。
(c)在ViewRootImpl
的requestLayout()
方法中 调用 scheduleTraversals();
(d)scheduleTraversals();
的核心代码向主线程消息队列设置同步屏障 和 调用 Choreographer.postCallback()
需要注意 mTraversalRunnable
对象,当接收到底层传递的Vsync信号后会被执行。内部调用performTraversals();
方法,接近100行,巨复杂,会按顺序调用performMeasure()
performLayout()
performDraw()
执行view绘制流程
//View
public void requestLayout() {//代码省略if (mParent != null && !mParent.isLayoutRequested()) {mParent.requestLayout();}}public void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {checkThread();mLayoutRequested = true;scheduleTraversals();}
}void scheduleTraversals() {if (!mTraversalScheduled) {mTraversalScheduled = true;mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);notifyRendererOfFramePending();pokeDrawLockIfNeeded();}}final class TraversalRunnable implements Runnable {@Overridepublic void run() {doTraversal();}}void doTraversal() {if (mTraversalScheduled) {mTraversalScheduled = false;mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);if (mProfile) {Debug.startMethodTracing("ViewAncestor");}performTraversals();if (mProfile) {Debug.stopMethodTracing();mProfile = false;}}}
Choreographer
译名编舞者,跳舞的时候要跟着节拍,底层发出的Vsync信号,就好像舞蹈的节奏一样有条不紊的进行。Choreographer处于view与底层之间连接两者,承上启下,负责UI刷新。
(a)ViewRootImpl
调用Choreographer.postCallback(int callbackType, Runnable action, Object token)
方法,重要的一个步骤是把 ViewRootImpl
传递的参数,以callbackType
作为数组的下标 Runnable
作为value, 保存到 类型为CallbackQueue[]
的数组中,
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token)
,收到Vsync信号后从数组中取出执行Runnable
任务
(b)Choreographer.postCallback
()最终会调用到 FrameDisplayEventReceiver
的nativeScheduleVsync()
方法(定义在父类中),是一个native方法,作用是向底层注册监听,告诉底层当前应用有UI有更新需要重绘
(c)注册监听之后, FrameDisplayEventReceiver.onVsync()
会收到底层的Vsync信号,主要逻辑发送一条异步消息,FrameDisplayEventReceiver
自身实现Runnable
接口,handle发送异步消息,执行的就是自身run()
方法中的逻辑 ,核心代码是doCallbacks()
private final class FrameDisplayEventReceiver extends DisplayEventReceiverimplements Runnable{@Overridepublic void onVsync(long timestampNanos, long physicalDisplayId, int frame,VsyncEventData vsyncEventData) {Message msg = Message.obtain(mHandler, this);msg.setAsynchronous(true);mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);}@Overridepublic void run() {mHavePendingVsync = false;doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);}}void doFrame(long frameTimeNanos, int frame,DisplayEventReceiver.VsyncEventData vsyncEventData) {
//省略代码doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos, frameIntervalNanos);mFrameInfo.markAnimationsStart();doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos, frameIntervalNanos);doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos,frameIntervalNanos);mFrameInfo.markPerformTraversalsStart();doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos, frameIntervalNanos);doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos, frameIntervalNanos);
}
(d) Choreographer.doCallbacks()
要和 Choreographer.postCallback()
结合起来看才合理
postCallback()
添加UI更新任务到数组中,任务大致分为三种:输入事件,动画,UI绘制。根据常量类型就是数组下标。,当Choreographer
接收到底层传递的Vsync信号后,调用doCallbacks()
根据下标从数组取出任务执行。
public static final int CALLBACK_INPUT = 0; //输入事件处理
public static final int CALLBACK_ANIMATION = 1;//处理动画
//处理插入更新的动画 没看到ViewRootImpl中有使用它 不太清楚和 CALLBACK_ANIMATION的区别
public static final int CALLBACK_INSETS_ANIMATION = 2;
public static final int CALLBACK_TRAVERSAL = 3;//UI绘制,measure,layout,draw
public static final int CALLBACK_COMMIT = 4;//不太清楚它的作用
//初始化CallbackQueue数组使用
private static final int CALLBACK_LAST = CALLBACK_COMMIT;private final CallbackQueue[] mCallbackQueues;private Choreographer(Looper looper, int vsyncSource) {mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];for (int i = 0; i <= CALLBACK_LAST; i++) {mCallbackQueues[i] = new CallbackQueue();}}void doCallbacks(int callbackType, long frameTimeNanos, long frameIntervalNanos) {CallbackRecord callbacks;synchronized (mLock) {//省略代码callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);}}public void postCallback(int callbackType, Runnable action, Object token) {//省略代码 与 方法调用mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
}
(4)Choreographer流程总结
(a)底层根据屏幕刷新率按时发送Vsync信号
(b)应用通过调用requestLayout()
,invalidate()
或 动画等表明自己当前需要更新
(c)ViewRootImpl
内定义更新逻辑,如TraversalRunnable
定义UI绘制逻辑。当代码执行requestLayout()
,invalidate()
时把任务投递到Choreographer
内部的任务数组中
(d)Choreographer
可以接收:输入事件,动画,UI绘制三种任务,但不是立即执行,先将它们保存到数组中。同时Choreographer
调用native方法向底层注册Vsync信号监听
(e)Choreographer
收到Vsync信号回调,按照数组顺序依次执行任务,输入事件优先级最高,其次是动画,最后是UI绘制。 在UI绘制阶段通过view树层层调用measure,layout,draw
(f)整个流程说白了就是定义任务,监听事件,执行任务
偷一张代码流程图
屏幕刷新机制还有很多细节没有讲,屏幕的双缓存,Choreographer代码细节等可以看参考中的文章学习
(5)Handler同步屏障和异步消息
在Handler消息队列每条消息按顺序逐个执行,假设此时接收到了Vsync信号,向主线程中投递了几个绘制相关的消息。但是消息队列中已经有10个消息了,如果仍然按照正常逻辑逐个执行等前面所有的消息执行完毕才会轮到绘制相关的消息执行,肯定是不合理的。
不能确定前面的消息何时执行完毕,没准任务非常复杂耗时很久。就算不耗时,轻微的等待对于绘制影响也是巨大的。绘制一旦响应的慢了,在用户看来就是系统交互太慢了,差评!
所以为了即使处理绘制这种重要消息,设计出了同步屏障 和 异步消息机制。
异步消息设置方式如下,单独使用异步消息没啥用,必须结合同步屏障一起使用
val handler = Handler(Looper.getMainLooper())
val msg = handler.obtainMessage()
msg.isAsynchronous= true
同步屏障通过MessageQueue.postSyncBarrier()
开启,向队列头部添加一个没有target的(target==null)的message。
很遗憾MessageQueue.postSyncBarrier()
方法是私有方法,不能允许开发者调用。想想也合理,开发层面有什么紧急的任务非要和应用重绘这种系统任务抢地盘?
public int postSyncBarrier() {return postSyncBarrier(SystemClock.uptimeMillis());}private int postSyncBarrier(long when) {// Enqueue a new sync barrier token.// We don't need to wake the queue because the purpose of a barrier is to stall it.synchronized (this) {final int token = mNextBarrierToken++;final Message msg = Message.obtain();msg.markInUse();msg.when = when;msg.arg1 = token;Message prev = null;Message p = mMessages;if (when != 0) {while (p != null && p.when <= when) {prev = p;p = p.next;}}if (prev != null) { // invariant: p == prev.nextmsg.next = p;prev.next = msg;} else {msg.next = p;mMessages = msg;}return token;}}
在MessageQueue的next()方法关于消息的处理逻辑
if (msg != null && msg.target == null)
判断是否开启同步屏障
循环条件判断 while (msg != null && !msg.isAsynchronous())
Message next() {
//省略代码Message msg = mMessages;if (msg != null && msg.target == null) {// Stalled by a barrier. Find the next asynchronous message in the queue.do {prevMsg = msg;msg = msg.next;} while (msg != null && !msg.isAsynchronous());}if (msg != null) {if (now < msg.when) {// Next message is not ready. Set a timeout to wake up when it is ready.nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);} else {// Got a message.mBlocked = false;if (prevMsg != null) {prevMsg.next = msg.next;} else {mMessages = msg.next;}msg.next = null;if (DEBUG) Log.v(TAG, "Returning message: " + msg);msg.markInUse();return msg;}} else {// No more messages.nextPollTimeoutMillis = -1;}
}
在ViewRootImpl
中,开始绘制任务之前开启同步屏障,借此保证Choreographer.FrameHandler
发出的消息能够快速响应执行任务。
当任务结束或取消移除同步屏障,保证其他消息顺利进行。
// ViewRootImpl
void scheduleTraversals() {if (!mTraversalScheduled) {//开启同步屏障mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);}
}void unscheduleTraversals() {if (mTraversalScheduled) {mTraversalScheduled = false;//移除同步屏障mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);mChoreographer.removeCallbacks(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);}}void doTraversal() {if (mTraversalScheduled) {mTraversalScheduled = false;//移除同步屏障mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);}
}//Choreographerprivate final class FrameHandler extends Handler {public FrameHandler(Looper looper) {super(looper);}@Overridepublic void handleMessage(Message msg) {switch (msg.what) {case MSG_DO_FRAME:doFrame(System.nanoTime(), 0, new DisplayEventReceiver.VsyncEventData());break;case MSG_DO_SCHEDULE_VSYNC:doScheduleVsync();break;case MSG_DO_SCHEDULE_CALLBACK:doScheduleCallback(msg.arg1);break;}}}
(6)卡帧原理
为什么布局复杂会卡帧?主线程执行耗时操作会卡帧?
布局复杂,层级过多相应的计算view树数据的时间也会增加,view的绘制是在主线程进行的。如果不能在16.6ms内完成就会影响一下次绘制消息的处理。
虽然有同步屏障机制,但是如果主线程执行耗时操作超过16.6ms一致被占用,即便有同步屏障机制,但是也要当前消息执行完毕才会生效,也会出现掉帧的情况
主线程一次只能做一件事,处理耗时任务导致没有处理屏幕刷新的绘制任务,屏幕数据无法更新,产生掉帧现象
三,BlockCanary
经过上述卡帧原理分析可以得知,核心的绘制消息都是在主线程消息队列中处理的,当屏幕有重绘需求的时候,按照手机屏幕刷新率60Hz,每16.6ms刷新一帧画面有条不紊的进行。
在主线程进行耗时操作,影响了屏幕刷新的节奏,并不能准时执行每16.6ms一次的界面重绘,就会产生掉帧现象。
在优化性能时,检测寻找问题点比解决问题要麻烦的多。想靠人工在一行一行的代码中寻找耗时点是不可能的。
BlockCanary是Android平台的性能监控框架,能够找出主线程的耗时操作避免卡帧,核心原理并不复杂,非常巧妙。
BlockCanary分为两部分,检测耗时 和 耗时方法信息输出
(1)检测耗时
利用Handler消息机制实现,Looper线程唯一,无论有多少个Handler向主线程中发送消息都会走到同一个Looper中。
在Looper的loop() 方法中,msg.target.dispatchMessage()
是分发处理消息的位置,如果两次dispatchMessage()
调用时间间隔过长,就表明有耗时操作,出现掉帧情况需要优化。
那么如何判断dispatchMessage()
方法的执行时长呢?而且要监听到所有在主线程执行的消息。
在Looper源码中,dispatchMessage()
方法调用前后,Printer.println()
调用进行日志输出,默认情况下Printer
对象为null,可以通过setMessageLogging()
方法设置
//自定义Printer
handler.looper.setMessageLogging(object :Printer{override fun println(x: String?) {}})//Looper源码
private Printer mLogging;public static void loop() {...for (;;) {...// This must be in a local variable, in case a UI event sets the loggerPrinter logging = me.mLogging;if (logging != null) {logging.println(">>>>> Dispatching to " + msg.target + " " +msg.callback + ": " + msg.what);}msg.target.dispatchMessage(msg);if (logging != null) {logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);}...}
}public void setMessageLogging(@Nullable Printer printer) {mLogging = printer;}
在blockcanary中 定义LooperMonitor
类 实现Printer
接口,监听dispatchMessage()
方法调用耗时,核心代码如下
(a)通过变量mPrintingStarted
判断是在dispatchMessage()
方法前调用还是在方法后调用
(b)保存第一次调用时间mStartTimestamp
(c)通过判断开始时间与结束时间的差值,是否超过预定义的卡帧阈值,判断是否产生耗时操作
代码真的非常简单,知道原理的同学分分可以自定义一个。
@Override
public void println(String x) {if (mStopWhenDebugging && Debug.isDebuggerConnected()) {return;}if (!mPrintingStarted) {mStartTimestamp = System.currentTimeMillis();mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();mPrintingStarted = true;startDump();} else {final long endTime = System.currentTimeMillis();mPrintingStarted = false;if (isBlock(endTime)) {notifyBlockEvent(endTime);}stopDump();}
}private boolean isBlock(long endTime) {return endTime - mStartTimestamp > mBlockThresholdMillis;
}
(2)耗时方法信息输出
定位到那个方法有耗时操作后,BlockCanary利用线程API获取主线程堆栈,打印后可以到方法调用信息
val startTrace = Thread.currentThread().stackTrace
val sbInfo = StringBuilder()
for (item:StackTraceElement in startTrace) {sbInfo.append(item.toString())sbInfo.append("\n")
}
Log.d(TAG,sbInfo.toString())//输出日志如下 方法在 RegisterActivity.onCreate() 方法中调用D/aaa: dalvik.system.VMStack.getThreadStackTrace(Native Method)java.lang.Thread.getStackTrace(Thread.java:1538)com.example.module.user.register.RegisterActivity.onCreate(RegisterActivity.kt:35)android.app.Activity.performCreate(Activity.java:7232)android.app.Activity.performCreate(Activity.java:7221)android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1272)android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2965)android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3120)android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)android.app.ActivityThread$H.handleMessage(ActivityThread.java:1840)android.os.Handler.dispatchMessage(Handler.java:106)android.os.Looper.loop(Looper.java:207)android.app.ActivityThread.main(ActivityThread.java:6878)java.lang.reflect.Method.invoke(Native Method)com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)
为了帮助到大家更好的全面清晰的掌握好性能优化,准备了相关的核心笔记(还该底层逻辑):https://qr18.cn/FVlo89
性能优化核心笔记:https://qr18.cn/FVlo89
启动优化
内存优化
UI优化
网络优化
Bitmap优化与图片压缩优化:https://qr18.cn/FVlo89
多线程并发优化与数据传输效率优化
体积包优化
《Android 性能监控框架》:https://qr18.cn/FVlo89
《Android Framework学习手册》:https://qr18.cn/AQpN4J
- 开机Init 进程
- 开机启动 Zygote 进程
- 开机启动 SystemServer 进程
- Binder 驱动
- AMS 的启动过程
- PMS 的启动过程
- Launcher 的启动过程
- Android 四大组件
- Android 系统服务 - Input 事件的分发过程
- Android 底层渲染 - 屏幕刷新机制源码分析
- Android 源码分析实战
相关文章:

App卡帧与BlockCanary
作者:图个喜庆 一,前言 app卡帧一直是性能优化的一个重要方面,虽然现在手机硬件性能越来越高,明显的卡帧现象越来越少,但是了解卡帧相关的知识还是非常有必要的。 本文分两部分从app卡帧的原理出发,讨论屏…...

bpmnjs Properties-panel拓展(ExtensionElements拓展篇)
接上文bpmnjs Properties-panel拓展(属性设置篇),继续记录下第三个拓展需求的实现。 需求简述 在ExclusiveGateway标签的extensionElements标签中增加子标签<activiti:executionListener>子标签,可增加复数子标签。子标签…...

虚拟机的使用
首先需要安装VMware软件,这是虚拟机,在里面可以实现在windows的笔记本上运行包括,windows11和linux系统的开发和研究。 VMware是一种虚拟化技术,可以让你在一台物理计算机上运行多个操作系统和应用程序,而不需要重启或…...

CSS Flex布局
前言 Flex布局(弹性盒子布局) 是一种用于在容器中进行灵活和自适应布局的CSS布局模型。通过使用Flex布局,可以更方便地实现各种不同尺寸和比例的布局,使元素在容器内自动调整空间分配。 Flex-组成 Flex布局由以下几个主要组成部分…...
Virtual
虚拟接口可以用作编写操作系统和驱动程序独立测试的一种方式。任何连接到同一通道(来自同一Python进程)的VirtualBus实例都将相互接收消息。 如果消息应跨进程或主机边界发送,请考虑使用多播IP接口,并参考虚拟接口对不同虚拟接口进行比较和一般性讨论。 Example import …...
6、监测数据采集物联网应用开发步骤(5.2)
监测数据采集物联网应用开发步骤(5.1) 包含4个类数据库连接(com.zxy.db_Self.ConnectionPool_Self.py)、数据库操作类(com.zxy.db_Self.Db_Common_Self.py)、数据库管理类(com.zxy.db_Self.DBManager_Self.py…...
解释 Git 的基本概念和使用方式
该文为AI自动生成,InsCode AI 创作助手 Git 是一种版本控制工具,用于跟踪代码或文件的更改历史记录。以下是 Git 的基本概念和使用方式: 仓库 (Repository):仓库是一个存储项目代码和历史记录的地方,可以在本地或远程…...
不同ubuntu系统下的不同ros系统可以互相通讯吗
可以的,不同版本的Ubuntu系统和ROS版本的机器仍然可以实现ROS节点之间的通信。 主要的原因有:1. ROS节点间通信是通过ROS master实现的。不同机器上的ROS节点都可以连接到同一个ROS master,从而实现通信。 2. ROS消息系统可以兼容不同的ROS版本。即使节点使用的ROS版本不同,也…...

数学建模-模型详解(2)
微分模型 当谈到微分模型时,通常指的是使用微分方程来描述某个系统的动态行为。微分方程是描述变量之间变化率的数学方程。微分模型可以用于解决各种实际问题,例如物理学、工程学、生物学等领域。 微分模型可以分为两类:常微分方程和偏微分…...

IT运维:使用数据分析平台监控DELL服务器
概述 在企业日常运维中,我们有着大量的服务器设备,设备故障一般可以通过常用的监控软件实现自动告警,但如果在管理运维中我们要做的不仅仅是发现故障,处理硬件故障,我们还需要进一步的了解,今年一共出现了多…...

Spring Cloud Alibaba-Sentinel规则
1 流控规则 流量控制,其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时 对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。 第1步: 点击簇点链路,我们就可以看到访…...
go http-proxy
我们这里主要讲使用HTTP/1.1协议中的CONNECT方法建立起来的隧道连接,实现的HTTP Proxy。这种代理的好处就是不用知道客户端请求的数据,只需要原封不动的转发就可以了,对于处理HTTPS的请求就非常方便了,不用解析他的内容…...

用变压器实现德-英语言翻译【01/8】:嵌入层
一、说明 本文是“用变压器实现德-英语言翻译”系列的第一篇文章。它引入了小规模的嵌入来建立感知系统。接下来是嵌入层的变压器使用。下面简要概述了每种方法,然后是德语到英语的翻译。 二、技术背景 嵌入层的目标是使模型能够详细了解单词、标记或其他输入之间的…...
【vue3.0中ref与reactive的区别及使用】
什么是ref与reactive ref与reactive都是Vue3.0中新增的API,用于响应式数据的处理。 1. ref ref是一个函数,可以用于将一个普通的数据类型转换成响应式数据。ref返回一个包含value属性的对象,通过修改value属性的值,可以触发组件…...

计算机竞赛 基于情感分析的网络舆情热点分析系统
文章目录 0 前言1 课题背景2 数据处理3 文本情感分析3.1 情感分析-词库搭建3.2 文本情感分析实现3.3 建立情感倾向性分析模型 4 数据可视化工具4.1 django框架介绍4.2 ECharts 5 Django使用echarts进行可视化展示5.1 修改setting.py连接mysql数据库5.2 导入数据5.3 使用echarts…...
C++ 动态分配内存|动态数组
int** arr new int* [n]; for (int i 0; i < n; i) {arr[i] new int[2]; } 以上代码是用C动态分配了一个二维数组arr,其中arr是一个指向int指针的指针,n是一个整数。代码的目的是创建一个包含n个大小为2的整数数组的二维数组。 首先,…...
React Diff算法原理
文章目录 前言Diff算法原理 前言 👉点此(想要了解Diff算法) Diff算法原理 React Diff算法是React用于更新虚拟DOM树的一种算法。它通过比较新旧虚拟DOM树的差异,然后只对有差异的部分进行更新,从而提高性能。 Reac…...

查局域网所有占用IP
查局域网所有占用IP 按:winr 出现下面界面,在文本框中输入 cmd 按确定即可出现cmd命令界面 在cmd命令窗口输入你想要ping的网段,下面192.168.20.%i即为你想要ping的网段,%i代表0-255 for /L %i IN (1,1,254) DO ping -w 1 -n 1…...

【MySQL】引擎类型
与其他DBMS一样,MySQL有一个 具体管理和处理数据的内部引擎 。在使用create table语句时,该引擎具体创建表,而在使用select或进行其他数据库处理时,该引擎在内部处理你的请求。多数时候,引擎都隐藏在DBMS内࿰…...
springMVC之HttpMessageConverter
文章目录 前言一、RequestBody二、RequestEntity三、ResponseBody四、SpringMVC处理json五、SpringMVC处理ajax六、RestController注解七、ResponseEntity总结 前言 HttpMessageConverter,报文信息转换器,将请求报文转换为Java对象,或将Java…...

JavaScript 中的 ES|QL:利用 Apache Arrow 工具
作者:来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗?了解下一期 Elasticsearch Engineer 培训的时间吧! Elasticsearch 拥有众多新功能,助你为自己…...

ESP32读取DHT11温湿度数据
芯片:ESP32 环境:Arduino 一、安装DHT11传感器库 红框的库,别安装错了 二、代码 注意,DATA口要连接在D15上 #include "DHT.h" // 包含DHT库#define DHTPIN 15 // 定义DHT11数据引脚连接到ESP32的GPIO15 #define D…...

高等数学(下)题型笔记(八)空间解析几何与向量代数
目录 0 前言 1 向量的点乘 1.1 基本公式 1.2 例题 2 向量的叉乘 2.1 基础知识 2.2 例题 3 空间平面方程 3.1 基础知识 3.2 例题 4 空间直线方程 4.1 基础知识 4.2 例题 5 旋转曲面及其方程 5.1 基础知识 5.2 例题 6 空间曲面的法线与切平面 6.1 基础知识 6.2…...

Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...

OPenCV CUDA模块图像处理-----对图像执行 均值漂移滤波(Mean Shift Filtering)函数meanShiftFiltering()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 在 GPU 上对图像执行 均值漂移滤波(Mean Shift Filtering),用于图像分割或平滑处理。 该函数将输入图像中的…...

dify打造数据可视化图表
一、概述 在日常工作和学习中,我们经常需要和数据打交道。无论是分析报告、项目展示,还是简单的数据洞察,一个清晰直观的图表,往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server,由蚂蚁集团 AntV 团队…...
laravel8+vue3.0+element-plus搭建方法
创建 laravel8 项目 composer create-project --prefer-dist laravel/laravel laravel8 8.* 安装 laravel/ui composer require laravel/ui 修改 package.json 文件 "devDependencies": {"vue/compiler-sfc": "^3.0.7","axios": …...

免费PDF转图片工具
免费PDF转图片工具 一款简单易用的PDF转图片工具,可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件,也不需要在线上传文件,保护您的隐私。 工具截图 主要特点 🚀 快速转换:本地转换,无需等待上…...
JavaScript 数据类型详解
JavaScript 数据类型详解 JavaScript 数据类型分为 原始类型(Primitive) 和 对象类型(Object) 两大类,共 8 种(ES11): 一、原始类型(7种) 1. undefined 定…...