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)调用自身mParentrequestLayout() 方法,因为每个view都mParent 对象,会不断向上查找,找到根view,DecorView。

(b) DecorView的parent是ViewRootImpl,两者在activity启动时绑定。

(c)在ViewRootImplrequestLayout() 方法中 调用 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 {
        @Override
        public 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()最终会调用到 FrameDisplayEventReceivernativeScheduleVsync() 方法(定义在父类中),是一个native方法,作用是向底层注册监听,告诉底层当前应用有UI有更新需要重绘

(c)注册监听之后, FrameDisplayEventReceiver.onVsync() 会收到底层的Vsync信号,主要逻辑发送一条异步消息,FrameDisplayEventReceiver 自身实现Runnable 接口,handle发送异步消息,执行的就是自身run()方法中的逻辑 ,核心代码是doCallbacks()

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
        implements Runnable{
				@Override
        public 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);
        }

        @Override
        public 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.next
                msg.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);
        }
}

//Choreographer

private final class FrameHandler extends Handler {
        public FrameHandler(Looper looper) {
            super(looper);
        }

        @Override
        public 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 logger
        Printer 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

  1. 开机Init 进程
  2. 开机启动 Zygote 进程
  3. 开机启动 SystemServer 进程
  4. Binder 驱动
  5. AMS 的启动过程
  6. PMS 的启动过程
  7. Launcher 的启动过程
  8. Android 四大组件
  9. Android 系统服务 - Input 事件的分发过程
  10. Android 底层渲染 - 屏幕刷新机制源码分析
  11. Android 源码分析实战

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

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

相关文章

ElasticSearch总结

ES是什么 ES是一个天生支持分布式的搜索、聚合分析的存储引擎 基于Java开发 基于Lucene的开源分布式搜索引擎 ELK &#xff1a; elasticSearch Logstah Kibana 加入 Beats 后 ELK 改为 &#xff1a;Elastic stack ES解决了什么问题 ES解决的核心问题 &#xff1a; 1.海量数…

pinia——添加插件——基础积累

问题&#xff1a;是否给pinia添加过插件&#xff1f;具体添加的方式是什么&#xff1f; 在pinia中&#xff0c;我们可以为仓库添加插件&#xff0c;通过添加插件能够扩展以下的内容&#xff1a; 为 store 添加新的属性 定义 store 时增加新的选项 为 store 增加新的方法 包装现…

three.js(六):自适应设备分辨率

自适应设备分辨率 当今大多数的PC端和移动端显示器都是HD-DPI显示器。HD-DPI 是High Definition-Dots Per Inch 的简称&#xff0c;意思是高分辨率显示器。不同设备的显示器的分辨率是不一样的。 以上图中的iPhone6/7/8 为例&#xff1a;375*667 代表的手机的屏幕的物理尺寸&a…

本地套接字通信

1.本地套接字 本地套接字的作用&#xff1a;本地的进程间通信 有关系的进程间的通信 没有关系的进程间的通信 本地套接字实现流程和网络套接字类似&#xff0c;一般采用TCP的通信流程 2.本地套接字通信的流程 - tcp // 服务器端 1.创建监听的套接字int lfd socket(AF_U…

顺序表链表OJ题(3)——【数据结构】

W...Y的主页 &#x1f60a; 代码仓库分享 &#x1f495; 前言&#xff1a; 今天是链表顺序表OJ练习题最后一次分享&#xff0c;每一次的分享题目的难度也再有所提高&#xff0c;但是我相信大家都是非常机智的&#xff0c;希望看到博主文章能学到东西的可以一键三连关注一下博主…

数据库——Redis 单线程模型详解

文章目录 Redis 基于 Reactor 模式来设计开发了自己的一套高效的事件处理模型 &#xff08;Netty 的线程模型也基于 Reactor 模式&#xff0c;Reactor 模式不愧是高性能 IO 的基石&#xff09;&#xff0c;这套事件处理模型对应的是 Redis 中的文件事件处理器&#xff08;file …

科技政策 | 四川省科学技术厅关于发布2024年第一批省级科技计划项目申报指南的通知

原创 | 文 BFT机器人 近日&#xff0c;四川省科学技术厅发布了2024年第一批省级科技计划项目申报指南&#xff1b;其中包括自然科学基金项目、重点研发计划、科技成果转移转化引导计划、科技创新基地&#xff08;平台&#xff09;和人才计划。 01 自然科学基金项目 实施周期 …

聚类分析 | MATLAB实现基于DBSCAD密度聚类算法可视化

聚类分析 | MATLAB实现基于LP拉普拉斯映射的聚类可视化 目录 聚类分析 | MATLAB实现基于LP拉普拉斯映射的聚类可视化效果一览基本介绍程序设计参考资料 效果一览 基本介绍 基于DBSCAD密度聚类算法可视化&#xff0c;MATLAB程序。 使用带有KD树加速的dbscan_with_kdtree函数进行…

你的住宅安全吗?这个技能赶紧学学

随着城市化的不断加速和人口增长&#xff0c;住宅小区的管理和安全问题也愈发凸显出来。在这种背景下&#xff0c;门禁监控系统成为了一种既有效又实用的解决方案。 门禁监控系统不仅可以控制和管理出入小区的人员和车辆&#xff0c;还可以提供实时监控和记录&#xff0c;为小区…

C++ Day6

目录 一、菱形继承 1.1 概念 1.2 格式 二、虚继承 2.1 作用 2.2 格式 2.3注意 三、多态 3.1函数重写 3.2 虚函数 3.3 赋值兼容规则 3.4 多态中&#xff0c;函数重写的原理 3.5 虚析构函数 3.5.1 格式 3.6 纯虚函数 3.6.1格式 四、抽象类 五、模板 5.1模板的特…

CTFhub-sqli注入-报错注入

用到的函数 updatexml(1&#xff0c; &#xff0c;1) concat(0x7e, ,0x7e) group_concat(目标值) right(&#xff0c;32) 1 1 1 union select updatexml(1,concat(0x7e,database(),0x7e),1) 1 union select updatexml(1,concat(0x7e,(select(group_concat(ta…

Vue3 学习

基础 js:https://www.bilibili.com/video/BV15T411j7pJ/?spm_id_from333.337.search-card.all.click&vd_source9747207be61edfe4ec62226fc79b3589 官方文档&#xff1a; https://cn.vuejs.org/ 版本之间差异在关于---》版本发布 https://cn.vuejs.org/about/release…

Matter 设备配网流程 ---- 配网材料和 SPAKE2P 机制

Matter 设备配网流程 ---- 配网材料和 SPAKE2P 机制 1. Matter 配网材料 Matter 配网&#xff08;commissioning&#xff09;使用 SPAKE2P 协议完成 PASE&#xff0c;进而验证 DAC&#xff08;Device Attestation Credentials&#xff09;&#xff0c;派发 NOC&#xff0c;然…

MySQL的日志undolog、binlog、redolog

1. 日志层次 binlog是Server层&#xff0c;undolog和redolog是innodb引擎层特有的。 2. 记录了什么 & 作用 binlog 记录了所有数据库结构变更和表数据修改的SQL日志。 主要用于数据备份和主从复制&#xff0c;比如误删数据了可以用binlog找回。 undolog 如下图&#…

计算机网络-笔记-第五章-运输层

目录 五、第五章——运输层 1、运输层概述 2、运输层端口号、复用、分用 &#xff08;1&#xff09;熟知端口号、登记端口号、短暂端口号 &#xff08;2&#xff09;熟知端口号 &#xff08;3&#xff09;发送方复用、接收方分用 3、UDP与TCP对比 &#xff08;1&#x…

什么是响应式图片?如何在网页中实现响应式图片?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 响应式图片&#xff08;Responsive Images&#xff09;⭐ 实现响应式图片的方法1. 使用<img>标签的srcset属性2. 使用<picture>元素3. 使用CSS的max-width属性4. 使用响应式图片库 ⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&…

【AI】数学基础——高数(函数微分部分)

参考&#xff1a;https://www.bilibili.com/video/BV1mM411r7ko?p1&vd_source260d5bbbf395fd4a9b3e978c7abde437 唐宇迪&#xff1a;机器学习数学基础 文章目录 1.1 函数1.1.1 函数分类1.1.2 常见函数指/对数函数分段函数原函数&反函数sigmod函数Relu函数(非负函数)复…

nlp系列(7)三元组识别(Bert+CRF)pytorch

模型介绍 在实体识别中&#xff1a;使用了Bert模型&#xff0c;CRF模型 在关系识别中&#xff1a;使用了Bert模型的输出与实体掩码&#xff0c;进行一系列变化&#xff0c;得到关系 Bert模型介绍可以查看这篇文章&#xff1a;nlp系列&#xff08;2&#xff09;文本分类&…

windows环境下QuestaSim软件的使用

文章目录 前言一、QuestaSim使用方法1、编译vlog2、映射vmap3、仿真vism4、ifndef和define&#xff08;常用&#xff09;5、QuestaSim的仿真界面6、完整QuestaSim仿真——TCL脚本 前言 2023.8.29 一、QuestaSim使用方法 1、编译vlog vlog&#xff1a;questasim的编译命令 -s…

开始MySQL之路——MySQL 事务(详解分析)

MySQL 事务概述 MySQL 事务主要用于处理操作量大&#xff0c;复杂度高的数据。比如说&#xff0c;在人员管理系统中&#xff0c;你删除一个人员&#xff0c;你即需要删除人员的基本资料&#xff0c;也要删除和该人员相关的信息&#xff0c;如信箱&#xff0c;文章等等&#xf…