【android 9】【input】【7.发送按键事件1——InputReader线程】

系列文章目录

本人系列文章-CSDN博客


目录

系列文章目录

1.简介

1.1发送流程介绍

1.2 时序图

2.普通按键消息发送部分源码分析

 2.1 设备的监听

 2.2 inputreader线程阻塞等待事件发生

 2.3 按键事件的产生

 2.4 EventHub::getEvents

2.5 InputReader::loopOnce

 2.6 processEventsLocked

 2.7 processEventsForDeviceLocked

 2.8 InputDevice::process

 2.9 KeyboardInputMapper::process

2.10 KeyboardInputMapper::process(扫描消息)

 2.11 KeyboardInputMapper::process(按键按下)

 2.12 KeyboardInputMapper::processKey

2.13 QueuedInputListener::notifyKey

 2.14 KeyboardInputMapper::process(同步消息)

 2.15 KeyboardInputMapper::process(扫描事件)

 2.16 KeyboardInputMapper::process(按键抬起事件)

2.17 KeyboardInputMapper::process(同步消息)

 2.18 QueuedInputListener::flush

 2.19 NotifyKeyArgs::notify

 2.20 InputDispatcher::notifyKey

 2.21 NativeInputManager::interceptKeyBeforeQueueing

2.22 IMS中的interceptKeyBeforeQueueing

 2.23 WMS中的interceptKeyBeforeQueueing

2.24 PhoneWindowManager中的interceptKeyBeforeQueueing

 2.25 NativeInputManager::handleInterceptActions

 2.26 InputDispatcher::enqueueInboundEventLocked

 2.27 NotifyKeyArgs::notify(抬起)


1.简介

从之前的篇幅我们知道了,事件分为设备增删事件和原始输入事件,而原始输入事件主要有两种,一种是key按键事件的派发,一种是触摸事件的派发。本篇区别于其他作者,会不放过任何一个函数,进行系统完整的分析。此篇主要分析一个按键事件从驱动上报到应用监听获取的全流程。

1.1发送流程介绍

1.input启动后,完成对设备的加载后,当无事件产生时,inputreader线程便阻塞在epoll_wait等待有消息的产生。

2.当物理按键按下和抬起的时候,即按键事件便产生了。内核会上报按下和抬起的消息给到input系统。

3.inputreader线程中的不再阻塞,然后从notifyfd中读取原始的input_event输入事件。

 4.对原始的input_event事件,进行加工成RawEvent事件,然后调用对应的KeyboardInputMapper进行处理。

5.KeyboardInputMapper会将RawEvent事件再加工成NotifyKeyArgs事件,然后插入到mArgsQueue队列中。

6.然后会调用InputDispatcher的notifyKey,将NotifyKeyArgs事件再加工成KeyEntry事件,放入mInboundqueue队列队尾,然后唤醒InputDispatcher分发线程,进行按键事件的分发。

1.2 时序图

  为了完整的画出所有时序,较为模糊,读者可保存照片到本地放大观看。


2.普通按键消息发送部分源码分析

为了便于读者清晰的了解发送流程,此章节会删除不执行的代码,便于理解。

 2.1 设备的监听

要想知道某设备的事件发生,首先,便是要监听设备。在启动篇中,我们知道会注册设备到epoll中完成对设备的监听。

下面是前文监听设备节点的代码

status_t EventHub::registerDeviceForEpollLocked(Device* device) {struct epoll_event eventItem;memset(&eventItem, 0, sizeof(eventItem));eventItem.events = EPOLLIN;//监听的文件描述符是否有可读事件eventItem.data.u32 = device->id;//事件触发时会返回eventItem.data数据if (epoll_ctl(mEpollFd, EPOLL_CTL_ADD, device->fd, &eventItem)) {//监听device->fd对应的设备是否有可读事件。//如果有事件可读,则返回eventItem.data}return OK;
}

 2.2 inputreader线程阻塞等待事件发生

void InputReader::loopOnce() {int32_t oldGeneration;int32_t timeoutMillis;bool inputDevicesChanged = false;Vector<InputDeviceInfo> inputDevices;{ oldGeneration = mGeneration;//新的值timeoutMillis = -1;size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);//通过EventHub的getEvents函数获取事件,//并存放在mEventBuffer中。//参数分析:timeoutMillis=-1,代表等待超时的时间,感觉如果数字为0代表立即执行//mEventBuffer是一个存放从eventhub中读取的rawevent结构体类型的数组,源代码是:RawEvent mEventBuffer[EVENT_BUFFER_SIZE];//EVENT_BUFFER_SIZE值是256,代表最大可以读取256个原始事件,查看833行-840行,RawEvent结构体如下
.......

 即:此时阻塞在getEvents函数的epoll_wait中。

//第一次for循环,会阻塞在epoll_wait中,等待设备增加事件的到来。size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {ALOG_ASSERT(bufferSize >= 1);struct input_event readBuffer[bufferSize];//bufferSize值为传入的256RawEvent* event = buffer;//event指针指向传入的buffer首地址,每存入一个事件,event指针向后移动一个元素size_t capacity = bufferSize;//capacity表示buffer中剩余端元素数量,capacity为0,表示buffer已满bool awoken = false;for (;;) {nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);//获取当前时间mPendingEventIndex = 0;//此时mPendingEventItems数组中未处理的事件已经处理完了,将mPendingEventIndex设置为0int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);//此时读线程会阻塞在此处,等待有消息的事件发生......

 2.3 按键事件的产生

首先从本系列的input第一篇可知,一次按键按下和抬起的过程中,会产生六条数据包,一个按键按下到抬起,发了6包数据,依次是:

扫描事件、KEY事件、同步事件、扫描事件、KEY事件、同步事件。

比如按下A按键,然后抬起。则内核上报的数据为:

按下:
/dev/input/event4: EV_MSC MSC_SCAN 00090001//扫描事件,EV_MSC代表是杂项事件,MSC_SCAN代表是杂项事件中的扫描事件,Value代表值是00090001,说明扫描到BTN_GAMEPAD这个按键有变化
/dev/input/event4: EV_KEY BTN_GAMEPAD 00000001 //KEY事件,EV_KEY代表这是按键类型事件,BTN_GAMEPAD代表手柄注册中的按键是BTN_GAMEPAD,00000001表示按下
/dev/input/event4: EV_SYN SYN_REPORT 00000000 //同步事件,标识一个独立的事件的发生,它与另一个同步包夹中间的一组的打印就是一个事件的发生
抬起:
/dev/input/event4: EV_MSC MSC_SCAN 00090001 //扫描事件
/dev/input/event4: EV_KEY BTN_GAMEPAD 00000000 //KEY事件,00000000代表抬起
/dev/input/event4: EV_SYN SYN_REPORT 00000000 //同步事件

 然后便是按键事件的处理

 2.4 EventHub::getEvents

主要作用是:

1.epoll_wait不再阻塞,从发生事件的fd中读取按键原始事件并加工成RawEvent类型的消息。

回到2.2 inputreader线程阻塞的地方,因为对设备的fd进行了epoll监听其是否有可读事件,所以当有事件时,第一次for循环中epoll_wait不再阻塞,向下执行。

//第一次死循环,当按键按下产生按键按下的消息。epoll_wait不再阻塞,向下执行。
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {struct input_event readBuffer[bufferSize];//bufferSize值为传入的256RawEvent* event = buffer;//event指针指向传入的buffer首地址,每存入一个事件,event指针向后移动一个元素size_t capacity = bufferSize;//capacity表示buffer中剩余端元素数量,capacity为0,表示buffer已满bool awoken = false;for (;;) {nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);//获取当前时间mPendingEventIndex = 0;//此时mPendingEventItems数组中未处理的事件已经处理完了,将mPendingEventIndex设置为0int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);//此时读线程会阻塞在此处,等待有消息的事件发生///if (pollResult < 0) {} else {// 发生了某些事件mPendingEventCount = size_t(pollResult);}}/// 此时并没有返回而是开启了下一次死循环。///return event - buffer;
}

 第二次for循环,会从设备的fd中读取原始事件,并将消息加工成RawEvent类型。

//第二次for死循环
//假设只有一个设备,一个按下,一个抬起,相当于六条消息,
//依次为扫描事件、key按下事件、同步事件、扫描事件、key抬起事件、同步事件。
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {size_t capacity = bufferSize;//capacity表示buffer中剩余端元素数量,capacity为0,表示buffer已满RawEvent* event = buffer;for (;;) {nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);//获取当前时间//如果存在未处理的事件,则从mPendingEventItems数组中取出事件bool deviceChanged = false;while (mPendingEventIndex < mPendingEventCount) {//此时会执行,此时mPendingEventIndex=0,mPendingEventCount为设备有变化的数量//此时相当于mPendingEventCount =1,const struct epoll_event& eventItem = mPendingEventItems[mPendingEventIndex++];ssize_t deviceIndex = mDevices.indexOfKey(eventItem.data.u32);//获取事件的data.u32字段,此字段存储的是device-id,//然后判断此事件发生的设备是否在mDevices中能查找到对应的deviceidif (deviceIndex < 0) {//在已经打开的设备中,没找到此事件对应的设备ALOGW("Received unexpected epoll event 0x%08x for unknown device id %d.",eventItem.events, eventItem.data.u32);continue;}Device* device = mDevices.valueAt(deviceIndex);//通过deviceid,找到对应的deviceif (eventItem.events & EPOLLIN) {//现在处理的是input设备产生的原始事件int32_t readSize = read(device->fd, readBuffer,sizeof(struct input_event) * capacity);//从input设备中,读取event事件,存入readBuffer数组中,从input设备中,读取event事件,存入readBuffer数组中,if (readSize == 0 || (readSize < 0 && errno == ENODEV)) {//如果满足此条件,代表在通知INotify之前,设备已被移除。}else{int32_t deviceId = device->id == mBuiltInKeyboardId ? 0 : device->id;//判断是否是内置键盘,给devcie-id赋值size_t count = size_t(readSize) / sizeof(struct input_event);//计算事件的数量,此时等于6for (size_t i = 0; i < count; i++) {struct input_event& iev = readBuffer[i];//此处是将input_event信息, 封装成RawEventevent->deviceId = deviceId;event->type = iev.type;event->code = iev.code;event->value = iev.value;event += 1;capacity -= 1;}if (capacity == 0) {//buffer数组已满,重置mPendingEventIndex,//注意,此时有三个数组,分别是//1.存储epoll_event结构体的mPendingEventItems数组,epoll_event& eventItem = mPendingEventItems[mPendingEventIndex++];//2.存储input_event结构体的readbuffer数组,input_event readBuffer[bufferSize],//3.存储RawEvent类型的buffer数组,RawEvent* event = buffer//流程如下://当设备有可读事件时,会返回一个epoll_event结构体,存储mPendingEventItems数组中,然后从mPendingEventItems数组中,//取出这个epoll_event结构体,函数进行判断epoll_event结构体的eventItem.events = EPOLLIN,此字段代表监听的fd有可读事件,//然后,通过read(fd,readbuffer)函数,去读取此设备中的多个input_event结构体类型的事件,//存储在readbuffer数组中,这是第二个数组,然后循环取出input_event类型的数据,//将其进行加工成rawinput类型,存储在存储RawEvent类型的buffer数组中。//所以此处的mPendingEventIndex含义减去一的含义是://比如:buffer数组的容量是200,一个eventItem相当于对应一个设备,所以一个eventItem会对应多个事件。//第一个eventItem对应的按键事件有150个,第二个eventItem对应的事件有200个,//当拿到第二个eventItem时,因为eventItem = mPendingEventItems[mPendingEventIndex++];,所以mPendingEventIndex值等于3,//然后去读取数据,并将其加工到buffer数组中,此时buffer在加工到50个的时候已经满了。但是第二个eventItem对应的剩余事件还没加工完//所以此处重新吧mPendingEventIndex设置为第二个,等待下一次getevent中调用,继续处理第二个eventitem对应的所以事件。mPendingEventIndex -= 1;break;//退出循环,此时退出的是eventItem这个循环。}}}}// 当event的指针不再指向buffer的首地址时,代表里面有数据,或者被唤醒时,立即退出循环if (event != buffer || awoken) {break;//退出for死循环}}// 全部完成后,返回我们读取的事件数。return event - buffer;
}

 加工的代码如下:

RawEvent* event = buffer;
event->deviceId = deviceId;
event->type = iev.type;
event->code = iev.code;
event->value = iev.value;struct RawEvent {nsecs_t when;//时间int32_t deviceId;//事件发生的设备idint32_t type;//类型,例如按键事件等int32_t code;//扫描码,按键对应的扫描码int32_t value;//值,表示按键按下,或者抬起等
};

 所以此时只有按键按下的情况时,有六条RawEvent消息。

第一条:RawEvent是对应的扫描事件。type对应的是EV_MSC,code对应的是MSC_SCAN,value对应的是00090001

后面消息类同。

然后,我们需要回到调用getEvents函数的InputReader::loopOnce中。

2.5 InputReader::loopOnce

当getEvents函数返回6条RawEvent的消息时候,会调用processEventsLocked进行处理。

void InputReader::loopOnce() {int32_t oldGeneration;int32_t timeoutMillis;bool inputDevicesChanged = false;Vector<InputDeviceInfo> inputDevices;oldGeneration = mGeneration;//值为1,在inputreader构造函数中mGeneration=1,timeoutMillis = -1;uint32_t changes = mConfigurationChangesToRefresh;//值为0,初始化时设置为0,代表无变化,不刷新配置size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);//通过EventHub的getEvents函数获取事件,//并存放在mEventBuffer中。//参数分析:timeoutMillis=-1,代表啥意思不清楚,感觉如果数字为0代表立即执行//mEventBuffer是一个存放从eventhub中读取的rawevent结构体类型的数组,源代码是:RawEvent mEventBuffer[EVENT_BUFFER_SIZE];//EVENT_BUFFER_SIZE值是256,代表最大可以读取256个原始事件if (count) {//返回的事件数量大于,则调用processEventsLocked处理事件processEventsLocked(mEventBuffer, count);}//发布事件。 processEventsLocked()函数在对事件进行加工处理之后,便将处理后的事件存储在// mQueuedListener中。在循环的最后,通过调用flush()函数将所有事件交付给InputDispatchermQueuedListener->flush();
}

 2.6 processEventsLocked

此函数的主要作用是:

1.如果事件是原始输入事件,将同一设备的原始输入事件进行打包处理。

2.如果事件是设备增删事件,则进行增删事件的处理。

void InputReader::processEventsLocked(const RawEvent* rawEvents, size_t count) {//用rawEvents指针接收传入的从传入的mEventBuffer数组的首地址for (const RawEvent* rawEvent = rawEvents; count;) {//for循环遍历数组int32_t type = rawEvent->type;//取出数组中第0号的元素rawEvent的type.size_t batchSize = 1;//事件信息有两种,一种是设备节点的增加和删除事件,统称为设备事件,另一种是原始输入事件。//此处满足条件,则是对原始输入事件进行处理if (type < EventHubInterface::FIRST_SYNTHETIC_EVENT) {//FIRST_SYNTHETIC_EVENT值是0x10000000,type值都是小于FIRST_SYNTHETIC_EVENTint32_t deviceId = rawEvent->deviceId;//继续取出0号元素的id号while (batchSize < count) {if (rawEvent[batchSize].type >= EventHubInterface::FIRST_SYNTHETIC_EVENT|| rawEvent[batchSize].deviceId != deviceId) {//循环数组中所有其他的元素rawEvent对象,//只有当数组中其他rawEvent对象,即是原始输入事件也是同一个设备id的时候,则batchSize+1break;}batchSize += 1;}processEventsForDeviceLocked(deviceId, rawEvent, batchSize);//此处相当于三个事件一起打包//此处举个例子//现在rawEvent数组有五个事件,前两个是id为1的设备产生的。第三个是id为2设备产生的,第四和第五是id为1设备产生的。//此时会先取出第一个事件的设备id,然后遍历第2.3.4.5事件的设备id,发现第二个事件也是设备id为1的。那就batchSize+1=2,//此时遍历第三个事件设备id,发现和前两个不一样,则退出循环。//表示,从第一个事件开始,一共有两个事件是相同的设备id为1产生的。所以,rawEvent参数和batchSize参数的含义就明显了。//那后面的第3.4.5事件呢,从下面的代码看出调用完processEventsForDeviceLocked后,rawEvent指针指向了数组中第三个事件,//然后继续遍历第4.5事件的id。}count -= batchSize;rawEvent += batchSize;}
}

 2.7 processEventsForDeviceLocked

主要作用:

1.从保存的容器中找到此设备id对应的InputDevice类对象

2.调用此对象的process方法

void InputReader::processEventsForDeviceLocked(int32_t deviceId,const RawEvent* rawEvents, size_t count) {ssize_t deviceIndex = mDevices.indexOfKey(deviceId);//从已经扫描到的设备容器中查找此id对应的device对象InputDevice* device = mDevices.valueAt(deviceIndex);//获取此deviceId对应的device对象device->process(rawEvents, count);//调用device->process方法处理。rawEvents是指向所有事件数组的首地址的指针,//count之前打包的是同一设备产生的输入事件的数量}

 2.8 InputDevice::process

void InputDevice::process(const RawEvent* rawEvents, size_t count) {//按顺序处理每个映射器的所有事件。size_t numMappers = mMappers.size();//mMappers是一个存储InputMapper指针的容器,Vector<InputMapper*> mMappers;for (const RawEvent* rawEvent = rawEvents; count != 0; rawEvent++) {//取出每一个按键消息。此时第一个消息是扫描消息else {for (size_t i = 0; i < numMappers; i++) {//循环调用所有的mapper去派发按键消息InputMapper* mapper = mMappers[i];mapper->process(rawEvent);//将事件交给mapper处理。InputMapper有多个子类。//后续以keyborardinputMapper,用于处理键盘输入事件分析。}}--count;}
}

 2.9 KeyboardInputMapper::process

此函数的主要作用是:

1.循环取出所有原始事件,进行处理。

        1.1 如果是扫描消息,则取出其Value作为下一个按键消息的策略flag。但此扫描消息并不派发。

        1.2 如果是按键消息,则进行处理派发。

        1.3 如果是同步消息,则重置此按键消息的策略flag。

void KeyboardInputMapper::process(const RawEvent* rawEvent) {switch (rawEvent->type) {case EV_KEY: {//如果是按键消息int32_t scanCode = rawEvent->code;int32_t usageCode = mCurrentHidUsage;mCurrentHidUsage = 0;if (isKeyboardOrGamepadKey(scanCode)) {processKey(rawEvent->when, rawEvent->value != 0, scanCode, usageCode);}break;}case EV_MSC: {//如果是杂项消息if (rawEvent->code == MSC_SCAN) {mCurrentHidUsage = rawEvent->value;}break;}case EV_SYN: {//如果是同步消息if (rawEvent->code == SYN_REPORT) {mCurrentHidUsage = 0;}}}
}

2.10 KeyboardInputMapper::process(扫描消息)

我们先看看第一个扫描消息是如何处理的。

//先处理第一个消息-扫描消息,没派发
void KeyboardInputMapper::process(const RawEvent* rawEvent) {switch (rawEvent->type) {case EV_MSC: {if (rawEvent->code == MSC_SCAN) {mCurrentHidUsage = rawEvent->value;//记录当前扫描事件的value值。此时值为00090001,此值后续会对应policyflag}break;}}
}

 2.11 KeyboardInputMapper::process(按键按下)

 接下来,我们看看第二个消息按键按下是如何处理的。

//处理第二个消息-key消息(A按键按下信息)
void KeyboardInputMapper::process(const RawEvent* rawEvent) {switch (rawEvent->type) {case EV_KEY: {//如果此事件是按键事件int32_t scanCode = rawEvent->code;//取出事件代码保存的键盘扫描码,此时是0130int32_t usageCode = mCurrentHidUsage;//保存值为00090001mCurrentHidUsage = 0;//设置为0if (isKeyboardOrGamepadKey(scanCode)) {//排除对鼠标按键的处理。鼠标按键由CursorInputMapper处理processKey(rawEvent->when, rawEvent->value != 0, scanCode, usageCode);//调用KeyboardInputMapper的processKey方法。//processKey方法,主要是将rawevent事件加工成NotifyKeyArgs,然后将NotifyKeyArgs放入队列中。//传入参数分析:rawEvent->when表示时间,rawEvent->value != 0如果为真,表示按键按下,scanCode扫描码}break;}}
}//排除是鼠标的按键,鼠标按键由CursorInputMapper处理
bool KeyboardInputMapper::isKeyboardOrGamepadKey(int32_t scanCode) {return scanCode < BTN_MOUSE|| scanCode >= KEY_OK|| (scanCode >= BTN_MISC && scanCode < BTN_MOUSE)|| (scanCode >= BTN_JOYSTICK && scanCode < BTN_DIGI);
}

 2.12 KeyboardInputMapper::processKey

此处的主要作用是:

1.将扫描码和usagecode通过加载好的按键映射表转化为keycode和派发策略。

 2.生成NotifyKeyArgs按键事件,插入到分发线程的队列中,等待分发。

那么为什么要将扫描码和usagecode转化为keycode和派发策略呢?

因为驱动往往是各个不同的供应商开发的,故存在不同键盘的扫描码是不同的,为了保证上层无论是什么牌子的键盘,对于按键A受到的消息是一致的,因此需要转化为统一的keycode,厂商会将对应关系写入配置文件,这样,无论是任何牌子的键盘按键A,上层都是同一个keycode。

void KeyboardInputMapper::processKey(nsecs_t when, bool down, int32_t scanCode,int32_t usageCode) {int32_t keyCode;int32_t keyMetaState;uint32_t policyFlags;//通过EventHub的mapKey()函数进行映射。映射的输入为scancode和usagecode,而输出为keyCode和flag//scancode->keyCode//usagecode->flag//此步的作用是将物理的扫描码——scancode描述的按键,转化为操作系统的识别的keyCode——虚拟键值//EventHub::mapKey()函数可以根据设备Id找到对应的KeyLayoutMap,进而根据扫描码找到 //对应的Key结构体中所保存的虚拟键值(传出参数keycode)以及策略值(传出参数flag),从 //而完成从扫描码到虚拟键值的映射工作if (getEventHub()->mapKey(getDeviceId(), scanCode, usageCode, mMetaState,&keyCode, &keyMetaState, &policyFlags)) {keyCode = AKEYCODE_UNKNOWN;如果映射失败,则使用UNKNOWN作为事件的虚拟键值keyMetaState = mMetaState;//mMetaState默认是0,代表shift等控制键的状态policyFlags = 0;}if (down) {//KeyboardInputMapper维护了保存KeyDown结构体的容器mKeyDowns。//当按键按下时会生成一个保存了扫描码与keyCode的 KeyDown对象并添加到集合中。 ssize_t keyDownIndex = findKeyDown(scanCode);//通过扫描码对mKeyDowns容器查找,结果表明了按键是否是重复按下if (keyDownIndex >= 0) {} else {//生成keyDown,并保存到mKeyDowns容器中mKeyDowns.push();KeyDown& keyDown = mKeyDowns.editTop();keyDown.keyCode = keyCode;keyDown.scanCode = scanCode;}mDownTime = when;//按键按下的时间,也就是事件发生的时间}nsecs_t downTime = mDownTime;//将所有的按键事件信息封装为NotifykeyArga对象,并将此对象通知给inputdispather的listenerNotifyKeyArgs args(when, getDeviceId(), mSource, policyFlags,down ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP,AKEY_EVENT_FLAG_FROM_SYSTEM, keyCode, scanCode, keyMetaState, downTime);//参数分析://when对应eventTime, 其实是rawEvent->when,就是事件发生的时间//getDeviceId()对应deviceId, 返回的是设备id//mSource对应source,是KeyboardInputMapper初始化时赋值的//其值是INPUT_DEVICE_CLASS_KEYBOARD,INPUT_DEVICE_CLASS_DPAD从,INPUT_DEVICE_CLASS_GAMEPAD的某一个//policyFlags对应policyFlags值是策略标志//down ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP,对应action,描述按下或者抬起//AKEY_EVENT_FLAG_FROM_SYSTEM对应flag,表示此事件是系统信任的。//keyCode对应keyCode,虚拟健keycode//scanCode对应scanCode,扫描码//keyMetaState对应metaState,控制健的值,其可以表示控制键的按下或者抬起,此时值应该是0,代表无任何控制键按下//downTime对应downTime就等于whengetListener()->notifyKey(&args);//将生成的NotifyKeyArgs插入到mArgsQueue队列中//getListener返回的是QueuedInputListener类对象,其的一个属性是InputDispatcher类的对象
}

 此处便是scancode和usagecode转化输出为keyCode和flag的地方。

status_t EventHub::mapKey(int32_t deviceId,int32_t scanCode, int32_t usageCode, int32_t metaState,int32_t* outKeycode, int32_t* outMetaState, uint32_t* outFlags) const {AutoMutex _l(mLock);Device* device = getDeviceLocked(deviceId);//获取device对象status_t status = NAME_NOT_FOUND;if (device) {sp<KeyCharacterMap> kcm = device->getKeyCharacterMap();//获取键盘的映射表if (kcm != NULL) {if (!kcm->mapKey(scanCode, usageCode, outKeycode)) {//将扫描码和usageCode转化为outKeycode,如果转化失败,则代表需要策略*outFlags = 0;status = NO_ERROR;}}// Check the key layout next.if (status != NO_ERROR && device->keyMap.haveKeyLayout()) {if (!device->keyMap.keyLayoutMap->mapKey(scanCode, usageCode, outKeycode, outFlags)) {//将扫描码和usageCode转化为outKeycode和outFlagsstatus = NO_ERROR;}}if (status == NO_ERROR) {//转化成功后,再尝试将metaState转化为对应的outMetaStateif (kcm != NULL) {kcm->tryRemapKey(*outKeycode, metaState, outKeycode, outMetaState);} else {*outMetaState = metaState;}}}return status;
}
ssize_t KeyboardInputMapper::findKeyDown(int32_t scanCode) {size_t n = mKeyDowns.size();//mKeyDowns是一个存储KeyDown结构体的容器,Vector<KeyDown> mKeyDowns; //    struct KeyDown {//    int32_t keyCode;//    int32_t scanCode;//};for (size_t i = 0; i < n; i++) {if (mKeyDowns[i].scanCode == scanCode) {return i;}}return -1;
}

2.13 QueuedInputListener::notifyKey

 将按键消息放入分发队列中

void QueuedInputListener::notifyKey(const NotifyKeyArgs* args) {mArgsQueue.push(new NotifyKeyArgs(*args));//将消息放入mArgsQueue队列中
}

 2.14 KeyboardInputMapper::process(同步消息)

//处理第三个消息-同步消息,也没派发。
void KeyboardInputMapper::process(const RawEvent* rawEvent) {switch (rawEvent->type) {case EV_SYN: {if (rawEvent->code == SYN_REPORT) {mCurrentHidUsage = 0;//将mCurrentHidUsage重置为o}}}
}

 2.15 KeyboardInputMapper::process(扫描事件)

接下来,是第四条消息的扫描事件的处理。此处同2.10

//,处理第四个消息-扫描消息,没派发
void KeyboardInputMapper::process(const RawEvent* rawEvent) {switch (rawEvent->type) {case EV_MSC: {if (rawEvent->code == MSC_SCAN) {mCurrentHidUsage = rawEvent->value;//记录当前扫描事件的value值。此时值为00090001,此值后续会对应policyflag}break;}}
}

 2.16 KeyboardInputMapper::process(按键抬起事件)

//处理第二个消息-key消息(A按键抬起信息)
void KeyboardInputMapper::process(const RawEvent* rawEvent) {switch (rawEvent->type) {case EV_KEY: {//如果此事件是按键事件int32_t scanCode = rawEvent->code;//取出事件代码保存的键盘扫描码,此时是0130int32_t usageCode = mCurrentHidUsage;//保存值为00090001mCurrentHidUsage = 0;//设置为0if (isKeyboardOrGamepadKey(scanCode)) {//排除对鼠标按键的处理。鼠标按键由CursorInputMapper处理processKey(rawEvent->when, rawEvent->value != 0, scanCode, usageCode);//调用KeyboardInputMapper的processKey方法。//processKey方法,主要是将rawevent事件加工成NotifyKeyArgs,然后将NotifyKeyArgs放入队列中。//传入参数分析:rawEvent->when表示时间,rawEvent->value != 0如果为真,表示按键按下,scanCode扫描码}break;}}
}

此处的作用是:

1.将按键的扫描码和usagecode通过键盘映射表转化为keycode和分发策略

2.生成按键抬起的NotifyKeyArgs事件,并放入消息队列中。

//按键抬起事件分析
void KeyboardInputMapper::processKey(nsecs_t when, bool down, int32_t scanCode,int32_t usageCode) {int32_t keyCode;int32_t keyMetaState;uint32_t policyFlags;if (getEventHub()->mapKey(getDeviceId(), scanCode, usageCode, mMetaState,&keyCode, &keyMetaState, &policyFlags)) {keyCode = AKEYCODE_UNKNOWN;keyMetaState = mMetaState;policyFlags = 0;}if (down) {......} else {ssize_t keyDownIndex = findKeyDown(scanCode);if (keyDownIndex >= 0) {keyCode = mKeyDowns.itemAt(keyDownIndex).keyCode;//当按键抬起时,和按键按下保持同一个keycodemKeyDowns.removeAt(size_t(keyDownIndex));//从保存按下的容器中删除key按下} }nsecs_t downTime = mDownTime;//此按键按下发生的时间//将所有的按键事件信息封装为NotifykeyArga对象,并将此对象通知给inputdispather的listenerNotifyKeyArgs args(when, getDeviceId(), mSource, policyFlags,down ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP,AKEY_EVENT_FLAG_FROM_SYSTEM, keyCode, scanCode, keyMetaState, downTime);//参数分析://when对应eventTime, 其实是rawEvent->when,就是事件发生的时间//getDeviceId()对应deviceId, 返回的是设备id//mSource对应source,是KeyboardInputMapper初始化时赋值的//其值是INPUT_DEVICE_CLASS_KEYBOARD,INPUT_DEVICE_CLASS_DPAD从,INPUT_DEVICE_CLASS_GAMEPAD的某一个//policyFlags对应policyFlags值是策略标志//down ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP,对应action,描述按下或者抬起//AKEY_EVENT_FLAG_FROM_SYSTEM对应flag,表示此事件是系统信任的。//keyCode对应keyCode,虚拟健keycode//scanCode对应scanCode,扫描码//keyMetaState对应metaState,控制健的值,其可以表示控制键的按下或者抬起,此时值应该是0,代表无任何控制键按下//downTime对应按键按下的downTimegetListener()->notifyKey(&args);
}

2.17 KeyboardInputMapper::process(同步消息)

//抬起时的第三条消息同步消息,也没派发
void KeyboardInputMapper::process(const RawEvent* rawEvent) {switch (rawEvent->type) {case EV_SYN: {if (rawEvent->code == SYN_REPORT) {mCurrentHidUsage = 0;}}}
}

 2.18 QueuedInputListener::flush

所以此时分发队列的线程中存在两个事件,一个是按下事件,一个是抬起事件。然后在InputReader::loopOnce函数会调用flush唤醒分发线程去处理。

/所以当一个按键按下抬起时,产生6个消息时,只有key事件会被加入队列派发。
void QueuedInputListener::flush() {size_t count = mArgsQueue.size();for (size_t i = 0; i < count; i++) {//从存储NotifyArgs的容器中循环取出并调用notifyNotifyArgs* args = mArgsQueue[i];args->notify(mInnerListener);//传入的参数mInnerListener是InputDispatcher类对象delete args;}mArgsQueue.clear();
}

 2.19 NotifyKeyArgs::notify

此时先查看第一个按下事件是如何处理的。

void NotifyKeyArgs::notify(const sp<InputListenerInterface>& listener) const {listener->notifyKey(this);//此处listener是InputDispatcher类对象,this是调用者也就是一个NotifyKeyArgs对象
}

 2.20 InputDispatcher::notifyKey

此函数的作用为:

1.首先通过NotifyKeyArgs生成KeyEvent事件,KeyEvent事件一般是上层最终消费的事件类型。

然后向ims询问此事件的发送策略,是否是PhoneWindowManager消费。因为有的按键不会派发给应用,如手机的开机按键,不需要派发给应用,是派发给PhoneWindowManager拦截消费的,如果此按键被拦截消费了,则不再派发给应用。

2.查询派发策略是派发给应用的后,通过NotifyKeyArgs生成KeyEntry事件,并将其放入分发队列的队尾。然后唤醒分发线程派发。

注意:此处是每个消息都会唤醒一次,故此时是按键按下事件会唤醒一次,然后按键抬起事件再唤醒一次。

//此时我们分析的是普通按键的发送,故先不分析如开关键等需要phoneManager消费的按键事件。
void InputDispatcher::notifyKey(const NotifyKeyArgs* args) {if (!validateKeyEvent(args->action)) {//验证keyEvent是否有效,主要是验证action是否等于按下或者抬起,//如果不是则数据无效,则返回return;}uint32_t policyFlags = args->policyFlags;//派发策略flagint32_t flags = args->flags;//此时flag是AKEY_EVENT_FLAG_FROM_SYSTEMint32_t metaState = args->metaState;//控制键按下或者抬起的信息policyFlags |= POLICY_FLAG_TRUSTED;//指示输入事件来自受信任的源,例如直接连接的输入设备int32_t keyCode = args->keyCode;KeyEvent event;//用NotifyKeyArgs初始化了KeyEvent,主要用于inputfiter,询问是否过滤//例如:有些需要phoneManager消费的按键,如手机的开关键等,会拦截发送给phoneManager系统event.initialize(args->deviceId, args->source, args->action,flags, keyCode, args->scanCode, metaState, 0,args->downTime, args->eventTime);android::base::Timer t;mPolicy->interceptKeyBeforeQueueing(&event, policyFlags);//向mPolicy询问派发策略if (t.duration() > SLOW_INTERCEPTION_THRESHOLD) {}bool needWake;{ // acquire lockmLock.lock();int32_t repeatCount = 0;KeyEntry* newEntry = new KeyEntry(args->eventTime,//用NotifyKeyArgs初始化了KeyEntryargs->deviceId, args->source, policyFlags,args->action, flags, keyCode, args->scanCode,metaState, repeatCount, args->downTime);needWake = enqueueInboundEventLocked(newEntry);//此函数会在内部将KeyEntry添加到mInboundqueue队列队尾,//并返回是否需要唤醒,如果是true,则唤醒dispatcher线程mLock.unlock();} // release lockif (needWake) {mLooper->wake();//唤醒分发线程。}
}
static bool validateKeyEvent(int32_t action) {if (! isValidKeyAction(action)) {ALOGE("Key event has invalid action code 0x%x", action);return false;}return true;
}static bool isValidKeyAction(int32_t action) {switch (action) {case AKEY_EVENT_ACTION_DOWN:case AKEY_EVENT_ACTION_UP:return true;default:return false;}
}

 2.21 NativeInputManager::interceptKeyBeforeQueueing

此处主要作用为:

1.向IMS询问派发策略,最后其实是向PhoneWindowManager询问派发按键策略。

void NativeInputManager::interceptKeyBeforeQueueing(const KeyEvent* keyEvent, uint32_t& policyFlags) {ATRACE_CALL();bool interactive = mInteractive.load();//mInteractive是原子变量,所以需要用load。此时设备是否处于交互中if (interactive) {policyFlags |= POLICY_FLAG_INTERACTIVE;//指示截获事件时设备处于交互状态。}if ((policyFlags & POLICY_FLAG_TRUSTED)) {//如果policyFlags是受信任的nsecs_t when = keyEvent->getEventTime();JNIEnv* env = jniEnv();jobject keyEventObj = android_view_KeyEvent_fromNative(env, keyEvent);//通过jni用c++的keyevent形成一个java的keyeventjint wmActions;if (keyEventObj) {wmActions = env->CallIntMethod(mServiceObj,gServiceClassInfo.interceptKeyBeforeQueueing,keyEventObj, policyFlags);//调用了IMS中的interceptKeyBeforeQueueing方法if (checkAndClearExceptionFromCallback(env, "interceptKeyBeforeQueueing")) {wmActions = 0;}android_view_KeyEvent_recycle(env, keyEventObj);env->DeleteLocalRef(keyEventObj);} else {ALOGE("Failed to obtain key event object for interceptKeyBeforeQueueing.");wmActions = 0;}handleInterceptActions(wmActions, when, /*byref*/ policyFlags);//将wmActions策略添加给policyFlags} else {//如果policyFlags是不受信任的if (interactive) {policyFlags |= POLICY_FLAG_PASS_TO_USER;//则设备消息类型为发送给应用程序}}
}

2.22 IMS中的interceptKeyBeforeQueueing

此时通过jni走到了IMS.java的interceptKeyBeforeQueueing函数。

此函数的作用是:

1.调用到WMS中的interceptKeyBeforeQueueing函数。其实现类是InputMonitor.java

//IMS.java
//派发给wms,查看是否将消息拦截,此处并不分析//派发给wms,查看是否将消息拦截
private int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {return mWindowManagerCallbacks.interceptKeyBeforeQueueing(event, policyFlags);
}

 2.23 WMS中的interceptKeyBeforeQueueing

//此时已经到了WMS中,InputMonitor.JAVA中 
public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {return mService.mPolicy.interceptKeyBeforeQueueing(event, policyFlags);
//是WindowManagerPolicy接口的函数,实现类是PhoneWindowManager}

2.24 PhoneWindowManager中的interceptKeyBeforeQueueing

此函数的主要作用是:

1.如果是普通按键,则返回派发给应用程序的派发策略。

2.如果是特殊按键,如电源按键,会取消其派发给应用程序的策略。

//PhoneWindowManager.java
//此处代码太多,只看部分代码public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0; //interactive值是true,即应用正在和用户交互中final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;//按下或抬起final boolean canceled = event.isCanceled();//是否取消falsefinal int keyCode = event.getKeyCode();final boolean isInjected = (policyFlags & WindowManagerPolicy.FLAG_INJECTED) != 0;//是否是注入的方式,是false// Basic policy based on interactive state.int result;boolean isWakeKey = (policyFlags & WindowManagerPolicy.FLAG_WAKE) != 0|| event.isWakeKey();//是否是唤醒按键,此时是falseif (interactive || (isInjected && !isWakeKey)) {//此处走这里,不是唤醒,且正在和用户交互result = ACTION_PASS_TO_USER;//发送给应用程序isWakeKey = false;if (interactive) {if (keyCode == mPendingWakeKey && !down) {result = 0;}// Reset the pending keymPendingWakeKey = PENDING_KEY_NULL;}}// 此处主要处理一些特殊按键,仅列出电源按键switch (keyCode) {/*case KeyEvent.KEYCODE_POWER: {// Any activity on the power button stops the accessibility shortcutcancelPendingAccessibilityShortcutAction();result &= ~ACTION_PASS_TO_USER;//可以看出如果是电源按键,则会取消派发给应用程序的ACTION_PASS_TO_USER标志isWakeKey = false; // wake-up will be handled separatelyif (down) {interceptPowerKeyDown(event, interactive);} else {interceptPowerKeyUp(event, interactive, canceled);}break;}*/}return result;//返回派发策略,此时是派发给应用程序}

 2.25 NativeInputManager::handleInterceptActions

1.主要是添加发送给应用的flag标志

void NativeInputManager::handleInterceptActions(jint wmActions, nsecs_t when,uint32_t& policyFlags) {if (wmActions & WM_ACTION_PASS_TO_USER) {//主要是给policyFlags添加派发给应用的flagpolicyFlags |= POLICY_FLAG_PASS_TO_USER;} else {}
}

 2.26 InputDispatcher::enqueueInboundEventLocked

此函数的主要作用是:

1.将EventEntry类型的消息放入队尾。

2.然后判断其是不是home按键,甚至最长相应home按键抬起的时间是0.5秒。因此假设应用卡住,那么用户会疯狂的按home按键,请求推出,如果home按键的响应时间过长,则用户体验会非常的差,因此必须在0.5秒内响应home按键。

bool InputDispatcher::enqueueInboundEventLocked(EventEntry* entry) {bool needWake = mInboundQueue.isEmpty();mInboundQueue.enqueueAtTail(entry);//将该事件放入mInboundQueue队列尾部traceInboundQueueLengthLocked();//追踪队列的长度switch (entry->type) {case EventEntry::TYPE_KEY: {//为HOME键或其他窗口上的点击事件提高响应速度的优化操作,这些所谓的优化操作可能会导致派发队列之前 //的所有事件被丢弃KeyEntry* keyEntry = static_cast<KeyEntry*>(entry);if (isAppSwitchKeyEventLocked(keyEntry)) {//判断是否是home键,如果是if (keyEntry->action == AKEY_EVENT_ACTION_DOWN) {//如果是home按键按下,则设置mAppSwitchSawKeyDown为truemAppSwitchSawKeyDown = true;} else if (keyEntry->action == AKEY_EVENT_ACTION_UP) {//如果是home按键抬起,则设置最迟响应home键的时间。if (mAppSwitchSawKeyDown) {mAppSwitchDueTime = keyEntry->eventTime + APP_SWITCH_TIMEOUT;//设置mAppSwitchDueTime时间//mAppSwitchDueTime代表了最近发生窗口切换操作(按home键)的最迟发送时间。APP_SWITCH_TIMEOUT值是0.5秒mAppSwitchSawKeyDown = false;needWake = true;//需要立即唤醒inputdispatcher线程去分发}}}break;}}return needWake;
}
bool InputDispatcher::isAppSwitchKeyEventLocked(KeyEntry* keyEntry) {return ! (keyEntry->flags & AKEY_EVENT_FLAG_CANCELED)&& isAppSwitchKeyCode(keyEntry->keyCode)&& (keyEntry->policyFlags & POLICY_FLAG_TRUSTED)&& (keyEntry->policyFlags & POLICY_FLAG_PASS_TO_USER);
}bool InputDispatcher::isAppSwitchKeyCode(int32_t keyCode) {return keyCode == AKEYCODE_HOME //home按键|| keyCode == AKEYCODE_ENDCALL //挂机按键|| keyCode == AKEYCODE_APP_SWITCH; //app切换按键
}

 此时是按键按下事件的处理,接下来便是InputDispatcher线程篇的流程,请看【android 9】【input】【8.发送按键事件2——InputDispatcher线程】

我们继续看看按键抬起的事件处理。此时我们需要回到2.18节

 2.27 NotifyKeyArgs::notify(抬起)

此时查看第二个抬起事件是如何处理的。其实抬起按键的处理和按下按键的处理基本相似,即从2.19----2.26节的流程,故此处不再赘述

void NotifyKeyArgs::notify(const sp<InputListenerInterface>& listener) const {listener->notifyKey(this);//此处listener是InputDispatcher类对象,this是调用者也就是一个NotifyKeyArgs对象
}

此篇我们主要介绍了普通按键事件的inputreader线程是如何处理的,下一篇我将描述

InputDispatcher线程是如何派发处理的。

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

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

相关文章

丢失的数字 ---- 位运算

题目链接 题目: 分析: 解法一: 哈希表解法二: 高斯求和解法三:位运算 异或运算根据运算的性质, 相同的两个a异或 0 以示例一为例: 数组中有0,1,3, 缺失的数字是2, 那么只要我们将数组与0,1,2,3 异或, 就会得到2 代码: class Solution {public int missingNumber(int[] num…

【Redis】 Java操作客户端命令——集合操作与有序集合操作

文章目录 &#x1f343;前言&#x1f333;集合操作&#x1f6a9;sadd 和 smembers&#x1f6a9;srem 和 sismember&#x1f6a9;scard&#x1f6a9;sinter&#x1f6a9;sunion&#x1f6a9;sdiff &#x1f332;有序集合操作&#x1f6a9;zadd 和 zrange&#x1f6a9;zrem 和 zc…

拖拽tableView

拖拽tableView&#xff0c;随手指移动&#xff0c;插入。demo地址github

JAVAEE之网络初识_协议、TCP/IP网络模型、封装、分用

前言 在这一节我们简单介绍一下网络的发展 一、通信网络基础 网络互连的目的是进行网络通信&#xff0c;也即是网络数据传输&#xff0c;更具体一点&#xff0c;是网络主机中的不同进程间&#xff0c;基于网络传输数据。那么&#xff0c;在组建的网络中&#xff0c;如何判断到…

迪丽热巴与大姐的璀璨友情

迪丽热巴与“大姐”的璀璨友情&#xff1a;星光熠熠&#xff0c;友谊长存在娱乐圈的繁华舞台上&#xff0c;有两位耀眼的女星&#xff0c;她们如同夜空中亮的两颗星&#xff0c;交相辉映&#xff0c;共同谱写着一段段动人的佳话。她们&#xff0c;一个是被亲切称为“迪迪”的迪…

HarmonyOS-9(stage模式)

配置文件 {"module": {"requestPermissions": [ //权限{"name": "ohos.permission.EXECUTE_INSIGHT_INTENT"}],"name": "entry", //模块的名称"type": "entry", //模块类型 :ability类型和…

AWR设置工程仿真频率、原理图仿真频率、默认单位

AWR设置工程仿真频率、原理图仿真频率、默认单位 生活不易&#xff0c;喵喵叹气。马上就要上班了&#xff0c;公司的ADS的版权紧缺&#xff0c;主要用的软件都是NI 的AWR&#xff0c;只能趁着现在没事做先学习一下子了&#xff0c;希望不要裁我。 最近稍微学习了一下AWR这个软…

UMG绝对坐标与局部空间

在 Unreal Engine 的 UMG&#xff08;Unreal Motion Graphics&#xff09;中&#xff0c;“绝对坐标”和“局部空间”是两个常见的概念&#xff0c;主要用于描述 UI 元素的位置和大小。 概念与区别 绝对坐标&#xff08;Absolute Coordinates&#xff09;&#xff1a;这是指相…

list~模拟实现

目录 list的介绍及使用 list的底层结构 节点类的实现 list的实现 构造函数 拷贝构造 方法一&#xff1a;方法二&#xff1a; 析构函数 赋值重载 insert / erase push_/pop_(尾插/尾删/头插/头删) begin和end&#xff08;在已建立迭代器的基础上&#xff09; 迭代…

kafka命令--简单粗暴有效

zookeeper bin目录下执行 启动&#xff1a;./zkServer.sh start 停止&#xff1a;./zkServer.sh stop 重启&#xff1a;./zkServer.sh restart 状态&#xff1a;./zkServer.sh status kafka bin目录下执行 启动&#xff1a;./kafka-server-start.sh -daemon …/config/server.…

直播预告|手把手教你玩转 Milvus Lite !

Milvus Lite&#xff08;https://milvus.io/docs/milvus_lite.md&#xff09;是一个轻量级向量数据库&#xff0c;支持本地运行&#xff0c;可用于搭建 Python 应用&#xff0c;由 Zilliz 基于全球最受欢迎的开源向量数据库 Milvus&#xff08;https://milvus.io/intro&#xf…

使用python优雅的将PDF转为Word

使用python优雅的将PDF转为Word 先装这个优雅的库 pip install pdf2docx然后运行下面优雅的代码&#xff0c;将pdf路径和docx路径修改 from pdf2docx import Converter # path pdf_file C:\\Users\\phl\\Desktop\\软件工程期末\\软件工程模拟试题5.pdf docx_file C:\\User…

【iOS】UI学习——导航控制器、分栏控制器

UI学习&#xff08;三&#xff09; 导航控制器导航控制器基础导航控制器切换导航栏和工具栏 分栏控制器分栏控制器基础分栏控制器高级 导航控制器 导航控制器负责控制导航栏&#xff08;navigationBar&#xff09;&#xff0c;导航栏上的按钮叫UINavigationItem&#xff08;导航…

【STL源码剖析】deque 的使用

别院深深夏席清&#xff0c;石榴开遍透帘明。 树阴满地日当午&#xff0c;梦觉流莺时一声。 目录 deque 的结构 deque 的迭代器剖析 deque 的使用 ​编辑 deque 的初始化 deque 的容量操作 deque 的访问操作 在 pos 位置插入另一个向量的 [forst&#xff0c;last] 间的数据​编…

【人工智能Ⅱ】实验9:强化学习Q-Learning算法

实验9&#xff1a;强化学习Q-Learning算法 一&#xff1a;实验目的 1&#xff1a;了解强化学习的基本概念。 2&#xff1a;学习强化学习经典算法——Q-Learing算法。 3&#xff1a;通过Q-Learing算法解决问题。 二&#xff1a;实验内容 2.1 强化学习 强化学习&#xff08;…

iOS18新功能大爆料,打破常规,全面升级,这些变化不容错过!

众所周知&#xff0c;苹果 iOS 操作系统近年来都没有发生重大变化&#xff0c;主要是添加小部件、锁屏编辑和手机屏幕编辑等功能&#xff0c;再加上bug偏多&#xff0c;以至于越来越多iPhone用户不愿意再升级系统了。这一点&#xff0c;从 iOS 17 明显降低的安装率中就能看出一…

对人脸图像进行性别和年龄的判断

判断性别和年龄 导入必要的库加载预训练的人脸检测模型加载预训练的性别和年龄识别模型定义性别和年龄的标签列表创建Tkinter窗口&#xff1a;定义选择图片的函数&#xff1a;创建一个按钮&#xff0c;用于打开文件选择对话框定义显示图片的函数创建预测性别和年龄的函数创建预…

Docker大学生看了都会系列(二、Mac通过Homebrew安装Docker)

系列文章目录 第一章 Docker介绍 第二章 Mac通过Homebrew安装Docker 文章目录 前言Mac通过Homebrew安装本机环境系统要求terminal命令安装查看安装信息配置阿里云镜像加速登陆阿里云配置加速地址其他国内加速地址 总结 前言 在上一章了解了Docker容器是什么之后&#xff0c;本…

solidworks二维样条曲线使用实例

单位mm 绘制一个圆 直径为50mm&#xff0c; 基准面 上视基准面&#xff0c;距离50mm&#xff0c; 2个六边形 一个内嵌圆 另一个直径60mm&#xff0c; 将两个六边形改成构造线 选择样条曲线&#xff0c;将六边形的顶点连接在一起 放样曲面 插入–曲面–放样曲面 平面区域…

RabbitMQ不完整的笔记

同步的不足 1、拓展性差&#xff0c;当要添加功能时&#xff0c;需要在原来的功能代码上做修改&#xff0c;高耦合。 2、性能下降&#xff0c;调用者需要等待服务提供者执行完返回结果后&#xff0c;才能继续向下执行 3、级联失败&#xff0c;由于我们是基于OpenFeign调用交易…