【android 9】【input】【8.发送按键事件2——InputDispatcher线程】

系列文章目录

本人系列文章-CSDN博客


目录

系列文章目录

1.简介

1.1流程介绍

1.2 时序图

2.普通按键消息发送部分源码分析(按键按下事件)

2.1 开机后分发线程阻塞的地方

 2.2 InputDispatcher::dispatchOnceInnerLocked

 2.3 InputDispatcher::dispatchKeyLocked

 2.4 InputDispatcher::dispatchOnce

2.5 runCommandsLockedInterruptible

 2.6 InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible

 2.7 InputDispatcher::dispatchOnce

 2.8 InputDispatcher::dispatchKeyLocked

 2.9 InputDispatcher::findFocusedWindowTargetsLocked

2.10 dispatchEventLocked

 2.11 prepareDispatchCycleLocked

 2.12 enqueueDispatchEntriesLocked

2.13  enqueueDispatchEntryLocked

 2.14 startDispatchCycleLocked

2.15 InputPublisher::publishKeyEvent

 2.16 InputChannel::sendMessage


1.简介

从上一篇幅我们知道了,普通按键事件在inputreader线程中会将按下事件和抬起事件放入mInboundQueue队列中,然后唤醒InputDispatcher线程去进行派发。本篇我们便来介绍一下InputDispatcher线程是如何派发的。

1.1流程介绍

1.当InputDispatcher线程被唤醒时,第一次执行dispatchOnce时,会生成一个去查询按键事件派发策略的命令并放入命令队列中,然后开始第二次循环。

2.在第二次循环的时候,会从命令队列中取出命令,然后通过jni走到IMS,再走到WMS,再走到phonewindowMnager中询问此按键的派发策略。然后开始第三次循环。

3.如果询问的按键派发策略是立即派发,则会寻找焦点窗口。

4.查找到合适的目标窗口后,会从保存窗口信息的容器中取出窗口信息,窗口信息保存着socket的然后通过sokcet对,将按键消息从IMS发送到应用程序端。

此sokcet对是应用在创建的时候,会调用WMS的addView函数,此函数会创建两个socket对,其中一个fd通过binder通信返回给应用端,应用端会将此fd监听查看是否有消息,另外一个会传递给IMS,IMS也会监听其fd是否有消息。

1.2 时序图

 (图片可保存到本地放大观看)

2.普通按键消息发送部分源码分析(按键按下事件)

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

2.1 开机后分发线程阻塞的地方

void InputDispatcher::dispatchOnce() {
    nsecs_t nextWakeupTime = LONG_LONG_MAX;//下一次唤醒该线程的时间,也可以说是下一次线程循环的执行时间点
    { 
        
        mDispatcherIsAliveCondition.broadcast();//分发线程处于活跃状态,进行广播
		
        if (!haveCommandsLocked()) {//此时无命令。作用:检查inputdispatcher的缓存队列中是否有还未处理的命令,
		//只有无命令时才会执行dispatchOnceInnerLocked方法,进行事件的分发。
		
            dispatchOnceInnerLocked(&nextWakeupTime);// 传入的nextWakeupTime决定了下次派发线程循环的执行时间点,
        }

        if (runCommandsLockedInterruptible()) {//如果此时缓存队列中有命令,则立即执行该命令,
		//并将下一次线程循环的执行时间点设置为LONG_LONG_MIN,将使派发线程立刻开始下次线程。
		//最小值定义为0,最大值是unsigned long long的最大值:1844674407370955161
            nextWakeupTime = LONG_LONG_MIN;
        }
    } 

    nsecs_t currentTime = now();//获取当前时间点
    int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);//下一次唤醒时间点-当前时间点=线程休眠时间
    mLooper->pollOnce(timeoutMillis);//调用pollOnce使线程休眠,等待回调,唤醒或超时。
	//Looper的pollOnce()的实质就是epoll_wait()。 因此派发线程的休眠在三种情况下可能被唤醒:
	1.调用Looper::wake()函数主动唤醒(有输入事件注入派发队列中时),
	2.到达nextWakeupTime的事件点时唤醒
	3.epoll_waitepoll_wait()监听的fd有epoll_event发生时唤醒(这种唤醒方式将在介绍ANR机制时讨论)。
}

从上一篇我们知道,当 inputreader线程向mInboundQueue中每次插入消息后,都会调用wake来唤醒分发线程。我们首先看一下按键按下的事件分发流程。

 2.2 InputDispatcher::dispatchOnceInnerLocked

此函数的主要作用是:

1.取出消息,然后通过按键的派发策略,和时间等生成对应的分发策略,默认为不丢弃。

2.然后再调用dispatchKeyLocked进行事件的派发。

void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    nsecs_t currentTime = now();

	//如果没有待发送的事件,则从mInboundQueue队列中取出一个事件,进行分发
    if (! mPendingEvent) {

       }
		
		else {
            //从派发队列中将位于队首的一条EventEntry取出并保存在mPendingEvent成员变量中。 
            // mPendingEvent表示处于派发过程中的一个输入事件
            mPendingEvent = mInboundQueue.dequeueAtHead();
            traceInboundQueueLengthLocked();//更新一下队列长度
        }

        
        if (mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER) //设备消息类型为发送给应用程序
		{//判断此key是否需要发送到应用程序。
            pokeUserActivityLocked(mPendingEvent);//用来决定是否要唤醒设备或者点亮屏幕,最终调用的是PowerManagerService。
        }
		

        
        resetANRTimeoutsLocked();//重置ANR超时时间
    }


    bool done = false;
    DropReason dropReason = DROP_REASON_NOT_DROPPED;//默认丢弃原因为不丢弃
	

    switch (mPendingEvent->type) {

    case EventEntry::TYPE_KEY: {//按键类型
        KeyEntry* typedEntry = static_cast<KeyEntry*>(mPendingEvent);
		
        done = dispatchKeyLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);//进一步分发按键事件,结果赋值给 done
          // 无论是成功派发还是事件被丢弃,都返回 true,当要询问key派发策略时是false
        break;
    }

    default:
        break;
    }

    if (done) {//无论是成功派发还是事件被丢弃,都返回 true,当要询问key派发策略时是false

        mLastDropReason = dropReason;

        releasePendingEventLocked();
        *nextWakeupTime = LONG_LONG_MIN;  // force next poll to wake up immediately
    }
}

 2.3 InputDispatcher::dispatchKeyLocked

此函数的作用是:

1.生成一个commond命令,并入队命令队列,询问此按键按下事件的分发策略。注意,一共是有两个策略,一个是派发策略,派发策略主要是询问是否是系统消费,是否要派发给应用程序等,而此时的分发策略是询问是否丢弃派发,是否等会派发等(如组合按键)

2.终止此时按键事件的派发,开启下一轮循环,先询问按键的分发策略,再派发此按键按下事件。

//此次派发会生成一个commond命令,并入队。去询问key的派发策略,因此此key事件咱未派发。commandEntry->keyEntry = entry;中保存了此次要派发的entry的信息。
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, KeyEntry* entry,DropReason* dropReason, nsecs_t* nextWakeupTime) {
    // Preprocessing.
    if (! entry->dispatchInProgress) {//dispatchInProgress默认为false,表示事件是否正在派发

        entry->dispatchInProgress = true;//设置事件为正在派发中
    }


    // 给policy一个机会,去截断key.
	//如果事件的interceptKeyResult是不知道,但是事件的policyFlags是要发送到应用程序的,则生成一个CommandEntry,然后返回false,等待命令的执行
	//如果事件的interceptKeyResult是不知道,事件不是发送给应用程序的,则设置falg为INTERCEPT_KEY_RESULT_CONTINUE。
	//如果事件的interceptKeyResult是INTERCEPT_KEY_RESULT_SKIP,表示跳过此事件,则设置其丢弃原因为DROP_REASON_POLICY
	
    if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_UNKNOWN) {//初始化的时候值是INTERCEPT_KEY_RESULT_UNKNOWN
	//如果此事件尚未进行过派发策略查询,则通过发送一个命令的方式查询派发策路 
        if (entry->policyFlags & POLICY_FLAG_PASS_TO_USER) {
            CommandEntry* commandEntry = postCommandLocked(//生成了一个commandEntry
                    & InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible);

			
            commandEntry->keyEntry = entry;
            entry->refCount += 1;
            return false; // 终止此次派发,开始下一轮的循环等待策略查询完成
        }
	}
}
InputDispatcher::CommandEntry* InputDispatcher::postCommandLocked(Command command) {
    CommandEntry* commandEntry = new CommandEntry(command);
    mCommandQueue.enqueueAtTail(commandEntry);//入队命令队列
    return commandEntry;
}

 此时我们回到 2.2 InputDispatcher::dispatchOnceInnerLocked章节,所以我们可以看出来,第一次派发的按键时,要询问该按键的派发策略,所以done的值是flase

//此时回到上面928行的这个函数,注意此时命令队列中存在一条询问key事件派发策略的命令。
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
        done = dispatchKeyLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);//进一步分发按键事件,结果赋值给 done
          // 无论是成功派发还是事件被丢弃,都返回 true,当要询问key派发策略时是false
	/**
	//此时是false,
    if (done) {
        if (dropReason != DROP_REASON_NOT_DROPPED) {
            dropInboundEventLocked(mPendingEvent, dropReason);
        }
        mLastDropReason = dropReason;

        releasePendingEventLocked();
        *nextWakeupTime = LONG_LONG_MIN;  // force next poll to wake up immediately
    }
	*/
}

 2.4 InputDispatcher::dispatchOnce

然后开启了下一次循环,此时命令队列中存在查询按键按下的分发策略的命令,所以执行runCommandsLockedInterruptible函数

void InputDispatcher::dispatchOnce() {
    nsecs_t nextWakeupTime = LONG_LONG_MAX;//下一次唤醒该线程的时间,也可以说是下一次线程循环的执行时间点
    { 
        AutoMutex _l(mLock);
        mDispatcherIsAliveCondition.broadcast();//分发线程处于活跃状态,进行广播
		
		
		/*
        if (!haveCommandsLocked()) {//此时有命令。作用:检查inputdispatcher的缓存队列中是否有还未处理的命令,
		//只有无命令时才会执行dispatchOnceInnerLocked方法,进行事件的分发。
		
            dispatchOnceInnerLocked(&nextWakeupTime);// 传入的nextWakeupTime决定了下次派发线程循环的执行时间点,
        }*/

        if (runCommandsLockedInterruptible()) {//如果此时缓存队列中有一条询问key事件派发策略的命令,则立即执行该命令,
		//并将下一次线程循环的执行时间点设置为LONG_LONG_MIN,将使派发线程立刻开始下次线程。
		//最小值定义为0,最大值是unsigned long long的最大值:1844674407370955161
            nextWakeupTime = LONG_LONG_MIN;//立刻下次唤醒。
        }
    } // release lock

    nsecs_t currentTime = now();//获取当前时间点
    int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);//下一次唤醒时间点-当前时间点=线程休眠时间
    mLooper->pollOnce(timeoutMillis);//调用pollOnce使线程休眠,等待回调,唤醒或超时。
	//Looper的pollOnce()的实质就是epoll_wait()。 因此派发线程的休眠在三种情况下可能被唤醒:
	1.调用Looper::wake()函数主动唤醒(有输入事件注入派发队列中时),
	2.到达nextWakeupTime的事件点时唤醒
	3.epoll_waitepoll_wait()监听的fd有epoll_event发生时唤醒(这种唤醒方式将在介绍ANR机制时讨论)。
}

2.5 runCommandsLockedInterruptible

此函数的主要作用是:

1.取出命令,执行doInterceptKeyBeforeDispatchingLockedInterruptible函数,查询分发策略。

bool InputDispatcher::runCommandsLockedInterruptible() {

    do {
        CommandEntry* commandEntry = mCommandQueue.dequeueAtHead();//取出命令

        Command command = commandEntry->command;
        (this->*command)(commandEntry); // 执行doInterceptKeyBeforeDispatchingLockedInterruptible函数,并传入commandEntry作为参数

        commandEntry->connection.clear();//清除connection
        delete commandEntry;
    } while (! mCommandQueue.isEmpty());
    return true;
}

 2.6 InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible

此处简单介绍一下:

会先通过JNI走到IMS中,IMS会调用到WMS中,最终会调用到phoneWindowManager的interceptKeyBeforeDispatching函数,此函数内部会返回delay。

1.正常应用获取焦点时(即应用在前台交互),那么事件则可以继续派发。

2.比如我们现在按下了手机的音量按键,有可能存在接下来按键是电源按键,则存在是组合按键实现截屏的功能,因此现在系统也不知道此时是控制音量,还是截屏,所以需要等一会再询问此按键派发策略。

3.如果此时系统或者焦点(即应用程序没有焦点),则派发给应用是无效的,所以该按键丢弃。

//向mPolicy询问,
//如果delay小于0,则设置为INTERCEPT_KEY_RESULT_SKIP,代表丢弃该事件
//如果delay=0,则设置为INTERCEPT_KEY_RESULT_CONTINUE,代表事件可以继续派发
//如果delay>0,则设置为INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER,并设置interceptKeyWakeupTime,表示等会再询问派发策略
void InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible(
        CommandEntry* commandEntry) {
    KeyEntry* entry = commandEntry->keyEntry;//从命令队列中取出key entry事件

    KeyEvent event;
    initializeKeyEvent(&event, entry);

    mLock.unlock();

    android::base::Timer t;
    nsecs_t delay = mPolicy->interceptKeyBeforeDispatching(commandEntry->inputWindowHandle,&event, entry->policyFlags);//此时应该是空
    if (t.duration() > SLOW_INTERCEPTION_THRESHOLD) {
        ALOGW("Excessive delay in interceptKeyBeforeDispatching; took %s ms",
                std::to_string(t.duration().count()).c_str());
    }

    mLock.lock();

    if (delay < 0) {//如果delay小于0,则设置为INTERCEPT_KEY_RESULT_SKIP//代表丢弃该事件
        entry->interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_SKIP;
    } else if (!delay) {如果delay=0,则设置为INTERCEPT_KEY_RESULT_CONTINUE//代表事件可以继续派发
        entry->interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_CONTINUE;
    } else {//如果delay>0,则设置为INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER,表示等会再询问派发策略,并设置interceptKeyWakeupTime
        entry->interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER;
        entry->interceptKeyWakeupTime = now() + delay;
    }
    entry->release();
}

 2.7 InputDispatcher::dispatchOnce

然后会询问完按键按下事件的派发策略后,会立刻唤醒派发循环,开始派发此按键按下的事件。

//执行查询派发key事件策略的命令后,会立即执行下一次循环。
void InputDispatcher::dispatchOnce() {
    nsecs_t nextWakeupTime = LONG_LONG_MAX;//下一次唤醒该线程的时间,也可以说是下一次线程循环的执行时间点
    { 
        AutoMutex _l(mLock);
        mDispatcherIsAliveCondition.broadcast();//分发线程处于活跃状态,进行广播
		
        if (!haveCommandsLocked()) {//作用:检查inputdispatcher的缓存队列中是否有还未处理的命令
		//只有无命令时才会执行dispatchOnceInnerLocked方法,进行事件的分发。
		
            dispatchOnceInnerLocked(&nextWakeupTime);// 传入的nextWakeupTime决定了下次派发线程循环的执行时间点
        }

        
    } // release lock

    nsecs_t currentTime = now();//获取当前时间点
    int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);//下一次唤醒时间点-当前时间点=线程休眠时间
    mLooper->pollOnce(timeoutMillis);//调用pollOnce使线程休眠,等待回调,唤醒或超时。
	//Looper的pollOnce()的实质就是epoll_wait()。 因此派发线程的休眠在三种情况下可能被唤醒:
	//1.调用Looper::wake()函数主动唤醒(有输入事件注入派发队列中时),
	//2.到达nextWakeupTime的事件点时唤醒
	//3.epoll_waitepoll_wait()监听的fd有epoll_event发生时唤醒(这种唤醒方式将在介绍ANR机制时讨论)。
}

此时我们继续查看dispatchOnceInnerLocked函数

void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    nsecs_t currentTime = now();

	//此时不为空,此时mPendingEvent是之前key按下事件,在上一次返回flse时候,没有释放此事件
    if (! mPendingEvent) {//如果没有待发送的事件,则从mInboundQueue队列中取出一个事件,进行分发
    }

    bool done = false;
    DropReason dropReason = DROP_REASON_NOT_DROPPED;//默认丢弃原因为不丢弃

    switch (mPendingEvent->type) {

    case EventEntry::TYPE_KEY: {//按键类型
        KeyEntry* typedEntry = static_cast<KeyEntry*>(mPendingEvent);
		
        done = dispatchKeyLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);//进一步分发按键事件,结果赋值给 done
          // 无论是成功派发还是事件被丢弃,都返回 true,当要询问key派发策略或者此事件没有处理时是false
        break;
    }

    if (done) {
        if (dropReason != DROP_REASON_NOT_DROPPED) {
            dropInboundEventLocked(mPendingEvent, dropReason);
        }
        mLastDropReason = dropReason;

        releasePendingEventLocked();
        *nextWakeupTime = LONG_LONG_MIN;  // force next poll to wake up immediately
    }
}

 2.8 InputDispatcher::dispatchKeyLocked

此函数的主要作用是:

1.通过findFocusedWindowTargetsLocked从保存窗口信息的列表中,找到合适的派发窗口。

2.dispatchEventLocked向指定窗口派发按键按下的事件。

bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, KeyEntry* entry,
        DropReason* dropReason, nsecs_t* nextWakeupTime) {
    // Preprocessing.
    if (! entry->dispatchInProgress) {//此时dispatchInProgress为true
    }
    
    Vector<InputTarget> inputTargets;//inputTargets是一个容器,会存储目标窗口的信息,根据key事件的类型,寻找合适的目标窗口。
	//其返回值injectionResult指明寻找结果,而找到的合适的目标窗口信息将被保存在inputTargets列表中
    int32_t injectionResult = findFocusedWindowTargetsLocked(currentTime,
            entry, inputTargets, nextWakeupTime);

    setInjectionResultLocked(entry, injectionResult);//将injectionResult结果赋值到event内部的一个属性中

    ///addMonitoringTargetsLocked(inputTargets);//默认mMonitoringChannels是false

    // Dispatch the key.
    dispatchEventLocked(currentTime, entry, inputTargets);
    return true;
}

 2.9 InputDispatcher::findFocusedWindowTargetsLocked

按键的派发相较于motion事件的派发比较简单,按键事件的派发只需要派发给获取焦点的应用程序即可,获取焦点的应用程序即:用户正在交互使用的应用程序。

此函数的作用是:

1.检查目标窗口的权限和是否已经准备就绪。

2.当窗口准备好了以后,addWindowTargetLocked会将焦点窗口的相关信息保存到inputTargets中。

那么此时读者会疑问,焦点窗口是什么时候被设置到input中的,此篇仅简单介绍一下:

当窗口发生变化时,WMS会调用InputMonitor类的updateInputWindowsLw函数,而InputMonitor类是IMS的回调类,于是便通过jni更新窗口信息到InputDispatcher中。

int32_t InputDispatcher::findFocusedWindowTargetsLocked(nsecs_t currentTime,
        const EventEntry* entry, Vector<InputTarget>& inputTargets, nsecs_t* nextWakeupTime) {
    int32_t injectionResult;
    std::string reason;


    // 如果有焦点窗口,则检查权限,权限检查失败,则
    if (! checkInjectionPermission(mFocusedWindowHandle, entry->injectionState)) {
        injectionResult = INPUT_EVENT_INJECTION_PERMISSION_DENIED;
        goto Failed;
    }

    // 检查窗口是否准备好进行更多输入事件
    reason = checkWindowReadyForMoreInputLocked(currentTime,
            mFocusedWindowHandle, entry, "focused");
    if (!reason.empty()) {//如果reason不为空,则可能发生了anr,用handleTargetsNotReadyLocked去判断
        injectionResult = handleTargetsNotReadyLocked(currentTime, entry,
                mFocusedApplicationHandle, mFocusedWindowHandle, nextWakeupTime, reason.c_str());
        goto Unresponsive;
    }

    //代表此时有焦点窗口,并且权限通过,并且准备好接受输入事件
    injectionResult = INPUT_EVENT_INJECTION_SUCCEEDED;//给flag赋值
    addWindowTargetLocked(mFocusedWindowHandle,
            InputTarget::FLAG_FOREGROUND | InputTarget::FLAG_DISPATCH_AS_IS, BitSet32(0),
            inputTargets);//addWindowTargetLocked 函数将接收按键事件的目标窗口的一些信息保存到 inputTargets容器中
			//参数分析:
			//mFocusedWindowHandle是焦点窗口
			//InputTarget::FLAG_FOREGROUND | InputTarget::FLAG_DISPATCH_AS_IS代表,是前台窗口和必须原样发送事件
			//0
			//inputTargets保存目标窗口的信息的容器,此时还是空
		

    // Done.
Failed:
Unresponsive:
    nsecs_t timeSpentWaitingForApplication = getTimeSpentWaitingForApplicationLocked(currentTime);
	//timeSpentWaitingForApplication是等待应用程序花费的时间
    updateDispatchStatisticsLocked(currentTime, entry,
            injectionResult, timeSpentWaitingForApplication);

    return injectionResult;
}
bool InputDispatcher::checkInjectionPermission(const sp<InputWindowHandle>& windowHandle,
        const InjectionState* injectionState) {
	/*		
    if (injectionState//injectionState代表是否是从IMS注入的事件,默认物理按键是null
            && (windowHandle == NULL
                    || windowHandle->getInfo()->ownerUid != injectionState->injectorUid)
            && !hasInjectionPermission(injectionState->injectorPid, injectionState->injectorUid)) {
        if (windowHandle != NULL) {
            ALOGW("Permission denied: injecting event from pid %d uid %d to window %s "
                    "owned by uid %d",
                    injectionState->injectorPid, injectionState->injectorUid,
                    windowHandle->getName().c_str(),
                    windowHandle->getInfo()->ownerUid);
        } else {
            ALOGW("Permission denied: injecting event from pid %d uid %d",
                    injectionState->injectorPid, injectionState->injectorUid);
        }
        return false;
    }
	*/
    return true;
}
//从windowHandle的句柄中获取getInfo,然后将其inputchannel等信息生成InputTarget类对象,然后保存到inputTargets容器中
void InputDispatcher::addWindowTargetLocked(const sp<InputWindowHandle>& windowHandle,
        int32_t targetFlags, BitSet32 pointerIds, Vector<InputTarget>& inputTargets) {
    inputTargets.push();

    const InputWindowInfo* windowInfo = windowHandle->getInfo();
    InputTarget& target = inputTargets.editTop();
    target.inputChannel = windowInfo->inputChannel;//获取发送窗口的通道
    target.flags = targetFlags;//此时按键的flag是InputTarget::FLAG_FOREGROUND | InputTarget::FLAG_DISPATCH_AS_IS
    target.xOffset = - windowInfo->frameLeft;//窗口坐标的左
    target.yOffset = - windowInfo->frameTop;窗口坐标的顶部
    target.scaleFactor = windowInfo->scaleFactor;//窗口的缩放系数
    target.pointerIds = pointerIds;//pointerIds是0
}

2.10 dispatchEventLocked

void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,EventEntry* eventEntry, const Vector<InputTarget>& inputTargets) {

    pokeUserActivityLocked(eventEntry);//保持屏幕唤醒

    for (size_t i = 0; i < inputTargets.size(); i++) {//此时inputTargets中只有一个焦点窗口
        const InputTarget& inputTarget = inputTargets.itemAt(i);

        ssize_t connectionIndex = getConnectionIndexLocked(inputTarget.inputChannel);//此函数的作用是检查此窗口的inputchannel是否已注册,
		//如果mConnectionsByFd中存在对应的connection,则返回索引
        if (connectionIndex >= 0) {
            sp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex);//获取此窗口的connection类的对象
            prepareDispatchCycleLocked(currentTime, connection, eventEntry, &inputTarget);
        }
    }
}

 2.11 prepareDispatchCycleLocked

void InputDispatcher::prepareDispatchCycleLocked(nsecs_t currentTime,const sp<Connection> &connection, EventEntry *eventEntry, const InputTarget *inputTarget)
{

        //不分割,按照原样事件进行入队分发
        enqueueDispatchEntriesLocked(currentTime, connection, eventEntry, inputTarget);
}

 2.12 enqueueDispatchEntriesLocked

void InputDispatcher::enqueueDispatchEntriesLocked(nsecs_t currentTime,const sp<Connection> &connection, EventEntry *eventEntry, const InputTarget *inputTarget)
{
        bool wasEmpty = connection->outboundQueue.isEmpty();//此时是空

        // 调用了六次 enqueueDispatchEntryLocked 函数,仅仅最后一个参数不同,但不是执行六次,
		//而是根据inputTarget的flags和最后一个参数是否相等,如果相等则处理,不相等会返回。
		/**
        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);
		//key事件的flags是InputTarget::FLAG_FOREGROUND | InputTarget::FLAG_DISPATCH_AS_IS,
		//如果是key事件,则只有这个函数执行了,此函数看名字感觉是将entry入队
		/**
        enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
                                   InputTarget::FLAG_DISPATCH_AS_SLIPPERY_EXIT);
        enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
                                   InputTarget::FLAG_DISPATCH_AS_SLIPPERY_ENTER);
		*/

        // 当原先的 outbound 队列为空, 且当前 outbound 不为空的情况执行,开始循环分发
        if (wasEmpty && !connection->outboundQueue.isEmpty())
        {
            startDispatchCycleLocked(currentTime, connection);
        }
}

2.13  enqueueDispatchEntryLocked

此函数的作用是:

1.将EventEntry 保存到 DispatchEntry中。

2.判断按键是否是抬起按键,如果按键是抬起按键,则判断之前此按键是否按下过,如果没有,则删除此抬起按键。

3.将DispatchEntry按键事件放入outboundQueue的队列中。

void InputDispatcher::enqueueDispatchEntryLocked(const sp<Connection> &connection, EventEntry *eventEntry, const InputTarget *inputTarget,int32_t dispatchMode)
{
        int32_t inputTargetFlags = inputTarget->flags;//此时的获取的flag是InputTarget::FLAG_FOREGROUND | InputTarget::FLAG_DISPATCH_AS_IS
        inputTargetFlags = (inputTargetFlags & ~InputTarget::FLAG_DISPATCH_MASK) | dispatchMode;
		//FLAG_DISPATCH_MASK是所有的flag集合,dispatchMode此时传入的是原样发送

		//这是一个新的事件。
		//将 EventEntry 转换为 DispatchEntry, 在后面会加入 connection 的 outbound 队列
        DispatchEntry *dispatchEntry = new DispatchEntry(eventEntry, 
                                                         inputTargetFlags, inputTarget->xOffset, inputTarget->yOffset,
                                                         inputTarget->scaleFactor);

        //应用target标志并更新连接的输入状态。
        switch (eventEntry->type)
        {
        case EventEntry::TYPE_KEY:
        {
            KeyEntry *keyEntry = static_cast<KeyEntry *>(eventEntry);
            dispatchEntry->resolvedAction = keyEntry->action;//从keyEntry中设置其action和flag
            dispatchEntry->resolvedFlags = keyEntry->flags;
			//此函数的作用主要是检查一个抬起的按键之前是否有按下过,和将按下的按键添加到一个容器中
			//返回值为ture,代表是抬起的按键,之前有按下过。返回false,则代表up按键无按下过,不一致的事件,则删除
            if (!connection->inputState.trackKey(keyEntry,dispatchEntry->resolvedAction, dispatchEntry->resolvedFlags))
            {
#if DEBUG_DISPATCH_CYCLE
                ALOGD("channel '%s' ~ enqueueDispatchEntryLocked: skipping inconsistent key event",
                      connection->getInputChannelName().c_str());
#endif
                delete dispatchEntry;
                return; // skip the inconsistent event
            }
            break;
        }
        }

        // 我们正在等待此dispatchEntry事件的分发完成
        if (dispatchEntry->hasForegroundTarget())//是要发送到前台,所以是执行的如果此事件是要发送到前台窗口
        {
            incrementPendingForegroundDispatchesLocked(eventEntry);//增加未处理的前台分发数量
        }

        // 将dispatchEntry事件放入connection对象的outboundQueue序列的队尾
        connection->outboundQueue.enqueueAtTail(dispatchEntry);
        traceOutboundQueueLengthLocked(connection);//追踪队列的长度
}
InputDispatcher::DispatchEntry::DispatchEntry(EventEntry* eventEntry,
        int32_t targetFlags, float xOffset, float yOffset, float scaleFactor) :
        seq(nextSeq()),//nextSeq是一个静态函数,里面有一个int值的静态变量,每构造一个DispatchEntry对象,此值就加1,然后赋值给seq
        eventEntry(eventEntry),//保存的EventEntry对象
		targetFlags(targetFlags),//发送的flag
        xOffset(xOffset), //目标窗口的x偏移量
		yOffset(yOffset), //目标窗口的y偏移量
		scaleFactor(scaleFactor),//缩放比例
        deliveryTime(0), 
		resolvedAction(0), 
		resolvedFlags(0) 
		{
			eventEntry->refCount += 1;
		}
bool InputDispatcher::InputState::trackKey(const KeyEntry* entry,
        int32_t action, int32_t flags) {
    switch (action) {
	/**
    case AKEY_EVENT_ACTION_UP: {
        if (entry->flags & AKEY_EVENT_FLAG_FALLBACK) {
            for (size_t i = 0; i < mFallbackKeys.size(); ) {
                if (mFallbackKeys.valueAt(i) == entry->keyCode) {
                    mFallbackKeys.removeItemsAt(i);
                } else {
                    i += 1;
                }
            }
        }
        ssize_t index = findKeyMemento(entry);//如果从容器中找到此案件之前按下的事件,则移除按下的事件,并返回ture
        if (index >= 0) {
            mKeyMementos.removeAt(index);//mKeyMementos存储了所有按下还未抬起的key事件
            return true;
        }
        //我们不能只放弃按键事件,因为这会阻止创建弹出窗口,当按键被按下时,弹出窗口会自动显示,然后在按键被释放时被关闭。
		//问题是弹出窗口将不会收到原始的向下键,因此向上键将被视为与其观察到的状态不一致。
		//我们也许可以通过合成一个向下键来处理这个问题,但这会导致其他问题。
		//因此,现在,允许分发不一致的按键up事件。

#if DEBUG_OUTBOUND_EVENT_DETAILS
        ALOGD("Dropping inconsistent key up event: deviceId=%d, source=%08x, "
                "keyCode=%d, scanCode=%d",
                entry->deviceId, entry->source, entry->keyCode, entry->scanCode);
#endif
        return false;
        
        return true;
    }
	*/

    case AKEY_EVENT_ACTION_DOWN: {//如果按键是按下,则用findKeyMemento查找是否已经有此按键按下的事件,如果有,
	//则表示产生了连续两个按下的事件,故移除上一个按下的按键,并通过addKeyMemento函数将这次按下的按键保存到容器中
        ssize_t index = findKeyMemento(entry);
        if (index >= 0) {
            mKeyMementos.removeAt(index);
        }
        addKeyMemento(entry, flags);
        return true;
    }

    default:
        return true;
    }
}
void InputDispatcher::InputState::addKeyMemento(const KeyEntry* entry, int32_t flags) {
    mKeyMementos.push();
    KeyMemento& memento = mKeyMementos.editTop();
    memento.deviceId = entry->deviceId;
    memento.source = entry->source;
    memento.keyCode = entry->keyCode;
    memento.scanCode = entry->scanCode;
    memento.metaState = entry->metaState;
    memento.flags = flags;
    memento.downTime = entry->downTime;
    memento.policyFlags = entry->policyFlags;
}
inline bool hasForegroundTarget() const {
            return targetFlags & InputTarget::FLAG_FOREGROUND;
}
void InputDispatcher::incrementPendingForegroundDispatchesLocked(EventEntry* entry) {
    InjectionState* injectionState = entry->injectionState;
	/*
    if (injectionState) {//默认是null
        injectionState->pendingForegroundDispatches += 1;//pendingForegroundDispatches默认值是0,表示正在进行的前台分发的数量
    }*/
}

 2.14 startDispatchCycleLocked

void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,const sp<Connection>& connection) {
	//当connection的状态正常且outboundQueue不为空时
    while (connection->status == Connection::STATUS_NORMAL
            && !connection->outboundQueue.isEmpty()) {
        DispatchEntry* dispatchEntry = connection->outboundQueue.head;//取出队列头部的dispatchEntry事件
        dispatchEntry->deliveryTime = currentTime;//完成发送的时间

        // Publish the event.
        status_t status;
        EventEntry* eventEntry = dispatchEntry->eventEntry;//取出EventEntry类型的事件
        switch (eventEntry->type) {
        case EventEntry::TYPE_KEY: {
            KeyEntry* keyEntry = static_cast<KeyEntry*>(eventEntry);

            // Publish the key event.
            status = connection->inputPublisher.publishKeyEvent(dispatchEntry->seq,//DispatchEntry是一个链表,seq表示发送事件的序列号。
                    keyEntry->deviceId, keyEntry->source,
                    dispatchEntry->resolvedAction, dispatchEntry->resolvedFlags,
                    keyEntry->keyCode, keyEntry->scanCode,
                    keyEntry->metaState, keyEntry->repeatCount, keyEntry->downTime,
                    keyEntry->eventTime);
					//调用了Connection类中的InputPublisher类对象的publishKeyEvent方法。
					//参数分析:
					//dispatchEntry->seq,请求序列,seq表示发送事件的序列号。
					//keyEntry->deviceId,设备id
					//keyEntry->source,是KeyboardInputMapper初始化时赋值的,
					//其值是INPUT_DEVICE_CLASS_KEYBOARD,INPUT_DEVICE_CLASS_DPAD从,INPUT_DEVICE_CLASS_GAMEPAD的某一个
					//dispatchEntry->resolvedAction=keyEntry->action,表示按下或者抬起
					//dispatchEntry->resolvedFlags,表示flag策略,此时应该是POLICY_FLAG_PASS_TO_USER
					//keyEntry->keyCode,对应的按键码
					//keyEntry->scanCode,按键码对应的扫描码
            break;
        }
        }

        // 检查发送的结果
        if (status) {
            if (status == WOULD_BLOCK) {
                if (connection->waitQueue.isEmpty()) {
                    ALOGE("channel '%s' ~ Could not publish event because the pipe is full. "
                            "This is unexpected because the wait queue is empty, so the pipe "
                            "should be empty and we shouldn't have any problems writing an "
                            "event to it, status=%d", connection->getInputChannelName().c_str(),
                            status);
                    abortBrokenDispatchCycleLocked(currentTime, connection, true /*notify*/);
                } else {
                    // Pipe is full and we are waiting for the app to finish process some events
                    // before sending more events to it.

                    connection->inputPublisherBlocked = true;
                }
            } else {
                ALOGE("channel '%s' ~ Could not publish event due to an unexpected error, "
                        "status=%d", connection->getInputChannelName().c_str(), status);
                abortBrokenDispatchCycleLocked(currentTime, connection, true /*notify*/);
            }
            return;
        }

        // Re-enqueue the event on the wait queue.
        connection->outboundQueue.dequeue(dispatchEntry);//发送完成后,将此dispatchEntry退出outboundQueue队列
        traceOutboundQueueLengthLocked(connection);//追踪outboundQueue队列长度
        connection->waitQueue.enqueueAtTail(dispatchEntry);//将此dispatchEntry入队到waitQueue队列中
        traceWaitQueueLengthLocked(connection);//追踪waitQueue队列的长度
    }
}

2.15 InputPublisher::publishKeyEvent

主要作用是:

1.将消息封装成InputMessage,然后通过InputChannel发送消息。

status_t InputPublisher::publishKeyEvent(
        uint32_t seq,
        int32_t deviceId,
        int32_t source,
        int32_t action,
        int32_t flags,
        int32_t keyCode,
        int32_t scanCode,
        int32_t metaState,
        int32_t repeatCount,
        nsecs_t downTime,
        nsecs_t eventTime) {
    if (!seq) {
        ALOGE("Attempted to publish a key event with sequence number 0.");
        return BAD_VALUE;
    }

    InputMessage msg;
    msg.header.type = InputMessage::TYPE_KEY;//表示按键事件
    msg.body.key.seq = seq;//消息的序列号
    msg.body.key.deviceId = deviceId;//设备id
    msg.body.key.source = source;//表示键盘等
    msg.body.key.action = action;//按下或者抬起
    msg.body.key.flags = flags;//表示policy,发送给应用
    msg.body.key.keyCode = keyCode;
    msg.body.key.scanCode = scanCode;
    msg.body.key.metaState = metaState;//控制键的状态
    msg.body.key.repeatCount = repeatCount;
    msg.body.key.downTime = downTime;
    msg.body.key.eventTime = eventTime;
    return mChannel->sendMessage(&msg);
}

 2.16 InputChannel::sendMessage

此处便是真正调用socket send接口发送消息的地方。

status_t InputChannel::sendMessage(const InputMessage* msg) {
    const size_t msgLength = msg->size();
    InputMessage cleanMsg;
    msg->getSanitizedCopy(&cleanMsg);//InputMessage字段之间可能有非零字节。
	//强制将整个内存初始化为零,然后仅在每个字段的基础上复制有效字节。
    ssize_t nWrite;
    do {
        nWrite = ::send(mFd, &cleanMsg, msgLength, MSG_DONTWAIT | MSG_NOSIGNAL); 
    } while (nWrite == -1 && errno == EINTR);//如果出错,则一直循环发送等到其成功为止
	//函数说明:send()用来将数据由指定的socket 传给对方主机. 
	//参数s 为已建立好连接的socket. 
	//参数msg 指向发送的数据内容. 
	//参数len 则为数据长度. 
	//参数flags 一般设0, 其他数值定义如下: 
	//MSG_OOB 传送的数据以out-of-band 送出. 
	//MSG_DONTROUTE 取消路由表查询 
	//MSG_DONTWAIT 设置为不可阻断运作 
	//MSG_NOSIGNAL 此动作不愿被SIGPIPE 信号中断. 
	//返回值:成功则返回实际传送出去的字符数, 失败返回-1. 错误原因存于errno.
	//当 mFd 写入消息后,此时会唤醒处于 epoll_wait 状态的应用进程的 UI 线程
    if (nWrite < 0) {//如果发送失败
        int error = errno;
        if (error == EAGAIN || error == EWOULDBLOCK) {
            return WOULD_BLOCK;
        }
        if (error == EPIPE || error == ENOTCONN || error == ECONNREFUSED || error == ECONNRESET) {
            return DEAD_OBJECT;
        }
        return -error;
    }

    if (size_t(nWrite) != msgLength) {

        return DEAD_OBJECT;
    }
    return OK;
}
size_t InputMessage::size() const {
    switch (header.type) {
    case TYPE_KEY:
        return sizeof(Header) + body.key.size();
}
}
//InputMessage字段之间可能有非零字节。强制将整个内存初始化为零,然后仅在每个字段的基础上复制有效字节。
void InputMessage::getSanitizedCopy(InputMessage* msg) const {
    memset(msg, 0, sizeof(*msg));//初始化所有字节为0

    // Write the header
    msg->header.type = header.type;

    // Write the body
    switch(header.type) {
        case InputMessage::TYPE_KEY: {
            // uint32_t seq
            msg->body.key.seq = body.key.seq;
            // nsecs_t eventTime
            msg->body.key.eventTime = body.key.eventTime;
            // int32_t deviceId
            msg->body.key.deviceId = body.key.deviceId;
            // int32_t source
            msg->body.key.source = body.key.source;
            // int32_t displayId
            msg->body.key.displayId = body.key.displayId;
            // int32_t action
            msg->body.key.action = body.key.action;
            // int32_t flags
            msg->body.key.flags = body.key.flags;
            // int32_t keyCode
            msg->body.key.keyCode = body.key.keyCode;
            // int32_t scanCode
            msg->body.key.scanCode = body.key.scanCode;
            // int32_t metaState
            msg->body.key.metaState = body.key.metaState;
            // int32_t repeatCount
            msg->body.key.repeatCount = body.key.repeatCount;
            // nsecs_t downTime
            msg->body.key.downTime = body.key.downTime;
            break;
        }
        default: {
            LOG_FATAL("Unexpected message type %i", header.type);
            break;
        }
    }
}

此处涉及inputchannel的创建过程。

大致讲解一下:

1.应用程序app一定会有activity,然后在打开app时会调用Activity.onCreate,然后经过AMS,再经过WMS,会调用WindowManagerGlobal.addView(),这个函数里面会创建socket对,一个被注册到注册到 InputDispatcher 中,一个返回给app进程的客户端,app进程会监听此socket,当有事件发送到应用程序时,会回调 InputEventReceiver 对应的 native 层对象 NativeInputEventReceiver 的 handleEvent 函数。

此时,我们就通过socket发送按键消息到达了应用端,下一篇android 9】【input】【9.发送按键事件3——Inputchannel的创建过程】我们主要介绍一下应用端是在什么时候创建的sokcet进行的通信。然后在【android 9】【input】【10.发送按键事件4——view的处理流程】,在这一篇主要介绍一下应用层的处理流程。


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

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

相关文章

有没有不用写代码,贴图也简单的HTML WEB 3D 产品展示配置器?

品牌零售商&#xff0c;产品有3D模型但没有贴图&#xff0c;以前是直接送去生产&#xff0c;现在需要上线电商&#xff0c;看到国外挺多这种展示方式的网店&#xff0c;国内有没有这方面的供应商&#xff0c;网店顾客可以自己鼠标转动、换零件、换颜色等看定制效果&#xff0c;…

深入理解并发之LongAdder、DoubleAdder的实现原理

深入理解LongAdder、DoubleAdder的实现原理 本文主要通过LongAdder和DoubleAdder的源码&#xff0c;讲述一下其实现原理。通过LongAdder和DoubleAdder的源码可知。两者都是继承了Striped64的类。下面我们将通过源码的形式讲述一下这三个类都做了哪些事情。 1: Striped64 ​ …

用C#(WinForm)开发触摸屏,体验感满满

用C#&#xff08;WinForm&#xff09;开发触摸屏&#xff0c;体验感满满

人工智能大模型的进化之路:探索如何让它们变得更“聪明”

一、引言 在人工智能&#xff08;AI&#xff09;领域&#xff0c;大模型凭借其强大的处理能力和广泛的应用前景&#xff0c;已经成为研究的热点。然而&#xff0c;尽管这些模型在多个领域展现出了惊人的能力&#xff0c;但它们仍然面临着理解力、泛化能力和适应性等方面的挑战…

中央空调节能的分户计费系统

中央空调节能 在建筑能耗中&#xff0c;中央空调能耗一般占到了40%---60%的比例&#xff0c;因此如何有效降低空调能耗就成为建筑节能的重中之重。 项目案例描述 山东银座购物广场&#xff1a;为集购物中心、高级酒店式公寓和办公为一体的综合性公共建筑。整体建筑共为地下3层&…

我们身边的北斗:你知道吗北斗还能帮我们演唱会抢票

当我们步入2024年&#xff0c;演唱会与音乐节的热潮依然不减。每当周末或节假日&#xff0c;各大城市的演唱会场馆总是人头攒动&#xff0c;歌声与掌声交织成一片欢乐的海洋。而在朋友圈里&#xff0c;也总能看到乐迷们晒出的演唱会现场照片和视频&#xff0c;分享着那份独特的…

智领未来,安全无忧:智能网联汽车监控大屏的守护之旅

在繁忙的都市中&#xff0c;驾驶者往往面临着诸多安全隐患。传统的驾驶辅助系统虽然能够提供一定的帮助&#xff0c;但在复杂多变的交通环境中&#xff0c;其局限性也逐渐显现。而智能网联汽车安全监控大屏&#xff0c;正是为了解决这一问题而诞生的。 山海鲸可视化大屏 大屏采…

36. 【Java教程】输入输出流

本小节将会介绍基本输入输出的 Java 标准类&#xff0c;通过本小节的学习&#xff0c;你将了解到什么是输入和输入&#xff0c;什么是流&#xff1b;输入输出流的应用场景&#xff0c;File类的使用&#xff0c;什么是文件&#xff0c;Java 提供的输入输出流相关 API 等内容。 1…

【翻译软件】CopyTranslator复制即翻译的外文辅助阅读翻译软件NO.102

使用平台&#xff1a;Windows/Linux/macOS 设置里选择翻译引擎和翻译API&#xff0c;谷歌翻译已经退出中国了&#xff0c;但还是提供了镜像地址 一、复制即翻译 只需要复制文本到剪贴板&#xff0c;就可以查看翻译结果 记得开启“自动粘贴”哦。 二、多段同时翻译 三、智能…

别再emo了,还不赶紧去考PMP,搞钱要紧~

自从疫情之后经济大不如从前&#xff0c;现在大环境都不好&#xff0c;很多公司都在裁员&#xff0c;像我朋友就在上个月被裁掉了&#xff0c;虽说拿了补偿但也不可能靠那点补偿生活的&#xff0c;所以我朋友找了很久的工作&#xff0c;但是由于大环境的缺失所以导致他的薪资直…

误删照片怎么办?恢复删除的图片,3个指南!

在我们的日常生活中&#xff0c;照片就像是我们的小秘密宝藏&#xff0c;记录着我们与亲朋好友一起嗨皮的时光&#xff0c;还有那些让我们激动不已的人生大事。可是&#xff0c;有时候我们可能会因为一时的疏忽&#xff0c;比如手滑点错了按钮&#xff0c;或者在清理手机内存时…

微软云计算Windows Azure(一)

目录 一、微软云计算平台二、微软云操作系统Windows Azure&#xff08;一&#xff09;Windows Azure概述&#xff08;二&#xff09;Windows Azure计算服务&#xff08;三&#xff09;Windows Azure存储服务&#xff08;四&#xff09;Windows Azure Connect&#xff08;五&…

2003远程桌面端口修改,修改远程桌面端口的操作

在信息技术领域&#xff0c;远程桌面端口的修改是一项至关重要的安全操作&#xff0c;尤其在运行Windows 2003操作系统的环境中。对于系统管理员而言&#xff0c;了解和掌握如何正确、有效地修改远程桌面端口&#xff0c;是确保服务器安全、防止潜在攻击的关键步骤。 首先&…

图解Mysql索引原理

概述 是什么 索引像是一本书的目录列表&#xff0c;能根据目录快速的找到具体的书本内容&#xff0c;也就是加快了数据库的查询速度索引本质是一个数据结构索引是在存储引擎层&#xff0c;而不是服务器层实现的&#xff0c;所以&#xff0c;并没有统一的索引标准&#xff0c;…

笔记:如何在pycharm中使用anaconda的虚拟环境,新建工程和更改现有工程的虚拟环境。

1.用anaconda创建虚拟环境 (base) C:\Users\Administrator>conda -V conda 24.5.0(base) C:\Users\Administrator>conda create -n appenv python Channels:- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main- defaults Platform: win-64 Collecting package m…

upload-labs-第一关和第二关

目录 第一关 思路&#xff1a; 1、上传一个php文件 2、查看源码 3、查看文件上传地址 4、BP抓包&#xff0c;修改文件后缀名 5、使用蚁剑连接 第二关 1、这一关也可以跟第一关一样的方法进行绕过 2、上传一个一句话木马文件 第一关 原理&#xff1a; 思路&#xff1a…

Vray渲染器的作用是什么?渲染100邀请码1a12

Vray是一款专业的渲染器&#xff0c;为不同领域的CG制作者提供高质量的渲染&#xff0c;它有以下几个作用。 1、Vray能创建专业的照明效果&#xff0c;渲染最逼真的画面&#xff0c;让场景生动无比。 2、Vray能模拟各种自然光和人造光源&#xff0c;如太阳光、天空光、区域光…

乡村振兴的乡村生态文明建设:加强乡村生态环境保护,推进乡村绿色发展,打造生态宜居的美丽乡村

目录 一、引言 二、乡村生态环境保护的必要性 三、加强乡村生态环境保护的措施 &#xff08;一&#xff09;完善法律法规&#xff0c;强化制度保障 &#xff08;二&#xff09;加强宣传教育&#xff0c;提高环保意识 &#xff08;三&#xff09;推广生态农业&#xff0c;…

机器视觉检测--相机

一&#xff0c;相机就是CCD么&#xff1f; 通常&#xff0c;我们把相机都叫作CCD&#xff0c;CCD已经成了相机的代名词。其实很可能正在使用的是CMOS。CCD以及CMOS都称为感光元件&#xff0c;都是将光学图像转换为电子信号的半导体元件。他们在检测光时都采用光电二极管&#…

5252DE 5G 外场通信测试仪

5252DE 5G 外场通信测试仪 集先进算法和高性能硬件于一体的便携式测试仪表 产品综述 5252DE 5G 外场通信测试仪是集合高性能频谱处理模块、多制式解析算法软件于一体的手持式测试仪表&#xff0c;具有很好的便携性、兼容性与可拓展性。 5252DE 具有工作频段宽、性能指标高…