Android11 InputDispatcher 分发事件流程分析

news2024/11/17 9:53:19

在 Android11 InputReader分析 一文中分析到,InputReader将数据放入iq队列后,唤醒InputDispatcher线程,执行InputDispatcher的dispatchOnce方法

//frameworks\native\services\inputflinger\dispatcher\InputDispatcher.cpp
void InputDispatcher::dispatchOnce() {
    nsecs_t nextWakeupTime = LONG_LONG_MAX;
    { // acquire lock
        std::scoped_lock _l(mLock);
        mDispatcherIsAlive.notify_all();

        // Run a dispatch loop if there are no pending commands.
        // The dispatch loop might enqueue commands to run afterwards.
        if (!haveCommandsLocked()) {//此时mCommandQueue中没有命令
            dispatchOnceInnerLocked(&nextWakeupTime);//1
        }

        // Run all pending commands if there are any.
        // If any commands were run then force the next poll to wake up immediately.
        if (runCommandsLockedInterruptible()) {//执行mCommandQueue中的命令
            nextWakeupTime = LONG_LONG_MIN;
        }

      //省略
}

继续调用注释1处的dispatchOnceInnerLocked进行处理

//frameworks\native\services\inputflinger\dispatcher\InputDispatcher.cpp
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
	//省略
	if (!mPendingEvent) {
		//省略
	}else {
		// Inbound queue has at least one entry.
  		mPendingEvent = mInboundQueue.front();//1
		mInboundQueue.pop_front();
		traceInboundQueueLengthLocked();
    }
    // Poke user activity for this event.
  	if (mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER) {
		pokeUserActivityLocked(*mPendingEvent);//这个方法会向mCommandQueue中放入命令,后面在dispatchOnce中,调用	runCommandsLockedInterruptible执行这个命令,通过JNI调用,调用PowerManagerService的userActivityFromNative方法	
	}
	
	ALOG_ASSERT(mPendingEvent != nullptr);
    bool done = false;
    DropReason dropReason = DropReason::NOT_DROPPED;//标记事件是否需要抛弃掉,不传给应用窗口
    if (!(mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER)) {
        dropReason = DropReason::POLICY;
    } else if (!mDispatchEnabled) {
        dropReason = DropReason::DISABLED;
    }

    if (mNextUnblockedEvent == mPendingEvent) {
        mNextUnblockedEvent = nullptr;
    }

    switch (mPendingEvent->type) {
		//省略
		case EventEntry::Type::MOTION: {
            MotionEntry* typedEntry = static_cast<MotionEntry*>(mPendingEvent);
            if (dropReason == DropReason::NOT_DROPPED && isAppSwitchDue) {
                dropReason = DropReason::APP_SWITCH;
            }
            if (dropReason == DropReason::NOT_DROPPED && isStaleEvent(currentTime, *typedEntry)) {
                dropReason = DropReason::STALE;
            }
            if (dropReason == DropReason::NOT_DROPPED && mNextUnblockedEvent) {
                dropReason = DropReason::BLOCKED;
            }
            done = dispatchMotionLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);//2
            break;
        }
	//省略
|

注释1处,先从iq队列中取出事件,对于触摸事件,调用注释2处的dispatchMotionLocked继续处理

//frameworks\native\services\inputflinger\dispatcher\InputDispatcher.cpp
bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, MotionEntry* entry,
                                           DropReason* dropReason, nsecs_t* nextWakeupTime) {
	//省略
	bool isPointerEvent = entry->source & AINPUT_SOURCE_CLASS_POINTER;
	if (isPointerEvent) {
        // Pointer event.  (eg. touchscreen)
        injectionResult =findTouchedWindowTargetsLocked(currentTime, *entry, inputTargets, nextWakeupTime,
                                               &conflictingPointerActions);//1
    }
    
	//省略
	
	dispatchEventLocked(currentTime, entry, inputTargets);//2
    return true;

}

注释1处,InputDispatcher需要知道,输入事件应该派发给哪个窗口,所以需要找到目标窗口,将其放入inputTargets中,这个流程在文章的后面再分析。注释2处,查找到目标窗口后,调用dispatchEventLocked继续处理

//frameworks\native\services\inputflinger\dispatcher\InputDispatcher.cpp
void InputDispatcher::dispatchEventLocked(nsecs_t currentTime, EventEntry* eventEntry,
                                          const std::vector<InputTarget>& inputTargets) {

    pokeUserActivityLocked(*eventEntry);

    for (const InputTarget& inputTarget : inputTargets) {//遍历
        sp<Connection> connection =
                getConnectionLocked(inputTarget.inputChannel->getConnectionToken());//根据token取出connection 
        if (connection != nullptr) {
            prepareDispatchCycleLocked(currentTime, connection, eventEntry, inputTarget);//继续处理
        } else {
            if (DEBUG_FOCUS) {
                ALOGD("Dropping event delivery to target with channel '%s' because it "
                      "is no longer registered with the input dispatcher.",
                      inputTarget.inputChannel->getName().c_str());
            }
        }
    }
}

遍历inputTargets查找到对应的connection ,然后使用prepareDispatchCycleLocked继续处理,在prepareDispatchCycleLocked方法中,对于支持触摸事件分离的窗口,则是直接调用enqueueDispatchEntriesLocked来处理

//frameworks\native\services\inputflinger\dispatcher\InputDispatcher.cpp
void InputDispatcher::enqueueDispatchEntriesLocked(nsecs_t currentTime,
                                                   const sp<Connection>& connection,
                                                   EventEntry* eventEntry,
 
    bool wasEmpty = connection->outboundQueue.empty();

    // Enqueue dispatch entries for the requested modes.
    enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
                               InputTarget::FLAG_DISPATCH_AS_HOVER_EXIT);
    enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
                               InputTarget::FLAG_DISPATCH_AS_OUTSIDE);
    enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
                               InputTarget::FLAG_DISPATCH_AS_HOVER_ENTER);
    enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
                               InputTarget::FLAG_DISPATCH_AS_IS);//放入队列
    enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
                               InputTarget::FLAG_DISPATCH_AS_SLIPPERY_EXIT);
    enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
                               InputTarget::FLAG_DISPATCH_AS_SLIPPERY_ENTER);

    // If the outbound queue was previously empty, start the dispatch cycle going.
    if (wasEmpty && !connection->outboundQueue.empty()) {
        startDispatchCycleLocked(currentTime, connection);//分发
    }
}

首先是将事件放入oq队列,然后调用startDispatchCycleLocked继续处理
放入队列:

//frameworks\native\services\inputflinger\dispatcher\InputDispatcher.cpp
void InputDispatcher::enqueueDispatchEntryLocked(const sp<Connection>& connection,
                                                 EventEntry* eventEntry,
                                                 const InputTarget& inputTarget,
                                                 int32_t dispatchMode) {
	//省略
	// Enqueue the dispatch entry.
    connection->outboundQueue.push_back(dispatchEntry.release());//放入队列
    traceOutboundQueueLength(connection);//这里就是能在trace中看到oq的原因

}

startDispatchCycleLocked

//frameworks\native\services\inputflinger\dispatcher\InputDispatcher.cpp
void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,
                                               const sp<Connection>& connection) {
    //省略
	while (connection->status == Connection::STATUS_NORMAL && !connection->outboundQueue.empty()) {
        DispatchEntry* dispatchEntry = connection->outboundQueue.front();//从oq中取出事件
        dispatchEntry->deliveryTime = currentTime;
        const nsecs_t timeout =
                getDispatchingTimeoutLocked(connection->inputChannel->getConnectionToken());
        dispatchEntry->timeoutTime = currentTime + timeout;

        // Publish the event.
        status_t status;
        EventEntry* eventEntry = dispatchEntry->eventEntry;
		switch (eventEntry->type) {
			//省略
			case EventEntry::Type::MOTION: {

			 // Publish the motion event.
            status = connection->inputPublisher
                                 .publishMotionEvent(dispatchEntry->seq,
                                                     dispatchEntry->resolvedEventId,
                                                     motionEntry->deviceId, motionEntry->source,
                                                     motionEntry->displayId, std::move(hmac),
                                                     dispatchEntry->resolvedAction,
                                                     motionEntry->actionButton,
                                                     dispatchEntry->resolvedFlags,
                                                     motionEntry->edgeFlags, motionEntry->metaState,
                                                     motionEntry->buttonState,
                                                     motionEntry->classification, xScale, yScale,
                                                     xOffset, yOffset, motionEntry->xPrecision,
                                                     motionEntry->yPrecision,
                                                     motionEntry->xCursorPosition,
                                                     motionEntry->yCursorPosition,
                                                     motionEntry->downTime, motionEntry->eventTime,
                                                     motionEntry->pointerCount,
                                                     motionEntry->pointerProperties, usingCoords);//1
                reportTouchEventForStatistics(*motionEntry);
                break;
            }
        //省略
        // Re-enqueue the event on the wait queue.
        connection->outboundQueue.erase(std::remove(connection->outboundQueue.begin(),
                                                    connection->outboundQueue.end(),
                                                    dispatchEntry));
        traceOutboundQueueLength(connection);
        connection->waitQueue.push_back(dispatchEntry);//放入wq队列
        
        traceWaitQueueLength(connection);//这就是trace中可以看到wq的原因
    }
}

可以看出,该方法主要是从oq队列中取出事件,然后调用connection->inputPublisher的publishMotionEvent方法继续处理,处理完成后,将事件从oq队列中删除,并又添加到wq队列中。
connection的inputPublisher成员指向的是inputChannel,在Android 11 输入系统之InputDispatcher和应用窗口建立联系一文中分析到,使用inputChannel构造connection对象

//frameworks\native\services\inputflinger\dispatcher\Connection.cpp
Connection::Connection(const sp<InputChannel>& inputChannel, bool monitor,
                       const IdGenerator& idGenerator)
      : status(STATUS_NORMAL),
        inputChannel(inputChannel),
        monitor(monitor),
        inputPublisher(inputChannel),//初始化inputPublisher
        inputState(idGenerator) {}

继续来看InputChannel的publishMotionEvent方法

//frameworks\native\libs\input\InputTransport.cpp
status_t InputPublisher::publishMotionEvent(
        uint32_t seq, int32_t eventId, int32_t deviceId, int32_t source, int32_t displayId,
        std::array<uint8_t, 32> hmac, int32_t action, int32_t actionButton, int32_t flags,
        int32_t edgeFlags, int32_t metaState, int32_t buttonState,
        MotionClassification classification, float xScale, float yScale, float xOffset,
        float yOffset, float xPrecision, float yPrecision, float xCursorPosition,
        float yCursorPosition, nsecs_t downTime, nsecs_t eventTime, uint32_t pointerCount,
        const PointerProperties* pointerProperties, const PointerCoords* pointerCoords) {
   	//省略

    InputMessage msg;
    msg.header.type = InputMessage::Type::MOTION;
    msg.body.motion.seq = seq;
    msg.body.motion.eventId = eventId;
    msg.body.motion.deviceId = deviceId;
    msg.body.motion.source = source;
    msg.body.motion.displayId = displayId;
    msg.body.motion.hmac = std::move(hmac);
    msg.body.motion.action = action;
    msg.body.motion.actionButton = actionButton;
    msg.body.motion.flags = flags;
    msg.body.motion.edgeFlags = edgeFlags;
    msg.body.motion.metaState = metaState;
    msg.body.motion.buttonState = buttonState;
    msg.body.motion.classification = classification;
    msg.body.motion.xScale = xScale;
    msg.body.motion.yScale = yScale;
    msg.body.motion.xOffset = xOffset;
    msg.body.motion.yOffset = yOffset;
    msg.body.motion.xPrecision = xPrecision;
    msg.body.motion.yPrecision = yPrecision;
    msg.body.motion.xCursorPosition = xCursorPosition;
    msg.body.motion.yCursorPosition = yCursorPosition;
    msg.body.motion.downTime = downTime;
    msg.body.motion.eventTime = eventTime;
    msg.body.motion.pointerCount = pointerCount;
    for (uint32_t i = 0; i < pointerCount; i++) {
        msg.body.motion.pointers[i].properties.copyFrom(pointerProperties[i]);
        msg.body.motion.pointers[i].coords.copyFrom(pointerCoords[i]);
    }

    return mChannel->sendMessage(&msg);
}

构建InputMessage ,然后调用其sendMessage方法,将数据发送出去

//frameworks\native\libs\input\InputTransport.cpp
status_t InputChannel::sendMessage(const InputMessage* msg) {
    const size_t msgLength = msg->size();
    InputMessage cleanMsg;
    msg->getSanitizedCopy(&cleanMsg);
    ssize_t nWrite;
    do {
        nWrite = ::send(mFd.get(), &cleanMsg, msgLength, MSG_DONTWAIT | MSG_NOSIGNAL);//1
    } while (nWrite == -1 && errno == EINTR);

//省略

注释1处,向fd中写入数据,InputDispatcher就将数据分发出去了。

查找目标窗口的过程

前面提到,InputDispatcher通过findTouchedWindowTargetsLocked方法,来查找到目标窗口,从而决定事件应该分发给谁

//frameworks\native\services\inputflinger\dispatcher\InputDispatcher.cpp
int32_t InputDispatcher::findTouchedWindowTargetsLocked(nsecs_t currentTime,
                                                        const MotionEntry& entry,
                                                        std::vector<InputTarget>& inputTargets,
                                                        nsecs_t* nextWakeupTime,
                                                        bool* outConflictingPointerActions) {
	//省略
	bool newGesture = (maskedAction == AMOTION_EVENT_ACTION_DOWN ||
                       maskedAction == AMOTION_EVENT_ACTION_SCROLL || isHoverAction);//有新手指触摸
    const bool isFromMouse = entry.source == AINPUT_SOURCE_MOUSE;
    bool wrongDevice = false;
    if (newGesture) {
        //省略
        tempTouchState.reset();//先清空tempTouchState,然后重新赋值
        tempTouchState.down = down;
        tempTouchState.deviceId = entry.deviceId;
        tempTouchState.source = entry.source;
        tempTouchState.displayId = displayId;
        isSplit = false;
     }
	//省略
	if (newGesture || (isSplit && maskedAction == AMOTION_EVENT_ACTION_POINTER_DOWN)) {
		//省略
		sp<InputWindowHandle> newTouchedWindowHandle =
                findTouchedWindowAtLocked(displayId, x, y, &tempTouchState,
                                          isDown /*addOutsideTargets*/, true /*addPortalWindows*/);//1
		//省略
		if (newTouchedWindowHandle != nullptr) {
            // Set target flags.
            int32_t targetFlags = InputTarget::FLAG_FOREGROUND | InputTarget::FLAG_DISPATCH_AS_IS;
            if (isSplit) {
                targetFlags |= InputTarget::FLAG_SPLIT;
            }
            if (isWindowObscuredAtPointLocked(newTouchedWindowHandle, x, y)) {
                targetFlags |= InputTarget::FLAG_WINDOW_IS_OBSCURED;
            } else if (isWindowObscuredLocked(newTouchedWindowHandle)) {
                targetFlags |= InputTarget::FLAG_WINDOW_IS_PARTIALLY_OBSCURED;
            }

  			//省略
            tempTouchState.addOrUpdateWindow(newTouchedWindowHandle, targetFlags, pointerIds);//2
        }
		
		//省略
		for (const TouchedWindow& touchedWindow : tempTouchState.windows) {//3
        	addWindowTargetLocked(touchedWindow.windowHandle, touchedWindow.targetFlags,
                              touchedWindow.pointerIds, inputTargets);
    	}
	}
}

注释1处通过findTouchedWindowAtLocked方法查找到InputWindowHandle,注释2处将改InputWindowHandle添加到TouchedWindow中,并添加到tempTouchState的windows集合。上面省略了部分代码,还有其他的满足条件的InputWindowHandle,也会添加进来。注释3处遍历tempTouchState的windows,取出其中的TouchedWindow调用addWindowTargetLocked,来填充inputTargets集合。

先来看看findTouchedWindowAtLocked方法是怎么查找到符合条件的InputWindowHandle

//frameworks\native\services\inputflinger\dispatcher\InputDispatcher.cpp
sp<InputWindowHandle> InputDispatcher::findTouchedWindowAtLocked(int32_t displayId, int32_t x,
                                                                 int32_t y, TouchState* touchState,
                                                                 bool addOutsideTargets,
                                                                 bool addPortalWindows) {
    //省略
    // Traverse windows from front to back to find touched window.
    const std::vector<sp<InputWindowHandle>> windowHandles = getWindowHandlesLocked(displayId);//1
    for (const sp<InputWindowHandle>& windowHandle : windowHandles) {//2
        const InputWindowInfo* windowInfo = windowHandle->getInfo();
        if (windowInfo->displayId == displayId) {
            int32_t flags = windowInfo->layoutParamsFlags;

            if (windowInfo->visible) {
                if (!(flags & InputWindowInfo::FLAG_NOT_TOUCHABLE)) {
                    bool isTouchModal = (flags &
                                         (InputWindowInfo::FLAG_NOT_FOCUSABLE |
                                          InputWindowInfo::FLAG_NOT_TOUCH_MODAL)) == 0;
                    if (isTouchModal || windowInfo->touchableRegionContainsPoint(x, y)) {
                        int32_t portalToDisplayId = windowInfo->portalToDisplayId;
                        //省略
                        return windowHandle;
         //省略
                                             

注释1处返回的InputWindowHandle集合是根据displayID从mWindowHandlesByDisplay中取出来的,mWindowHandlesByDisplay中的元素是SurfaceFlinger中,调用 updateInputWindowInfo方法,最后调用到InputDispatcher的updateWindowHandlesForDisplayLocked添加的。
注释2处遍历前面得到的InputWindowHandle集合,判断触摸的区域是否是在该InputWindowHandle,如果是,则返回该InputWindowHandle。

查找到InputWindowHandle后,就会将其加入到tempTouchState的windows集合中

//frameworks\native\services\inputflinger\dispatcher\TouchState.cpp
void TouchState::addOrUpdateWindow(const sp<InputWindowHandle>& windowHandle, int32_t targetFlags,
                                   BitSet32 pointerIds) {
    //省略
    TouchedWindow touchedWindow;//构建TouchedWindow
    touchedWindow.windowHandle = windowHandle;
    touchedWindow.targetFlags = targetFlags;
    touchedWindow.pointerIds = pointerIds;
    windows.push_back(touchedWindow);//添加进windows集合
}

最后就是遍历windows集合,根据InputWindowHandle信息来填充inputTargets集合了

//frameworks\native\services\inputflinger\dispatcher\InputDispatcher.cpp
void InputDispatcher::addWindowTargetLocked(const sp<InputWindowHandle>& windowHandle,
                                            int32_t targetFlags, BitSet32 pointerIds,
                                            std::vector<InputTarget>& inputTargets) {
   //省略

    if (it == inputTargets.end()) {
        InputTarget inputTarget;
        sp<InputChannel> inputChannel = getInputChannelLocked(windowHandle->getToken());//取出inputChannel 
        inputTarget.inputChannel = inputChannel;
        inputTarget.flags = targetFlags;
        inputTarget.globalScaleFactor = windowInfo->globalScaleFactor;
        inputTargets.push_back(inputTarget);//添加进集合
        it = inputTargets.end() - 1;
    }
	//省略
}

首先是根据windowHandle中的token,取出对应的inputChanel,然后根据inputChanel构造inputTarget并放入集合 中。这样inputTargets集合中就包含了一个个的inputTarget,而inputTarget就包含了窗口对应的inputChannel 信息。

总结

InputDispatcher所做的工作就是从iq队列中取出数据,然后找到目标窗口,进而找到目标窗口对应的connect,将数据放入connection的oq队列,后面取出oq队列的数据并将其通过fd发送出去。分发完成后,将数据移至wq队列。
流程图如下
在这里插入图片描述

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

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

相关文章

软件测试之 接口测试 Postman使用

接口测试 URL HTTP协议 HTTP 请求部分 HTTP响应部分 Postman使用 界面介绍 这里 注意 如果你无法访问 那么 captchaImage这个打错了&#xff0c;给的资料中是错误的地址 https://kdtx-test.itheima.net/api/captchaImage登录接口 科大天下 第一个接口的登录设置 https://kd…

【生信技能树】数据挖掘全流程

R包的安装&#xff0c;每次做分析的时候先运行这段代码把R包都安装好了&#xff0c;这段代码不需要任何改动&#xff0c;每次分析直接运行。 options("repos""https://mirrors.ustc.edu.cn/CRAN/") if(!require("BiocManager")) install.packag…

【CCF-CSP】202403-4 十滴水

题目描述 十滴水是一个非常经典的小游戏。 小 C 正在玩一个一维版本的十滴水游戏。我们通过一个例子描述游戏的基本规则。 游戏在一个 1c 的网格上进行&#xff0c;格子用整数x(1≤x≤c) 编号&#xff0c;编号从左往右依次递增。网格内 m 个格子里有 1∼41∼4 滴水&#xff0…

【教程向】从零开始创建浏览器插件(四)探索Chrome扩展的更多常用API

探索Chrome扩展的更多常用API 在Chrome扩展开发中&#xff0c;除了最基础的API外&#xff0c;Chrome还提供了一系列强大的API&#xff0c;允许开发者与浏览器的各种功能进行交互。本文将介绍其中几个常用的API&#xff0c;并提供详细的示例代码帮助您开始利用这些API。 书签…

华为与达梦数据签署全面合作协议

4月26日&#xff0c;武汉达梦数据库股份有限公司&#xff08;简称“达梦数据”&#xff09;与华为技术有限公司&#xff08;简称“华为”&#xff09;在达梦数据武汉总部签署全面合作协议。 达梦数据总经理皮宇、华为湖北政企业务总经理吕晓龙出席并见证签约&#xff1b;华为湖…

free5gc容器端口映射

启动容器 docker-compose up -d 停止容器 docker-compose stop 随后进入webconsole进行终端配置&#xff0c;与free5gc-ue.yaml中配置一致 查看端口占用进程 sudo lsof -i :8080 结束占用端口进程 sudo kill -9 68465 停止指定容器 docker stop amf 获取docker中启动容器…

什么是Meme币?——区块链技术的加密货币

Meme代币是一种基于区块链技术的加密货币&#xff0c;旨在为用户提供一种简单、有趣且易于传播的方式来进行数字资产交易和投资。Meme代币通常与特定的主题或故事相关联&#xff0c;通过社交媒体等渠道进行传播和推广&#xff0c;吸引更多的用户参与并增加其价值。 Meme代币的…

GA-CNN-LSTM多输入时序预测|遗传算法-卷积-长短期神经网络|Matlab

目录 一、程序及算法内容介绍&#xff1a; 基本内容&#xff1a; 亮点与优势&#xff1a; 二、实际运行效果&#xff1a; 三、算法介绍&#xff1a; 四、完整程序下载&#xff1a; 一、程序及算法内容介绍&#xff1a; 基本内容&#xff1a; 本代码基于Matlab平台编译&am…

在go-zero中使用jwt

gozero使用jwt 两个步骤 获取token验证token 前端获取token 先编写 jwt.api 文件&#xff0c;放在api目录下 syntax "v1"info (title: "type title here"desc: "type desc here"author: "type author here"email: &quo…

远程桌面连接不上怎么连服务器,原因是什么?如何解决?

远程桌面连接不上怎么连服务器&#xff0c;原因是什么&#xff1f;如何解决&#xff1f; 面对远程桌面连接不上的困境&#xff0c;我们有办法&#xff01; 当你尝试通过远程桌面连接服务器&#xff0c;但遭遇连接失败的挫折时&#xff0c;不要慌张。这种情况可能由多种原因引起…

W801学习笔记十九:古诗学习应用——下

经过前两章的内容&#xff0c;背唐诗的功能基本可以使用了。然而&#xff0c;仅有一种模式未免显得过于单一。因此&#xff0c;在本章中对其进行扩展&#xff0c;增加几种不同的玩法&#xff0c;并且这几种玩法将采用完全不同的判断方式。 玩法一&#xff1a;三分钟限时挑战—…

docker镜像nginx1.26.0版本,与删除docker容器【灵异事件】

为了http3 的这个模块&#xff0c;所以需要升级nginx的版本&#xff0c;需要nginx1.26.0才有 –with-http_v3_module 这个模块 为什么记录一下&#xff1f;因为觉得奇怪 1&#xff1a;删除nginx镜像&#xff0c;显示镜像还被某个容器在使用 luichunluichun:~$ docker rmi ng…

【雅思写作】Vince9120雅思小作文笔记——P1 Intro(前言)

文章目录 链接P1 Intro&#xff08;前言&#xff09;字数限制题型综述&#xff08;problem types overview&#xff09;1. **柱状图&#xff08;Bar Chart&#xff09;** - 描述不同类别在某个或多个变量上的数据量比较。2. **线图&#xff08;Line Graph&#xff09;** - 展示…

ICRA 2024 成果介绍:基于 RRT* 的连续体机器人高效轨迹规划方法

近来&#xff0c;连续体机器人研究受到越来越多的关注。其灵活度高&#xff0c;可以调整形状适应动态环境&#xff0c;特别适合于微创手术、工业⽣产以及危险环境探索等应用。 连续体机器人拥有无限自由度&#xff08;DoF&#xff09;&#xff0c;为执行空间探索等任务提供了灵…

【HMGD】GD32/STM32 DMA接收不定长串口数据

单片机型号&#xff1a;GD32F303系列 CubeMX配置 配置串口参数 开启DMA 开启中断 示例代码 使用到的变量 uint8_t RX_Buff_FLAG 0; uint8_t RX_Buff[300] {0}; uint8_t TX_Buff[300] {0};串口接收空闲函数 // 串口接收空闲函数 void HAL_UARTEx_RxEventCallback(UART_H…

【JVM基础篇】JVM入门介绍

JVM入门介绍 为什么学习JVM 岗位要求 解决工作中遇到的问题 性能调优 真实案例 导出超大文件&#xff0c;系统崩溃从数据库中查询超大量数据出错消费者消费来不及导致系统崩溃Mq消息队列接受消息导致的内存泄漏业务高峰期系统失去响应 初识JVM 什么是JVM&#xff1f; JV…

echarts指标盘属性概括

echarts指标盘属性概括 代码 有模拟数据可以直接使用const options {animation: true,title: {top: "35%",left: "center",// text: "单元测试覆盖度", // 主标题itemGap: 15,textStyle: {// 主标题样式color: "#666666",fontSize:…

云端地球联动大疆机场,支撑矿山高效巡检与智能监测

矿产资源是我国的重要战略性资源。近年来&#xff0c;随着矿山开采深度的逐渐增加&#xff0c;露天矿山边坡滑落等灾害频繁发生&#xff0c;威胁人民群众生命与财产安全。因此&#xff0c;对露天矿边坡进行快速、实时、有效的形变监测和预警已成为当前我国矿山防灾与安全生产的…

Python:如何找到列表中给定元素的索引

在Python编程中&#xff0c;我们经常需要找到列表中某个特定元素的位置&#xff0c;也就是它的索引。索引是元素在列表中的位置&#xff0c;从0开始计数。本文将详细讲解如何使用Python来找到列表中给定元素的索引&#xff0c;并通过示例代码来加深理解。 基本原理 Python提供…

二级等保与三级等保的区别有哪些

二级等保和三级等保的区别主要体现在保护能力、安全要求、监管严格程度等方面。以下是根据提供的搜索结果中关于二级和三级等保的具体差异&#xff1a; 1. 保护能力&#xff1a; 二级等保要求信息系统能够防护来自外部小型组织的威胁&#xff0c;发现重要的安全漏洞和事件&…