Input事件在应用中的传递(一)

news2024/9/24 13:25:48

Input事件在应用中的传递(一)

hongxi.zhu 2023-4-25

前面我们已经梳理了input事件在native层的传递,这一篇我们接着探索input事件在应用中的传递与处理,我们将按键事件和触摸事件分开梳理,这一篇就只涉及按键事件。

一、事件的接收

从前面的篇幅我们知道,framework native层InputDispatcher向应用通过socket方式发送事件,应用的Looper 通过epoll方式监听sockcet的fd, 当应用的socket变为可读时(例如,它有可读事件),Looper将回调handleEvent。 此时,应用应读取已进入套接字的事件。 只要socket中有未读事件,函数 handleEvent 就会继续触发。(这个event不是真正的输入事件,只是Looper的状态event)

//frameworks/base/core/jni/android_view_InputEventReceiver.cpp

int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) {
    // Allowed return values of this function as documented in LooperCallback::handleEvent
    constexpr int REMOVE_CALLBACK = 0;
    constexpr int KEEP_CALLBACK = 1;
    //注意:下面这个event不是真正的输入事件,只是Looper的状态event
    if (events & (ALOOPER_EVENT_ERROR | ALOOPER_EVENT_HANGUP)) {
        //当inputdispatcher异常导致socket被关闭或者目标窗口正在被移除或者传递窗口时输入法,但是输入法正在关闭时会直接抛弃这个事件
        // This error typically occurs when the publisher has closed the input channel
        // as part of removing a window or finishing an IME session, in which case
        // the consumer will soon be disposed as well.
        if (kDebugDispatchCycle) {
            ALOGD("channel '%s' ~ Publisher closed input channel or an error occurred. events=0x%x",
                  getInputChannelName().c_str(), events);
        }
        return REMOVE_CALLBACK;
    }
    //如果是输入事件,即是framework传递过来的事件时需要处理时
    if (events & ALOOPER_EVENT_INPUT) {
        JNIEnv* env = AndroidRuntime::getJNIEnv();
        status_t status = consumeEvents(env, false /*consumeBatches*/, -1, nullptr);
        mMessageQueue->raiseAndClearException(env, "handleReceiveCallback");
        return status == OK || status == NO_MEMORY ? KEEP_CALLBACK : REMOVE_CALLBACK;
    }
    //如果已处理的事件需要告知inputdispatcher这个事件已处理时
    if (events & ALOOPER_EVENT_OUTPUT) {
        const status_t status = processOutboundEvents();
        if (status == OK || status == WOULD_BLOCK) {
            return KEEP_CALLBACK;
        } else {
            return REMOVE_CALLBACK;
        }
    }

    ALOGW("channel '%s' ~ Received spurious callback for unhandled poll event.  events=0x%x",
          getInputChannelName().c_str(), events);
    return KEEP_CALLBACK;
}

handleEvent区分是本次Looper获取到的event, 是需要系统处理接收输入事件,还是需要回复给InputDispatcher事件处理结束的event,如果是需要处理的输入事件,就调用consumeEvents消费这个事件。(注意:这个event不是真正的输入事件,只是Looper的状态case)

//frameworks/base/core/jni/android_view_InputEventReceiver.cpp

status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,
        bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {
    ...

    ScopedLocalRef<jobject> receiverObj(env, nullptr);
    bool skipCallbacks = false;
    for (;;) {
        uint32_t seq;
        InputEvent* inputEvent;
        //真正的去获取socket发过来的事件,并构建成具体的某种InputEvent,例如KeyEvent
        status_t status = mInputConsumer.consume(&mInputEventFactory,
                consumeBatches, frameTime, &seq, &inputEvent);
        if (status != OK && status != WOULD_BLOCK) {
            ALOGE("channel '%s' ~ Failed to consume input event.  status=%s(%d)",
                  getInputChannelName().c_str(), statusToString(status).c_str(), status);
            return status;
        }
        ...

consumeEvents中我们才开始真正的拿着对应的socket fd去读取socket的msg, 具体读取会调用InputConsumer::consume

//frameworks/native/libs/input/InputTransport.cpp

status_t InputConsumer::consume(InputEventFactoryInterface* factory, bool consumeBatches,
                                nsecs_t frameTime, uint32_t* outSeq, InputEvent** outEvent) {
    ...
    *outSeq = 0;
    *outEvent = nullptr;

    // Fetch the next input message.
    // Loop until an event can be returned or no additional events are received.
    while (!*outEvent) {  //获取到一次真正的事件就退出
        if (mMsgDeferred) {
            ...
        } else {
            // Receive a fresh message.
            status_t result = mChannel->receiveMessage(&mMsg);  //通过InputChannel来接收socket中真正的InputMessage(描述事件的结构体)
            ...
        }
        ...
        }
    }
    return OK;
}

InputConsumer::consume中获取事件实际上是通过InputChannel去读取

frameworks/native/libs/input/InputTransport.cpp

status_t InputChannel::receiveMessage(InputMessage* msg) {
    ssize_t nRead;
    do {
        nRead = ::recv(getFd(), msg, sizeof(InputMessage), MSG_DONTWAIT); //在这里真正的读取socket fd,并将输入事件信息装入msg(InputMessage)
    } while (nRead == -1 && errno == EINTR);
    
    ...
    
    return OK;  //最后返回OK
}

通过InputChannel去读取真正的事件信息,并装入InputMessage对象,最后返回OK

//frameworks/native/libs/input/InputTransport.cpp

status_t InputConsumer::consume(InputEventFactoryInterface* factory, bool consumeBatches,
                                nsecs_t frameTime, uint32_t* outSeq, InputEvent** outEvent) {
    ...
    *outSeq = 0;
    *outEvent = nullptr;

    // Fetch the next input message.
    // Loop until an event can be returned or no additional events are received.
    while (!*outEvent) {  //获取到一次真正的事件就退出
        if (mMsgDeferred) {
            // mMsg contains a valid input message from the previous call to consume
            // that has not yet been processed.
            mMsgDeferred = false;
        } else {
            // Receive a fresh message.
            status_t result = mChannel->receiveMessage(&mMsg);  //通过InputChannel来接收socket中真正的InputMessage(描述事件的结构体)
            if (result == OK) {
                mConsumeTimes.emplace(mMsg.header.seq, systemTime(SYSTEM_TIME_MONOTONIC));
            }
            if (result) {  //result = OK = 0 ,所以并不会进入批处理流程
                // Consume the next batched event unless batches are being held for later.
                if (consumeBatches || result != WOULD_BLOCK) {
                    result = consumeBatch(factory, frameTime, outSeq, outEvent);
                    if (*outEvent) {
                        if (DEBUG_TRANSPORT_ACTIONS) {
                            ALOGD("channel '%s' consumer ~ consumed batch event, seq=%u",
                                  mChannel->getName().c_str(), *outSeq);
                        }
                        break;
                    }
                }
                return result;
            }
        }

        switch (mMsg.header.type) {
            case InputMessage::Type::KEY: {
                KeyEvent* keyEvent = factory->createKeyEvent();  //创建KeyEvent
                if (!keyEvent) return NO_MEMORY;

                initializeKeyEvent(keyEvent, &mMsg);  //将InputMessage信息填充到keyEvent
                *outSeq = mMsg.header.seq;
                *outEvent = keyEvent;  // 返回到上一级使用(keyEvent 继承于InputEvent)
                if (DEBUG_TRANSPORT_ACTIONS) {
                    ALOGD("channel '%s' consumer ~ consumed key event, seq=%u",
                          mChannel->getName().c_str(), *outSeq);
                }
            break;
            }
            ...
        }
    }
    return OK;
}

回到上一级,InputConsumer::consume中,receiveMessage中获取到的InputMessage在这里转化为对应的event类型,对应的按键事件就是KeyEvent,*outEvent = keyEvent返回到前面的NativeInputEventReceiver::consumeEvents

//frameworks/base/core/jni/android_view_InputEventReceiver.cpp

status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,
        bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {
    ...
    for (;;) {
        uint32_t seq;
        InputEvent* inputEvent;
        //真正的去获取socket发过来的事件,并构建成具体的某种InputEvent,例如KeyEvent
        status_t status = mInputConsumer.consume(&mInputEventFactory,
                consumeBatches, frameTime, &seq, &inputEvent);
        ...
        if (!skipCallbacks) {
            jobject inputEventObj;
            switch (inputEvent->getType()) {
            case AINPUT_EVENT_TYPE_KEY:
                if (kDebugDispatchCycle) {
                    ALOGD("channel '%s' ~ Received key event.", getInputChannelName().c_str());
                }
                inputEventObj = android_view_KeyEvent_fromNative(env,
                        static_cast<KeyEvent*>(inputEvent));  //将consume()中拿到的inputEvent转为相应的KeyEvent(在内部我们创建的就是KeyEvent)
                break;
            ...
            if (inputEventObj) {
                ...
                env->CallVoidMethod(receiverObj.get(),
                        gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);  //jni调用InputEventReceiver.java中的InputEventReceiver,将事件传递到java的世界
                ...
                env->DeleteLocalRef(inputEventObj);
            }
            ...
        }
    }
}

通过consume中拿到inputEvent,然后转为KeyEvent,最通过Jni的方式调用java中的InputEventReceiver的方法dispatchInputEvent,正式开始事件的分发。

//frameworks/base/core/java/android/view/InputEventReceiver.java

public abstract class InputEventReceiver {
    ...
    // Called from native code.
    @SuppressWarnings("unused")
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private void dispatchInputEvent(int seq, InputEvent event) {
        mSeqMap.put(event.getSequenceNumber(), seq);
        onInputEvent(event);
    }
    ...
}

InputEventReceiver是一个抽象类,但是对应的dispatchInputEvent方法,它的子类WindowInputEventReceiver并没有实现,所以native层调用父类的InputEventReceiver的方法,这个方法中接着调用了onInputEvent接着处理。onInputEvent子类是有实现的,所以会走子类的方法。

//frameworks/base/core/java/android/view/ViewRootImpl.java
    ...
    final class WindowInputEventReceiver extends InputEventReceiver {
        public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
            super(inputChannel, looper);
        }

        @Override
        public void onInputEvent(InputEvent event) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "processInputEventForCompatibility");
            List<InputEvent> processedEvents;
            try {
                //对M版本之前的触摸事件的兼容处理,按键事件不涉及, return null
                processedEvents =
                    mInputCompatProcessor.processInputEventForCompatibility(event); 
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            if (processedEvents != null) {
                if (processedEvents.isEmpty()) {
                    // InputEvent consumed by mInputCompatProcessor
                    finishInputEvent(event, true);
                } else {
                    for (int i = 0; i < processedEvents.size(); i++) {
                        enqueueInputEvent(
                                processedEvents.get(i), this,
                                QueuedInputEvent.FLAG_MODIFIED_FOR_COMPATIBILITY, true);
                    }
                }
            } else {  //因为上面返回null 所以走到这里
                //在这里将自己this传入
                //processImmediately 为true意味着需要马上处理,而不是延迟处理       
                enqueueInputEvent(event, this, 0, true);
            }
        }
    ...

onInputEvent中会通过QueuedInputEventenqueueInputEvent将事件加入队列中再处理

//frameworks/base/core/java/android/view/ViewRootImpl.java

    @UnsupportedAppUsage
    void enqueueInputEvent(InputEvent event,
            InputEventReceiver receiver, int flags, boolean processImmediately) {
        QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);  //将事件加入队列,确保事件的有序处理

        if (event instanceof MotionEvent) {
            ...
        } else if (event instanceof KeyEvent) { //如果案件事件是一个key的canceled事件
            KeyEvent ke = (KeyEvent) event;
            if (ke.isCanceled()) {
                EventLog.writeEvent(EventLogTags.VIEW_ENQUEUE_INPUT_EVENT, "Key - Cancel",
                        getTitle().toString());
            }
        }
        // 无论时间戳如何,始终按顺序排列输入事件。
        // 我们这样做是因为应用本身或 IME 可能会注入事件,
        // 我们希望确保注入的按键按照接收到的顺序进行处理,不能仅仅通过时间戳的前后来确定顺序。
        // Always enqueue the input event in order, regardless of its time stamp.
        // We do this because the application or the IME may inject key events
        // in response to touch events and we want to ensure that the injected keys
        // are processed in the order they were received and we cannot trust that
        // the time stamp of injected events are monotonic.
        QueuedInputEvent last = mPendingInputEventTail;
        if (last == null) {
            mPendingInputEventHead = q;
            mPendingInputEventTail = q;
        } else {
            last.mNext = q;
            mPendingInputEventTail = q;
        }
        mPendingInputEventCount += 1;
        Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
                mPendingInputEventCount);

        if (processImmediately) {
            doProcessInputEvents();  //前面传进来的processImmediately = true所以走这里处理
        } else {
            scheduleProcessInputEvents();
        }
    }

为什么需要加入队列处理?一般来说,当InputDispatcher发送一个事件给应用,应用需要处理完并反馈给InputDispatcher,后者才会发送下一个事件,本身操作流程就是串行的,看起来是不需要队列来保证串行处理。但是不要忘记,除了来自底层驱动的事件外,应用和IME都是可以往应用进程注入事件的,那么就需要保证处理的顺序。那么下一步就是从队头依次拿出事件来分发了,对应方式是doProcessInputEvents

//frameworks/base/core/java/android/view/ViewRootImpl.java

    void doProcessInputEvents() {
        // Deliver all pending input events in the queue.
        while (mPendingInputEventHead != null) {
            QueuedInputEvent q = mPendingInputEventHead;  //从队列中拿出数据分发,确保有序
            mPendingInputEventHead = q.mNext;
            if (mPendingInputEventHead == null) {
                mPendingInputEventTail = null;
            }
            q.mNext = null;

            mPendingInputEventCount -= 1;
            Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
                    mPendingInputEventCount);

            mViewFrameInfo.setInputEvent(mInputEventAssigner.processEvent(q.mEvent));

            deliverInputEvent(q);  //开始分发事件
        }

        // We are done processing all input events that we can process right now
        // so we can clear the pending flag immediately.
        if (mProcessInputEventsScheduled) {
            mProcessInputEventsScheduled = false;
            mHandler.removeMessages(MSG_PROCESS_INPUT_EVENTS);
        }
    }

前面将事件入队,然后在doProcessInputEvents就开始从队头拿出并通过deliverInputEvent开始分发

frameworks/base/core/java/android/view/ViewRootImpl.java

    private void deliverInputEvent(QueuedInputEvent q) {
        Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
                q.mEvent.getId());
        ...
        try {
            ...
            InputStage stage;
            if (q.shouldSendToSynthesizer()) {
                stage = mSyntheticInputStage;
            } else {
                //如果忽略输入法窗口则从mFirstPostImeInputStage阶段开始分发,否则从mFirstInputStage开始
                stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
            }
            ...
            if (stage != null) {
                handleWindowFocusChanged();  //在分发前确认是否焦点窗口变化了,如果变化就需要更新焦点的信息
                stage.deliver(q);  //调用对应的stage阶段的deliver方法分发事件
            } else {
                finishInputEvent(q);
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

把事件从拿出,下一步就是往view或者IME分发,分发的过程这里会分为多个阶段(InputStage)来顺序执行, 这些阶段在ViewRootImpl中setView时会指定


    /**
     * We have one child
     */
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
        synchronized (this) {
            if (mView == null) {
            
                ...
                // Set up the input pipeline.
                CharSequence counterSuffix = attrs.getTitle();
                mSyntheticInputStage = new SyntheticInputStage();
                InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
                InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
                        "aq:native-post-ime:" + counterSuffix);
                InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
                InputStage imeStage = new ImeInputStage(earlyPostImeStage,
                        "aq:ime:" + counterSuffix);
                InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
                InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
                        "aq:native-pre-ime:" + counterSuffix);

                mFirstInputStage = nativePreImeStage;
                mFirstPostImeInputStage = earlyPostImeStage;
                mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix;
            }
        }
    }

InputStage这里采用责任链的设计模式,从抽象类InputStage内容可以知道,每一个子类都会将next指向下一个stage子类对象

    abstract class InputStage {
        private final InputStage mNext;

        protected static final int FORWARD = 0;
        protected static final int FINISH_HANDLED = 1;
        protected static final int FINISH_NOT_HANDLED = 2;

        private String mTracePrefix;

        /**
         * Creates an input stage.
         * 将所有的阶段都组成一个链表,next指向下一个阶段
         * @param next The next stage to which events should be forwarded.
         */
        public InputStage(InputStage next) {
            mNext = next;
        }
        ...

setView方法中的内容,我们得出整个链条的结构

NativePreImeInputStage
ViewPreImeInputStage
ImeInputStage
EarlyPostImeInputStage
NativePostImeInputStage
ViewPostImeInputStage
SyntheticInputStage

分发阶段就会从第一个创建的stage子类开始执行到最后一个stage子类,无论要不要处理,都要从链表的头传递到尾。
回到deliverInputEvent方法中stage.deliver(q)正式进入stage的分发中,观察下完整的一个stage的处理流程

        /**
         * Delivers an event to be processed.
         */
        public final void deliver(QueuedInputEvent q) {
            if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {  //如果上一stage中事件被处理(FLAG_FINISHED)那么本stage就不会再处理(onProcess),直接传递到下一个stage(无论是要处理,链表都要走完)
                forward(q);
            } else if (shouldDropInputEvent(q)) {
                finish(q, false);
            } else {
                traceEvent(q, Trace.TRACE_TAG_VIEW);
                final int result;
                try {
                    result = onProcess(q);  //如果前面的阶段没有被处理,本stage就需要走处理流程
                } finally {
                    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
                }
                apply(q, result);  //判断是否需要下一个阶段走处理流程
            }
        }

        /**
         * Marks the input event as finished then forwards it to the next stage.
         *  如果事件在当前阶段被结束,q.mFlags被标记为FLAG_FINISHED,并通过forward(q)传递给下一个阶段
         */
        protected void finish(QueuedInputEvent q, boolean handled) {
            q.mFlags |= QueuedInputEvent.FLAG_FINISHED;
            if (handled) {
                q.mFlags |= QueuedInputEvent.FLAG_FINISHED_HANDLED;
            }
            forward(q);
        }

        /**
         * Forwards the event to the next stage.
         * 往下一个阶段分发
         */
        protected void forward(QueuedInputEvent q) {
            onDeliverToNext(q);// 继续往下一个阶段传递
        }

        /**
         * Applies a result code from {@link #onProcess} to the specified event.
         * 判断是否需要继续接着往下一个阶段分发
         */
        protected void apply(QueuedInputEvent q, int result) {
            if (result == FORWARD) {  //如果上一个阶段还没处理这个事件,则继续往下一个阶段分发处理
                forward(q);
            } else if (result == FINISH_HANDLED) {   //如果事件被处理了,就标记为FLAG_FINISHED|FLAG_FINISHED_HANDLED,然后继续传递给下一个阶段(但不走onProcess()了)
                finish(q, true);
            } else if (result == FINISH_NOT_HANDLED) {  //如果事件没有被处理则标记为FLAG_FINISHED,然后继续传递给下一个阶段(但不走onProcess()了)
                finish(q, false);
            } else {
                throw new IllegalArgumentException("Invalid result: " + result);
            }
        }

        /**
         * Called when an event is ready to be processed.
         * @return A result code indicating how the event was handled.
         */
        protected int onProcess(QueuedInputEvent q) {
            return FORWARD;
        }

        /**
         * Called when an event is being delivered to the next stage.
         * 继续执行下一阶段的deliver
         */
        protected void onDeliverToNext(QueuedInputEvent q) {
            if (DEBUG_INPUT_STAGES) {
                Log.v(mTag, "Done with " + getClass().getSimpleName() + ". " + q);
            }
            if (mNext != null) {
                mNext.deliver(q);  //如果下一阶段不为空就继续执行下一阶段的deliver,继续往下一阶段传递
            } else {
                finishInputEvent(q);
            }
        }

具体如流程图:
在这里插入图片描述
NativePreImeInputStage开始deliver,事件经过每一个stage, 如果该事件没有被处理(标记为)FLAG_FINISHED或者该事件应该被抛弃(shouldDropInputEvent),那么就应该传给本阶段(stage)处理(onProcess),按照这个逻辑一直跑完整个链表。

在这里阶段里我们本篇比较关心玩View树分发的阶段,即ViewPostImeInputStage

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

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

相关文章

Kubernetes(k8s)概念介绍

文章目录 1. k8s概述和特性2. k8s架构组件3. k8s核心概念 1. k8s概述和特性 K8s是一个开源的用于管理云平台中多个主机上的容器化的应用&#xff0c;k8s的目标是让部署容器化的应用简单并且高效&#xff0c;k8s提供了应用部署&#xff0c;规划&#xff0c;更新&#xff0c;维护…

R语言 | 程序的流程控制

目录 一、if语句 1.1 if语句的基本操作 1.2 if...else语句 1.3 if语句也可有返回值 1.4 if...else if...else if...else 1.5 嵌套式if语句 二、递归式函数的设计 三、向量化的逻辑表达式 3.1 处理向量数据if...else的错误 3.2 ifelse()函数 四、switch语句 五、for循…

塔望3W消费战略全案丨窄赛道,高增长 普麦尔知己精酿,掘金中国啤酒利基市场

普麦尔 知己精酿 客户&#xff1a;上海麦仕醇啤酒有限公司 品牌&#xff1a;普麦尔 purmalz beer 服务&#xff1a;3W消费战略 品牌全案 掘金中国啤酒利基市场 精酿啤酒(Craft Beer)的概念最早起源于美国。通常与传统的工业啤酒形成较大的反差&#xff0c;精酿最早的称呼 …

C learning_10 (函数的嵌套调用和链式访问、函数的声明和定义、函数递归)

目录 函数的嵌套调用和链式访问 嵌套调用 函数可以嵌套调用&#xff0c;可以嵌套定义吗&#xff1f; 链式访问 函数的声明和定义 声明 定义 函数递归 递归的两个必要条件 函数的嵌套调用和链式访问 嵌套调用 函数嵌套调用指的是在一个函数中调用另外一个函数&#xf…

OpenSSH升级版本

项目被扫出openssh有严重漏洞&#xff0c;因此要升级openssh到最新版本 1、为防止安装失败&#xff0c;无法用ssh做远程连接&#xff0c;因此先安装telnet防一手 yum -y install telnet*systemctl enable telnet.socketsystemctl start telnet.socket注&#xff1a;安全文件关…

【星戈瑞】脂溶性CY5.5马来酰亚胺Cyanine5.5 maleimide

脂溶性CY5.5马来酰亚胺是一种荧光染料&#xff0c;其分子中含有马来酰亚胺结构&#xff0c;结构中的共轭体系赋予其良好的光学性质。该染料在红外区域有较强的吸收峰&#xff0c;具有较高的荧光量子产率和较长的荧光寿命&#xff0c;因此被应用于细胞和生物分子的荧光标记和成像…

cleanmymac正式版mac电脑清理软件

电脑清理软件在我们的电脑使用过程中是非常实用便捷的清理软件.当电脑使用了一段时间内,电脑就会产生各种各样的垃圾文件,比如说残留的注册表或者无效的注册表,系统碎片以及毫无用处的文件等,这些的存在将会极大程度地拖慢电脑的运行速度,而你删除的话可能会删除本不能删除的文…

解锁新技能《Java绘制2D图形验证码方法》

一、常见的图形验证码有如下三种&#xff1a; 字母数字混合&#xff08;干扰线&#xff09;&#xff1b;纯数字&#xff08;干扰线&#xff09;&#xff1b;纯字母&#xff08;干扰线&#xff09;&#xff1b; 二、示例图如下&#xff1a; 三、案例代码 问题一&#xff1a;绘…

LC-2106. 摘水果(滑动窗口)

2106. 摘水果 难度困难78 在一个无限的 x 坐标轴上&#xff0c;有许多水果分布在其中某些位置。给你一个二维整数数组 fruits &#xff0c;其中 fruits[i] [positioni, amounti] 表示共有 amounti 个水果放置在 positioni 上。fruits 已经按 positioni 升序排列 &#xff0c…

C语言动态内存管理与文件操作:打造高效通讯录

本篇博客会讲解如何使用C语言实现一个通讯录。实现通讯录的过程中&#xff0c;会大量用到C语言的知识点&#xff0c;包括但不限于&#xff1a;函数、自定义类型、指针、动态内存管理、文件操作&#xff0c;这些知识点在我的其他博客中都有讲解过&#xff0c;欢迎大家阅读&#…

进程及进程管理

Linux基础 提示&#xff1a;个人学习总结&#xff0c;仅供参考。 一、Linux系统部署 二、服务器初始化 三、文件和用户管理 四、用户的权限 五、进程及进程管理 提示&#xff1a;文档陆续更新整理 进程及进程管理 Linux基础一、进程简介&#xff08;一&#xff09;进程三问…

面向万物智联的应用框架的思考和探索(上)

原文&#xff1a;面向万物智联的应用框架的思考和探索&#xff08;上&#xff09;&#xff0c;点击链接查看更多技术内容。 应用框架&#xff0c;是操作系统连接开发者生态&#xff0c;实现用户体验的关键基础设施。其中&#xff0c;开发效率和运行体验是永恒的诉求&#xff0c…

代码命名规范是真优雅呀!代码如诗

日常编码中&#xff0c;代码的命名是个大的学问。能快速的看懂开源软件的代码结构和意图&#xff0c;也是一项必备的能力。那它们有什么规律呢&#xff1f; Java项目的代码结构&#xff0c;能够体现它的设计理念。Java采用长命名的方式来规范类的命名&#xff0c;能够自己表达…

网络安全怎么学,才不会成为脚本小子?

一&#xff0c;怎么入门&#xff1f; 1、Web 安全相关概念&#xff08;2 周&#xff09; 了解网络安全相关法律法规 熟悉基本概念&#xff08;SQL 注入、上传、XSS、CSRF、一句话木马等&#xff09;。 通过关键字&#xff08;SQL 注入、上传、XSS、CSRF、一句话木马等&#…

RK3588 lt16911uxc hdmi in

我们的单板需要四路HDMI IN,其中一路使用了rk3588自带的hdmi rx,另外三路通过LT6911UXC 转接芯片,实现了三路hdmi in 使用了rk3588的DPHY0,DPHY1以及DCPHY0 设备树修改如下: /* DCPHY0 */ &csi2_dcphy0 { status = "okay"; ports { #addres…

【Errors】【计算机图形学】A-SDF复现的一点纠正记录

ICCV 2021的工作A-SDF&#xff0c;在跑的过程中可能是一些版我Run了这篇工作代码的Reconstruction&#xff0c;然后出现了一点小小的错误&#xff0c;记录如下。 问题一&#xff1a;对数据做直接修改导致出错&#xff08;可能是不同的pytorch版本导致的?&#xff09; 错误描述…

Notepad++ 配置python环境及虚拟环境和其它编程语言环境

背景&#xff1a; 在执行某些项目的时候&#xff0c;经常会否遇到这样一种情况(以python语言为例)&#xff1a;不想在python的基础环境下运行&#xff0c;创建了虚拟环境来装相关项目的包&#xff0c;但是每次使用都要切换到虚拟环境下面使用"activate "命令激活环境…

AT24C02芯片介绍

AT24C02管脚介绍 AT24C02低功耗CMOS串行EEPROM&#xff0c;它是内含2568位存储空间&#xff0c;具有工作电压宽&#xff08;2.5&#xff5e;5.5V&#xff09;、擦写次数多&#xff08;大于10000次&#xff09;、写入速度快&#xff08;小于10ms&#xff09;等特点。 AT24C02的1…

Elasticsearch --- 数据同步、集群

一、数据同步 elasticsearch中的酒店数据来自于mysql数据库&#xff0c;因此mysql数据发生改变时&#xff0c;elasticsearch也必须跟着改变&#xff0c;这个就是elasticsearch与mysql之间的数据同步。 思路分析&#xff1a; 常见的数据同步方案有三种&#xff1a; 同步调用 …

JavaScript:链表

文章目录 链表js没有链表吗203. 移除链表元素为什么设置虚拟头节点思路与代码分析上面代码补充分析注意&#xff1a;为什么把虚拟头节点赋值给 cur 204. 设计链表206. 反转链表双指针法--具体思路见代码双指针法具体分析&#xff08;上面代码看懂这里可以忽略&#xff09;递归法…