Android 13 VSYNC重学习
Android 13 VSYNC重学习
引言
学无止境,一个字干就完事!
源码参考基于Android 13 aosp!
一. Android VSync模块开胃菜
在开始正式的分析之前,我们先简单对Android的Vsync模块简单介绍下,如下图所示,其中:
- HW_VSync是由屏幕产生的脉冲信号,用于控制屏幕的刷新
- VSync-app和VSync-sf统称为软件VSync,它们是由SurfaceFlinger通过模拟硬件VSync而产生的VSync信号量,再分发给app和sf用来控制它们的合成节奏


二. Android VSync小结
这里有几点需要补充:
-
VSync-sf是没有对应的EventThread和DispSyncSource
-
VSync-app和VSync-appSf各自都有对应的EventThread和DispSyncSource
-
VSync-sf和VSync-app以及Sync-appSf通过Scheduler的成员mVsyncSchedule指向的VSyncDispatchTimerQueue实例对象关联
Android下VSync设计,牵涉的核心关系图如下:

2.1 VSync信号的分类
VSync信号分为两种:硬件VSync信号HW-VSync和软件VSync信号SW-VSync。SW-VSync信号由SW-VSync模型产生。HW-VSync信号负责对SW-VSync模型进行校准。
2.2 HW-Vsync信号的开启
三种场景下会开启硬件VSync信号HW-VSync会对软件VSync信号SW-VSync进行校准
-
SurfaceFlinger初始化。
-
连续两次请求VSync-app信号的时间间隔超过750ms。
-
SurfaceFlinger合成后,添加FenceTime到VSyncTracker中导致模型计算误差过大。
2.3 SW-VSync模型与计算
谷歌官方采用一元线性回归分析预测法(最小二乘法),通过采样的HW-VSync信号样本(屏幕刷新率),计算对应的SW-VSync信号周期。最终得到一条y=bx+a的拟合曲线。其中,b称为回归系数,a称为截距。SW-VSync模型就是这这条曲线的回归系数和截距。
2.4 SW-VSync信号的分类
SW-VSync信号也分为两种,VSync-sf信号和Vsync-app信号。这两个信号,各司其职:
- VSync-sf信号用于控制SurfaceFlinger的Layer合成
- VSync-app信号用于控制App渲染UI
VSync-sf信号和VSync-app信号是在SW-VSync信号的基础上通过叠加不同的偏移量产生,这些偏移量被称为VSync相位偏移。由于偏移量不同VSync-sf信号和VSync-app信号的回调时机也不同。
三. VSync-sf的申请和分发
VSync-sf用于控制SurfaceFlinger合成和渲染一帧图像。当SurfaceFlinger上帧时(BufferQueue中有新的GraphicBuffer),SurfaceFlinger会触发MessageQueue的scheduleFrame方法。接下来我们看下,VSync-sf是如何完成从申请到分发的流程。
3.1 VSync-sf的申请
SurfaceFlinger::scheduleCommit(...)//请求上帧mScheduler->scheduleFrame()//MessageQueue.cppmVsync.registration->schedule()//这里的registration实现是VSyncCallbackRegistration,定义在Scheduler/VSyncDispatchTimerQueue.cppmDispatch.get().schedule()//这里的mDispatch指向VSyncDispatchTimerQueue对象/*** @brief * * @param token * @param scheduleTiming * @return ScheduleResult * 1)根据CallbackToken找到所有满足要求的VSyncDispatchTimerQueueEntry。VSyncDispatchTimerQueueEntry是VSyncDispatchTimerQueue中对外部VSync信号请求的封装。* 2)遍历调用VSyncDispatchTimerQueue的schedule方法,计算下一次VSync信号的发送时间。* 3)对发射时间进行定时,等待下一次VSync信号的发送*/
ScheduleResult VSyncDispatchTimerQueue::schedule(CallbackToken token,ScheduleTiming scheduleTiming) {...//根据CallbackToken找到所有满足要求的VSyncDispatchTimerQueueEntry。VSyncDispatchTimerQueueEntry是VSyncDispatchTimerQueue中对外部VSync信号请求的封装。auto it = mCallbacks.find(token);auto& callback = it->second;//遍历调用VSyncDispatchTimerQueue的schedule方法,计算下一次VSync信号的发送时间result = callback->schedule(scheduleTiming, mTracker, now);//对发射时间进行定时,等待下一次VSync-sf信号的发送rearmTimerSkippingUpdateFor(now, it);VSyncDispatchTimerQueue::setTimer()void VSyncDispatchTimerQueue::setTimer(nsecs_t targetTime, nsecs_t /*now*/) {mIntendedWakeupTime = targetTime;mTimeKeeper->alarmAt(std::bind(&VSyncDispatchTimerQueue::timerCallback, this),mIntendedWakeupTime);mLastTimerSchedule = mTimeKeeper->now();
} /*** @brief * 1)遍历CallbackMap找到达到唤醒时间的VSyncDispatchTimerQueueEntry,并封装成Invocation,加入Invocation列表。* 2)遍历Invocation列表,通过Invocation获取VSyncDispatchTimerQueueEntry,并调用VSyncDispatchTimerQueueEntry的callback方法分发VSync信号。*///Scheduler/VSyncDispatchTimerQueue.cpp
void VSyncDispatchTimerQueue::timerCallback() {struct Invocation {std::shared_ptr<VSyncDispatchTimerQueueEntry> callback;nsecs_t vsyncTimestamp;nsecs_t wakeupTimestamp;nsecs_t deadlineTimestamp;};std::vector<Invocation> invocations;{std::lock_guard lock(mMutex);auto const now = mTimeKeeper->now();mLastTimerCallback = now;for (auto it = mCallbacks.begin(); it != mCallbacks.end(); it++) {auto& callback = it->second;auto const wakeupTime = callback->wakeupTime();if (!wakeupTime) {continue;}auto const readyTime = callback->readyTime();auto const lagAllowance = std::max(now - mIntendedWakeupTime, static_cast<nsecs_t>(0));if (*wakeupTime < mIntendedWakeupTime + mTimerSlack + lagAllowance) {callback->executing();invocations.emplace_back(Invocation{callback, *callback->lastExecutedVsyncTarget(),*wakeupTime, *readyTime});}}mIntendedWakeupTime = kInvalidTime;rearmTimer(mTimeKeeper->now());}for (auto const& invocation : invocations) {invocation.callback->callback(invocation.vsyncTimestamp, invocation.wakeupTimestamp,invocation.deadlineTimestamp);}
}}
3.2 VSync-sf的分发
那么VSync-df的callback是怎么注册到VSyncDispatchTimerQueue的呢,这个我们看下:
SurfaceFlinger::initScheduler(...)mScheduler->initVsync(...)//实现在Scheduler/MessageQueue.cpp中mVsync.registration = std::make_unique<scheduler::VSyncCallbackRegistration>(dispatch,std::bind(&MessageQueue::vsyncCallback, this,std::placeholders::_1,std::placeholders::_2,std::placeholders::_3),"sf");//这里的dispatch指向VSyncDispatchTimerQueue//Scheduler/VSyncDispatchTimerQueue.cppVSyncCallbackRegistration::VSyncCallbackRegistration(VSyncDispatch& dispatch,VSyncDispatch::Callback callback,std::string callbackName): mDispatch(dispatch),mToken(dispatch.registerCallback(std::move(callback), std::move(callbackName))),mValidToken(true) {} VSyncDispatchTimerQueue::CallbackToken VSyncDispatchTimerQueue::registerCallback(Callback callback, std::string callbackName) {std::lock_guard lock(mMutex);return CallbackToken{//最终注册到了mCallbacks中mCallbacks.emplace(++mCallbackToken,std::make_shared<VSyncDispatchTimerQueueEntry>(std::move(callbackName),std::move(callback),mMinVsyncDistance)).first->first};
}
所以最后VSync-sf的分发会调用到MessageQueue::vsyncCallback中,我们看下它的实现:
//Scheduler/MessageQueue.cpp
MessageQueue::vsyncCallback(...)mHandler->dispatchFrame(vsyncId, vsyncTime)mQueue.mLooper->sendMessage(this, Message())//Handle的handleMessage接收前面发过来的消息
void MessageQueue::Handler::handleMessage(const Message&) {mFramePending.store(false);const nsecs_t frameTime = systemTime();auto& compositor = mQueue.mCompositor;//这里的compositor实现类是SurfaceFlingerif (!compositor.commit(frameTime, mVsyncId, mExpectedVsyncTime)) {return;}compositor.composite(frameTime, mVsyncId);compositor.sample();
}
四. VSync-app的申请和分发
在开始后续的章节编写前,我们先重点申明下:
VSync-app用于控制App的UI渲染
VSync-app用于控制App的UI渲染
VSync-app用于控制App的UI渲染
4.1 VSync-app的申请
当Choreographer通过FrameDisplayEventReceiver调用scheduleVsync方法时,会触发VSync-app信号的申请。在FrameDisplayEventReceiver的scheduleVsync方法中,会调用nativeScheduleVsync方法。

FrameDisplayEventReceiver的nativeScheduleVsync方法对应的native实现为android_view_DisplayEventReceiver的nativeScheduleVsync函数。
在nativeScheduleVsync函数中,主要做了两件事:
-
获取native层的DisplayEventDispatcher。
-
调用DisplayEventDispatcher的scheduleVsync方法,请求VSync信号。

在DisplayEventDispatcher的scheduleVsync方法中,会调用DisplayEventReceiver的requestNextVsync方法。

在DisplayEventReceiver的requestNextVsync方法中,会调用IDisplayEventConnection的requestNextVsync方法。

IDisplayEventConnection是一个Binder类,对应bn端的实现类为BnDisplayEventConnection。而EventThreadConnection继承自BnDisplayEventConnection,因此实际调用的是EventThreadConnection的requestNextVsync方法。

在EventThreadConnection的requestNextVsync方法中,会调用EventThread的requestNextVsync方法。

在EventThread的requestNextVsync方法中,主要做了三件事:
-
开启硬件VSync信号对软件VSync信号进行校准。
-
标记EventThreadConnection的vsyncRequest,为后续信号分发做准备。
-
唤起EventThread对应的线程继续执行VSync信号的分发。

//Scheduler/EventThread.cpp
void EventThread::requestNextVsync(const sp<EventThreadConnection>& connection) {if (connection->resyncCallback) {/*** @brief * 调用到Scheduler::resync* 开启硬件Vsync信号对软件Vsync信号进行校准*/connection->resyncCallback();}std::lock_guard<std::mutex> lock(mMutex);if (connection->vsyncRequest == VSyncRequest::None) {connection->vsyncRequest = VSyncRequest::Single;mCondition.notify_all();//唤起EventThread中的线程} else if (connection->vsyncRequest == VSyncRequest::SingleSuppressCallback) {connection->vsyncRequest = VSyncRequest::Single;}
}
在EventThread的threadMain中,会通过VSyncCallbackRegistration请求或取消VSync信号。
如果是请求VSync信号,会调用VSyncCallbackRegistration的schedule方法。在VSyncCallbackRegistration的schedule方法,会调用VSyncDispatch的schedule方法。

void EventThread::threadMain(std::unique_lock<std::mutex>& lock) {if (mState != nextState) {if (mState == State::VSync) {mVSyncSource->setVSyncEnabled(false);} else if (nextState == State::VSync) {mVSyncSource->setVSyncEnabled(true);}mState = nextState;}}
之后的流程与VSync-sf信号的申请流程相同。在VSyncDispatchTimerQueue的schedule方法中,会调用scheduleLocked方法。
在VSyncDispatchTimerQueue的scheduleLocked方法中,主要做了三件事:
-
根据CallbackToken找到所有满足要求的VSyncDispatchTimerQueueEntry。VSyncDispatchTimerQueueEntry是VSyncDispatchTimerQueue中对外部VSync信号请求的封装。
-
遍历调用VSyncDispatchTimerQueue的schedule方法,计算下一次VSync信号的发送时间。
-
对发射时间进行定时,等待下一次VSync信号的发送。

4.2 VSync-app的分发
当定时时间到达时,TimerKeeper会回调VSyncDispatchTimerQueue的timerCallback方法。
在VSyncDispatchTimerQueue的timerCallback方法方法中,主要做了两件事:
-
遍历CallbackMap找到达到唤醒时间的VSyncDispatchTimerQueueEntry,并封装成Invocation,加入Invocation列表。
-
遍历Invocation列表,通过Invocation获取VSyncDispatchTimerQueueEntry,并调用VSyncDispatchTimerQueueEntry的callback方法分发VSync信号。

在VSyncDispatchTimerQueueEntry的callback方法中,会调用类型为CallbackRepeater::callbackk,然后在该方法中接着调用mCallback(vsyncTime, wakeupTime, readyTime)方法,而这里的mCallback(指向DispSyncSource::onVsyncCallback,最后回调EventThread的onVSyncEvent方法。
对于上述的分发流程是不是还有点懵逼,我们反过来看看VSync-app分发的注册,其核心是DispSyncSource和EventThread以及VSyncDispatchTimerQueue的各种回调callback流程:
//Scheduler/VSyncDispatchTimerQueue.cpp
VSyncDispatchTimerQueue::CallbackToken VSyncDispatchTimerQueue::registerCallback(Callback callback, std::string callbackName) {std::lock_guard lock(mMutex);return CallbackToken{mCallbacks.emplace(++mCallbackToken,std::make_shared<VSyncDispatchTimerQueueEntry>(std::move(callbackName),std::move(callback),mMinVsyncDistance)).first->first};
}//Scheduler/VSyncDispatchTimerQueue.cpp
VSyncCallbackRegistration::VSyncCallbackRegistration(VSyncDispatch& dispatch,VSyncDispatch::Callback callback,std::string callbackName): mDispatch(dispatch),mToken(dispatch.registerCallback(std::move(callback), std::move(callbackName))),mValidToken(true) {}//Scheduler/DispSyncSource.cpp
class CallbackRepeater {
public:CallbackRepeater(VSyncDispatch& dispatch, VSyncDispatch::Callback cb, const char* name,std::chrono::nanoseconds workDuration, std::chrono::nanoseconds readyDuration,std::chrono::nanoseconds notBefore): mName(name),mCallback(cb),//VSyncCallbackRegistration mRegistration GUARDED_BY(mMutex);mRegistration(dispatch,std::bind(&CallbackRepeater::callback, this, std::placeholders::_1,std::placeholders::_2, std::placeholders::_3),mName),mStarted(false),mWorkDuration(workDuration),mReadyDuration(readyDuration),mLastCallTime(notBefore) {}~CallbackRepeater() {std::lock_guard lock(mMutex);mRegistration.cancel();}void start(std::chrono::nanoseconds workDuration, std::chrono::nanoseconds readyDuration) {std::lock_guard lock(mMutex);mStarted = true;mWorkDuration = workDuration;mReadyDuration = readyDuration;auto const scheduleResult = mRegistration.schedule({.workDuration = mWorkDuration.count(),.readyDuration = mReadyDuration.count(),.earliestVsync = mLastCallTime.count()});LOG_ALWAYS_FATAL_IF((!scheduleResult.has_value()), "Error scheduling callback");}void stop() {std::lock_guard lock(mMutex);LOG_ALWAYS_FATAL_IF(!mStarted, "DispSyncInterface misuse: callback already stopped");mStarted = false;mRegistration.cancel();}void dump(std::string& result) const {std::lock_guard lock(mMutex);const auto relativeLastCallTime =mLastCallTime - std::chrono::steady_clock::now().time_since_epoch();StringAppendF(&result, "\t%s: ", mName.c_str());StringAppendF(&result, "mWorkDuration=%.2f mReadyDuration=%.2f last vsync time ",mWorkDuration.count() / 1e6f, mReadyDuration.count() / 1e6f);StringAppendF(&result, "%.2fms relative to now (%s)\n", relativeLastCallTime.count() / 1e6f,mStarted ? "running" : "stopped");}private:void callback(nsecs_t vsyncTime, nsecs_t wakeupTime, nsecs_t readyTime) {{std::lock_guard lock(mMutex);mLastCallTime = std::chrono::nanoseconds(vsyncTime);}mCallback(vsyncTime, wakeupTime, readyTime);{std::lock_guard lock(mMutex);if (!mStarted) {return;}auto const scheduleResult =mRegistration.schedule({.workDuration = mWorkDuration.count(),.readyDuration = mReadyDuration.count(),.earliestVsync = vsyncTime});LOG_ALWAYS_FATAL_IF(!scheduleResult.has_value(), "Error rescheduling callback");}}const std::string mName;scheduler::VSyncDispatch::Callback mCallback;mutable std::mutex mMutex;VSyncCallbackRegistration mRegistration GUARDED_BY(mMutex);bool mStarted GUARDED_BY(mMutex) = false;std::chrono::nanoseconds mWorkDuration GUARDED_BY(mMutex) = 0ns;std::chrono::nanoseconds mReadyDuration GUARDED_BY(mMutex) = 0ns;std::chrono::nanoseconds mLastCallTime GUARDED_BY(mMutex) = 0ns;
};mAppConnectionHandle =mScheduler->createConnection("app" .....)Scheduler::createConnection()auto vsyncSource = makePrimaryDispSyncSource(connectionName, workDuration, readyDuration)return std::make_unique<scheduler::DispSyncSource>(mVsyncSchedule->getDispatch(),mVsyncSchedule->getTracker(), workDuration,readyDuration, traceVsync, name);//std::unique_ptr<CallbackRepeater> mCallbackRepeater;mCallbackRepeater =std::make_unique<CallbackRepeater>(vSyncDispatch,std::bind(&DispSyncSource::onVsyncCallback, this,std::placeholders::_1,std::placeholders::_2,std::placeholders::_3),name, workDuration, readyDuration,std::chrono::steady_clock::now().time_since_epoch()); mVSyncSource->setCallback(this);//为DispVsyncSource设置回调void DispSyncSource::setCallback(VSyncSource::Callback* callback) {std::lock_guard lock(mCallbackMutex);mCallback = callback;} //最终整理出来的Vsync-app分发流程为,各种弯弯绕绕:VSyncDispatchTimerQueue::timerCallback()//Scheduler/VSyncDispatchTimerQueue.cppinvocation.callback->callback(...)//这里的callback指向VSyncDispatchTimerQueueEntry::callback,Scheduler/VSyncDispatchTimerQueue.cppmCallback(vsyncTimestamp, wakeupTimestamp, deadlineTimestamp)//这里的 mCallback指向CallbackRepeater::callback,实现在Scheduler/DispSyncSource.cpp mCallback(vsyncTime, wakeupTime, readyTime)//这里的callback指向DispSyncSource::onVsyncCallback。是现在Scheduler/DispSyncSource.cppcallback = mCallback;callback->onVSyncEvent(targetWakeupTime, {vsyncTime, readyTime})//这里的callback指向EventThread::onVSyncEvent
在EventThread的onVSyncEvent方法中,主要做了三件事:
-
调用makeVSync函数,创建Event。
-
将Event加入到vector<DisplayEventReceiver::Event> 中。
-
唤醒等待线程,执行threadMain方法。

void EventThread::onVSyncEvent(nsecs_t timestamp, VSyncSource::VSyncData vsyncData) {std::lock_guard<std::mutex> lock(mMutex);LOG_FATAL_IF(!mVSyncState);//包装为DisplayEventReceiver::Event对象,存入mPendingEvents尾部mPendingEvents.push_back(makeVSync(mVSyncState->displayId, timestamp, ++mVSyncState->count,vsyncData.expectedPresentationTime,vsyncData.deadlineTimestamp));//唤醒线程mCondition.notify_all();
}
我们接下来看EventThread是如何处理分发事件的:
//Scheduler/EventThread.cpp
void EventThread::threadMain(std::unique_lock<std::mutex>& lock) {DisplayEventConsumers consumers;while (mState != State::Quit) {std::optional<DisplayEventReceiver::Event> event;// Determine next event to dispatch.if (!mPendingEvents.empty()) {event = mPendingEvents.front();mPendingEvents.pop_front(); ...}// Find connections that should consume this event.auto it = mDisplayEventConnections.begin();while (it != mDisplayEventConnections.end()) {if (const auto connection = it->promote()) {vsyncRequested |= connection->vsyncRequest != VSyncRequest::None;//用来在任务的循环执行中保存当前Vsync信号的消费者if (event && shouldConsumeEvent(*event, connection)) {consumers.push_back(connection);//这里的consumers就是待分发的目标}++it;} else {it = mDisplayEventConnections.erase(it);}} /*** @brief * 在该方法中,会循环分发信号,主要做了五件事情* 1) 从Vsync信息队列中获取消息* 2)收集监听Vsync信号的EventThreadConnection,并加入到consumers中* 3) 调用dispatchEvent方法来分发Vsync信号* 4)计算当前状态,根据状态请求或取消下一次VSync信号* 5)如果没有Vsync信号需要分发,线程进入等待状态*/if (!consumers.empty()) {dispatchEvent(*event, consumers);consumer->postEvent(copy)DisplayEventReceiver::sendEvents(...)consumers.clear();}
最终VSync-app分发的事件会被Choreographer模块接收,开始安排应用相关的渲染UI逻辑!
Andoid SurfaceFlinger(二) VSYNC的开始,连续,结束
VSYNC研究-最后的窗户纸
Android 12(S) 图像显示系统 - SurfaceFlinger之VSync-上篇(十六)
Android 12(S) 图像显示系统 - SurfaceFlinger 之 VSync - 中篇(十七)
深度详解 Android S(12.0)屏幕刷新机制之 Choreographer
View绘制流程3-Vsync信号是如何发送和接受的
Android R Vsync相关梳理
显示框架之深入Vsync原理
App/Sf的Vsync部分源码流程结合perfetto/systrace分析
Android-View绘制原理(02)-VSync原理之SurfaceFlinger篇
一文搞定Android VSync来龙机制去脉
VSync信号系统与SurfaceFlinger
SurfaceFlinger-Vsync信号
Android VSync事件分发过程源码分析
相关文章:
Android 13 VSYNC重学习
Android 13 VSYNC重学习 引言 学无止境,一个字干就完事! 源码参考基于Android 13 aosp! 一. Android VSync模块开胃菜 在开始正式的分析之前,我们先简单对Android的Vsync模块简单介绍下,如下图所示,其中: HW_VSync是…...
std::move和左值右值
引用:windows程序员面试指南 std::move std::move 是 C 标准库中的一个函数模板,用于将一个左值(左值引用)转化为右值引用,从而实现移动语义。 移动语义是一种可以将资源(如内存)从一个对象转…...
QT学习备份
2023年1月2日09:00:32 1.信号/槽编辑器 发送者:控件 信号:是控件发出的信号 接受者:包含控件的容器 槽:程序上用slot标识的方法 2.Q_OBJECT宏 只有继承了QObject类的类,才具有信号槽的能力。所以,为了使用…...
【wiki知识库】03.前后端的初步交互(展现所有的电子书)
📝个人主页:哈__ 期待您的关注 目录 一、🔥今日目标 二、📂前端配置文件补充 三、🌏前端Vue的改造 四、💡总结 一、🔥今日目标 在上一篇文章当中,我已带大家把后端的一些基本工…...
AOP——学习
AOP(面向切面编程)是Spring框架的重要特性之一,用于分离关注点并处理横切关注点,如日志记录、安全性和事务管理。在面试中,AOP相关的问题通常会涉及基本概念、应用场景、实际使用、以及与其他编程范式的比较。以下是一…...
Linux静态库、共享动态库介绍、制作及使用
参考学习:Linux下的各种文件 、动态库基本原理和使用方法,-fPIC选项的来龙去脉 、Linux静态库和动态库分析 文章写作参考:Linux共享库、静态库、动态库详解 - sunsky303 - 博客园 (cnblogs.com) 一.Linux共享库、静态库、动态库详解 使用G…...
【Paddle】稀疏计算的使用指南 稀疏ResNet的学习心得 (2) + Paddle3D应用实例稀疏 ResNet代码解读 (1.6w字超详细)
【Paddle】稀疏计算的使用指南 & 稀疏ResNet的学习心得 Paddle3D应用实例稀疏 ResNet代码解读 写在最前面一、稀疏格式简介1. COO(Coordinate Format)2. CSR(Compressed Sparse Row Format) 二、Paddle稀疏张量支持1. 创建 C…...
Linux系统维护
1. 批量安装部署 2. 初始化配置 3. 禁用Selinux 永久更改 SELinux 配置: 编辑 SELinux 配置文件:使用文本编辑器打开 /etc/selinux/config 文件: 在配置文件中,找到 SELINUX… 的行。将其值更改为以下选项之一: e…...
经典文献阅读之--RenderOcc(使用2D标签训练多视图3D Occupancy模型)
0. 简介 3D占据预测在机器人感知和自动驾驶领域具有重要的潜力,它将3D场景量化为带有语义标签的网格单元。最近的研究主要利用3D体素空间中的完整占据标签进行监督。然而,昂贵的注释过程和有时模糊的标签严重限制了3D占据模型的可用性和可扩展性。为了解…...
蓝牙设备中的UUID
文章目录 一、Device UUID二、Service UUID 一、Device UUID Device UUID也可以被称作为DeviceID。 Android 设备上扫描获取到的 deviceId 为外围设备的 MAC 地址,相对固定。iOS 设备上扫描获取到的 deviceId 是系统根据外围设备 MAC 地址及发现设备的时间生成的 …...
网络之再谈体系结构
大家都知道的是网络的体系结构,现代软件常用的体系结构无非是TCP/IP协议栈,OSI因为实现复杂并且效率没有TCP/IP协议栈好,所以不用OSI,但是,最近在复习网络知识的时候,发现了一些奇怪的地方,那就…...
在flutter initState 方法,触发 setState导致循环执行
在Flutter中,如果你在initState中调用了一个方法,并且这个方法可能导致状态更新,这可能会引起无限循环,因为每次状态更新都会再次调用initState。 为了避免这种情况,你应该检查调用的方法是否会导致状态更新ÿ…...
JavaScript字符串方法
charAt() 方法:返回指定索引处的字符。 示例: let str "Hello World"; console.log(str.charAt(0)); // Output: "H" console.log(str.charAt(6)); // Output: "W"charCodeAt() 方法:返回指定索引处字符的 Un…...
YD/T 2698-2014 《电信网和互联网安全防护基线配置要求及检测要求 网络设备》标准介绍
编写背景 随着互联网技术的飞速发展,网络设备的安全问题日益凸显。为了加强电信网和互联网的安全防护,保障网络环境的稳定和用户信息的安全,YD/T 2698-2014标准应运而生。此标准旨在为网络设备提供一套基线配置要求,以及相应的安…...
QCC30XX如何查找本地地址码
查找本地地址段/********************************************************************** Copyright (c) 2016 - 2017 Qualcomm Technologies International, Ltd. FILE NAME sink_private_data.c DESCRIPTION This module works as a container for all private and common…...
基于 DCT 的图像滤波
需求分析 对于图像去噪这一需求,我们可以通过DCT(离散余弦变换)算法来实现。DCT是一种基于频域的变换技术,可以将图像从空间域转换为频域,然后通过滤波等处理方式进行去噪。 针对这一需求,我们需要进行以下…...
spdlog日志库源码:自定义异常类spdlog_ex
自定义异常类spdlog_ex 标准库异常类(std::exception)系列,能满足大多数使用异常的场景,但对系统调用异常及错误信息缺乏支持。spdlog通过继承std::exception,扩展对系统调用的支持,实现自定义异常类spdlo…...
3.每日LeetCode-数组类,爬楼梯(Go,Java,Python)
目录 题目 解法 Go Java Python 代码地址:leetcode: 每日leetcode刷题 题目 题号70. 爬楼梯 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? 示例 1: 输入ÿ…...
单节点11.2.0.3参数文件恢复到RAC11.2.0.4启动失败
问题描述 通过pfile生成spfile失败,提示DATA磁盘不存在 SQL> create spfileDATA/DXJ/spfiledxj.ora from pfile/home/oracle/initdxj20240529.ora; create spfileDATA/DXJ/spfiledxj.ora from pfile/home/oracle/initdxj20240529.ora * ERROR at line 1: ORA-1…...
Windows电脑高颜值桌面便利贴,便签怎么设置
在这个看颜值的时代,我们不仅在衣着打扮上追求时尚与美观,就连电脑桌面也不愿放过。一张唯美的壁纸,几款别致的小工具,总能让我们的工作空间焕发出不一样的光彩。如果你也热衷于打造高颜值的电脑桌面,那么,…...
Java如何权衡是使用无序的数组还是有序的数组
在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...
智能在线客服平台:数字化时代企业连接用户的 AI 中枢
随着互联网技术的飞速发展,消费者期望能够随时随地与企业进行交流。在线客服平台作为连接企业与客户的重要桥梁,不仅优化了客户体验,还提升了企业的服务效率和市场竞争力。本文将探讨在线客服平台的重要性、技术进展、实际应用,并…...
涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...
R语言速释制剂QBD解决方案之三
本文是《Quality by Design for ANDAs: An Example for Immediate-Release Dosage Forms》第一个处方的R语言解决方案。 第一个处方研究评估原料药粒径分布、MCC/Lactose比例、崩解剂用量对制剂CQAs的影响。 第二处方研究用于理解颗粒外加硬脂酸镁和滑石粉对片剂质量和可生产…...
jmeter聚合报告中参数详解
sample、average、min、max、90%line、95%line,99%line、Error错误率、吞吐量Thoughput、KB/sec每秒传输的数据量 sample(样本数) 表示测试中发送的请求数量,即测试执行了多少次请求。 单位,以个或者次数表示。 示例:…...
(一)单例模式
一、前言 单例模式属于六大创建型模式,即在软件设计过程中,主要关注创建对象的结果,并不关心创建对象的过程及细节。创建型设计模式将类对象的实例化过程进行抽象化接口设计,从而隐藏了类对象的实例是如何被创建的,封装了软件系统使用的具体对象类型。 六大创建型模式包括…...
Golang——7、包与接口详解
包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...
深入理解Optional:处理空指针异常
1. 使用Optional处理可能为空的集合 在Java开发中,集合判空是一个常见但容易出错的场景。传统方式虽然可行,但存在一些潜在问题: // 传统判空方式 if (!CollectionUtils.isEmpty(userInfoList)) {for (UserInfo userInfo : userInfoList) {…...
LCTF液晶可调谐滤波器在多光谱相机捕捉无人机目标检测中的作用
中达瑞和自2005年成立以来,一直在光谱成像领域深度钻研和发展,始终致力于研发高性能、高可靠性的光谱成像相机,为科研院校提供更优的产品和服务。在《低空背景下无人机目标的光谱特征研究及目标检测应用》这篇论文中提到中达瑞和 LCTF 作为多…...
