Android InputChannel事件发送接收系统分析

2023-05-16

本文基于Android12。

InputChannel表示其他进程通过文件描述符传递输入事件到View的通道,因为需要跨进程传输,实现了Parcelable序列化接口,所以也能够理解Java层的InputChannel后面为什么使用copyTo()方法初始化。

输入事件的接收方是View,所以InputChannel的创建肯定和View的创建流程有关,关于View的创建流程参考:https://blog.csdn.net/qq_36063677/article/details/129908973。

一、InputChannel初始化

ViewRootImpl在setView()方法实例化了Java层的InputChannle对象,但是正如ViewRootImpl创建的mSurface对象一样,这只是一个引用,一个“空的”对象,后续在WindowManagerService经过实际的初始化,再通过copyTo()方法拷贝到InputChannel引用中。

    // ViewRootImpl.java
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
      	// 1.创建 inputChannel引用。
        InputChannel inputChannel = null;
        if ((mWindowAttributes.inputFeatures
                & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
            inputChannel = new InputChannel();
        }
      	// 2.传递引用给mWindowSession,inputChannel在WMS中被初始化,并通过copyTo()全拷贝到inputChannel引用。
        res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
                getHostVisibility(), mDisplay.getDisplayId(), userId,
                mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
                mTempControls, attachedFrame, sizeCompatScale);
      	// 3.通过inputChannel创建WindowInputEventReceiver对象,接收处理输入事件。ALOOPER_EVENT_INPUT
        if (inputChannel != null) {
            mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
                    Looper.myLooper());
        }
    }

setView()方法中关于InputChannel的操作主要有3步:

  1. 创建 inputChannel引用。就是实例化InputChannel引用,InputChannel构造方法是个空方法,所以什么实际操作都没有做。
  2. 传递引用给mWindowSession,inputChannel在WMS中被初始化,并通过copyTo()全拷贝到inputChannel引用。
  3. 通过inputChannel创建WindowInputEventReceiver对象,接收处理输入事件。

我们从第2步开始分析。

1.1 openInputChannle

mWindowSession将inputChannel引用传递给WMS.addWindow()方法:

    // WindowManagerService.java    
    		public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
                int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
                InputChannel outInputChannel, InsetsState outInsetsState,
                InsetsSourceControl[] outActiveControls, Rect outAttachedFrame,
                float[] outSizeCompatScale) {
          			final WindowState win = new WindowState(this, session, client, token, parentWindow,
                        appOp[0], attrs, viewVisibility, session.mUid, userId,
                        session.mCanAddInternalSystemWindow);
                final boolean openInputChannels = (outInputChannel != null
                        && (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);
                if  (openInputChannels) {
                    win.openInputChannel(outInputChannel);
                }
        }

addWindow()创建了WindowState对象,调用其openInputChannel(outInputChannel)方法。

    // WindowState.java    
    		void openInputChannel(InputChannel outInputChannel) {
            if (mInputChannel != null) {
                String name = getName();
                mInputChannel = mWmService.mInputManager.createInputChannel(name);
                if (outInputChannel != null) {
                    mInputChannel.copyTo(outInputChannel);
                }
            }
        }

WindowState通过InputMangerService创建InputChannel,经过NativeInputManagerService类的native方法createInputChannel(name),最终到InputDispatcher::createInputChannel(name)方法实际创建。

1.1.1 InputDispatcher::createInputChannel

    // InputDispatcher.h    
    		// All registered connections mapped by input channel token.
        std::unordered_map<sp<IBinder>, sp<Connection>, StrongPointerHash<IBinder>> mConnectionsByToken
                GUARDED_BY(mLock);
    
    // InputDispatcher.cpp
    	Result<std::unique_ptr<InputChannel>> InputDispatcher::createInputChannel(const std::string& name) {
      
        std::unique_ptr<InputChannel> serverChannel;
        std::unique_ptr<InputChannel> clientChannel;
        // 1.通过CPP层的InputChannel创建serverChannel,clientChannel
        status_t result = InputChannel::openInputChannelPair(name, serverChannel, clientChannel);
        {
            std::scoped_lock _l(mLock);
            const sp<IBinder>& token = serverChannel->getConnectionToken();
          	// 2.得到serverChannel的文件描述符 fd
            int fd = serverChannel->getFd();
          	// 3.将connection对象添加到 mConnectionsByToken管理
            sp<Connection> connection =
                    new Connection(std::move(serverChannel), false /*monitor*/, mIdGenerator);
            mConnectionsByToken.emplace(token, connection);
    
            std::function<int(int events)> callback = std::bind(&InputDispatcher::handleReceiveCallback,
                                                                this, std::placeholders::_1, token);
          	mGlobalMonitorsByDisplay[displayId].emplace_back(serverChannel, pid);
          	// 4.监听serverChannel的文件描述符 fd,当有事件发生时,回调callback
            mLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, new LooperEventCallback(callback), nullptr);
        }
    
        // 5.唤醒mLooper
        mLooper->wake();
        return clientChannel;
    }

InputDispatcher主要做了5件事:

  1. 通过CPP层的InputChannel创建serverChannel,clientChannel,最后返回clientChannel给Java层的InputChannel,到这里Java层InputChannel才被初始化完成,返回创建WindowInputEventReceiver对象。
  2. 得到serverChannel的文件描述符 fd。
  3. 将connection对象添加到 mConnectionsByToken管理,mConnectionsByToken定义在InputDispatcher.h文件,管理所有连接的InputChannel对象,map的key是token,又是使用Binder对象作为token。inputflinger文章中(https://blog.csdn.net/qq_36063677/article/details/130475299)4.3.3小节InputDispatcher在分发事件时就是通过这个mConnectionsByToken获取到具体的connection,发送事件。
  4. 监听serverChannel的文件描述符 fd,ALOOPER_EVENT_INPUT表示为要监听的文件类型,当有事件发生时,回调callback方法handleEvent(),也就是InputDispatcher::handleReceiveCallback()。LooperEventCallback继承了LooperCallback类,LooperCallback是Looper监听文件描述符回调方法的标准类,当文件描述符fd上有事件到来时,LooperCallback的handleEvent()方法会被执行,关于Looper->addFd()更多细节参考:https://blog.csdn.net/chwan_gogogo/article/details/46953563
  5. 唤醒mLooper,mLooper在InputDispatcher构造方法中被初始化,mLooper = new Looper(false);。

查看cpp层InputChannel::openInputChannelPair()具体细节:

1.1.2 InputChannel::openInputChannelPair

    // InputTransport.cpp
    status_t InputChannel::openInputChannelPair(const std::string& name,
                                                std::unique_ptr<InputChannel>& outServerChannel,
                                                std::unique_ptr<InputChannel>& outClientChannel) {
        int sockets[2];
	    if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) {
	        outServerChannel.clear();
	        outClientChannel.clear();
	        return result;
	    }
    
        int bufferSize = SOCKET_BUFFER_SIZE;
        setsockopt(sockets[0], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
        setsockopt(sockets[0], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));
        setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
        setsockopt(sockets[1], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));
    
        sp<IBinder> token = new BBinder();
    
        std::string serverChannelName = name + " (server)";
        android::base::unique_fd serverFd(sockets[0]);
        outServerChannel = InputChannel::create(serverChannelName, std::move(serverFd), token);
    
        std::string clientChannelName = name + " (client)";
        android::base::unique_fd clientFd(sockets[1]);
        outClientChannel = InputChannel::create(clientChannelName, std::move(clientFd), token);
        return OK;
    }
    
    std::unique_ptr<InputChannel> InputChannel::create(const std::string& name,
                                                       android::base::unique_fd fd, sp<IBinder> token) {
      	const int result = fcntl(fd, F_SETFL, O_NONBLOCK);
        return std::unique_ptr<InputChannel>(new InputChannel(name, std::move(fd), token));
    }
    
    InputChannel::InputChannel(const std::string name, android::base::unique_fd fd, sp<IBinder> token)
          : mName(std::move(name)), mFd(std::move(fd)), mToken(std::move(token)) {
    }
  1. InputChannel使用socket通信,openInputChannelPair()方法创建两个socke对象,一个client,一个server。socketpair()函数用于创建一对无名的、相互连接的套接子。如果函数成功,则返回0,创建好的套接字分别是sv[0]和sv[1],所以outServerChannel和outClientChannel这两个socket在创建的时候就是相互连接的,之后只需要在各自的fd中通过send()发送数据就好。
  2. setsockopt()设置socket属性,在<sys/socket.h>头文件中声明,实例化一个BBinder()对象作为token使用,这里outServerChannel和outClientChannel都使用的是同一个token。
  3. InputChannel::create()通过fcntl()设置socket为非阻塞类型,fcntl()方法可以改变已打开的文件性质。
  4. 封装InputChannel对象。

InputChannel初始化过程终于结束了,那么1.1.1小节中mLooper监听的serverChannel的文件描述符 fd什么时候会触发呢?

答案在上一篇inputflinger文章中(https://blog.csdn.net/qq_36063677/article/details/130475299)4.3.3小节。

二、InputChannel发送事件

InputDispatcher在接收到事件后,InputDispatcher::dispatchEventLocked()从mConnectionsByToken变量中通过token获取到Connection对象,最终在4.3.4.2 小节startDispatchCycleLocked()方法调用

connection->inputPublisher.publishMotionEvent()发送输入事件,这里还是以Motion事件为例:

    // InputTransport.cpp
    status_t InputPublisher::publishMotionEvent(
            uint32_t seq, int32_t eventId, int32_t deviceId, int32_t source, int32_t displayId,
            std::array<uint8_t, 32> hmac, int32_t action, int32_t actionButton, int32_t flags,
            int32_t edgeFlags, int32_t metaState, int32_t buttonState,
            MotionClassification classification, const ui::Transform& transform, float xPrecision,
            float yPrecision, float xCursorPosition, float yCursorPosition,
            const ui::Transform& rawTransform, nsecs_t downTime, nsecs_t eventTime,
            uint32_t pointerCount, const PointerProperties* pointerProperties,
            const PointerCoords* pointerCoords) {
      	
      	InputMessage msg;
        msg.header.type = InputMessage::Type::MOTION;
        msg.header.seq = seq;
        msg.body.motion.eventId = eventId;
        msg.body.motion.deviceId = deviceId;
        msg.body.motion.source = source;
      	//.......
      	return mChannel->sendMessage(&msg);
    }

mChannel是之前创建的InputChannel对象serverChannel,查看sendMessage(&msg)方法:

    // InputTransport.cpp
    status_t InputChannel::sendMessage(const InputMessage* msg) {
        const size_t msgLength = msg->size();
        InputMessage cleanMsg;
        msg->getSanitizedCopy(&cleanMsg);
        ssize_t nWrite;
        do {
            nWrite = ::send(getFd(), &cleanMsg, msgLength, MSG_DONTWAIT | MSG_NOSIGNAL);
        } while (nWrite == -1 && errno == EINTR);
        return OK;
    }

调用socket的send()函数发送数据。

至此输入事件通过socket发送出去了,InputDispatcher执行回调LooperEventCallback,那么事件又是如何被接收的呢?

三、InputEventReceiver

ViewRootImpl在setView()方法实例化InputChannel后,作为参数初始化WindowInputEventReceiver对象,WindowInputEventReceiver继承InputEventReceiver类。

3.1 WindowInputEventReceiver

WindowInputEventReceiver也在ViewRootImpl中定义:

    // 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 {
                    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 {
                    enqueueInputEvent(event, this, 0, true);
                }
            }
    }
    
    // InputEventReceiver.java
    public InputEventReceiver(InputChannel inputChannel, Looper looper) {
        mInputChannel = inputChannel;
        mMessageQueue = looper.getQueue();
        mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
                    mInputChannel, mMessageQueue);
    
        mCloseGuard.open("InputEventReceiver.dispose");
    }

WindowInputEventReceiver构造方法中将参数传递给了父类InputEventReceiver,实现了onInput()方法,后续接收到事件后通过这个方法处理Java层的分发逻辑,Eventlooper是ViewRoolImpl的当前线程Looper.myLooper(),也就是主线程,InputEventReceiver调用nativeInit()继续下一步操作,创建NativeInputEventReceiver。

3.2 NativeInputEventReceiver

NativeInputEventReceiver定义在JNI文件中,方便后续回调JAVA方法。

    // android_view_InputEventReceiver.cpp
    static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak,
            jobject inputChannelObj, jobject messageQueueObj) {
      	// 获取InputChannel
        std::shared_ptr<InputChannel> inputChannel =
                android_view_InputChannel_getInputChannel(env, inputChannelObj);	
      	// 获取messageQueue
        sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
      	// 1. 实例化NativeInputEventReceiver
        sp<NativeInputEventReceiver> receiver = new NativeInputEventReceiver(env,
                receiverWeak, inputChannel, messageQueue);
      	// 2. receiver->initialize()
        status_t status = receiver->initialize();	
        receiver->incStrong(gInputEventReceiverClassInfo.clazz); // retain a reference for the object
        return reinterpret_cast<jlong>(receiver.get())
    }

nativeInit()主要做了两件事,实例化NativeInputEventReceiver,并且调用其initialize()方法。

3.2.1 NativeInputEventReceiver实例化

    // android_view_InputEventReceiver.cpp
    NativeInputEventReceiver::NativeInputEventReceiver(
            JNIEnv* env, jobject receiverWeak, const std::shared_ptr<InputChannel>& inputChannel,
            const sp<MessageQueue>& messageQueue)
          : mReceiverWeakGlobal(env->NewGlobalRef(receiverWeak)),
            mInputConsumer(inputChannel),
            mMessageQueue(messageQueue),
            mBatchedInputEventPending(false),
            mFdEvents(0) {
    }

NativeInputEventReceiver构造方法中持有Java层InputEventRecevier对象的引用mReceiverWeakGlobal, 将inputChannel作为参数实例化mInputConsumer对象。

InputConsumer类定义在InputTransport.h头文件,从InputChannel消费事件。

    // InputTransport.cpp
    InputConsumer::InputConsumer(const std::shared_ptr<InputChannel>& channel)
          : mResampleTouch(isTouchResamplingEnabled()), mChannel(channel), mMsgDeferred(false) {}

client端的inputChannel被赋值给InputConsumer的mChannel变量。

回到NativeInputEventReceiver初始化流程中。

3.2.2 NativeInputEventReceiver::initialize

    // android_view_InputEventReceiver.cpp
    status_t NativeInputEventReceiver::initialize() {
        setFdEvents(ALOOPER_EVENT_INPUT);
        return OK;
    }
    
    void NativeInputEventReceiver::setFdEvents(int events) {
        if (mFdEvents != events) {
            mFdEvents = events;
            int fd = mInputConsumer.getChannel()->getFd();
            if (events) {
                mMessageQueue->getLooper()->addFd(fd, 0, events, this, nullptr);
            } else {
                mMessageQueue->getLooper()->removeFd(fd);
            }
        }
    }

initialize()监听client InputChannel的文件描述符 fd,文件类型文件类型和发送端一样,也是ALOOPER_EVENT_INPUT。在上述1.1.1小节中我们知道后续会回调LooperCallback的handleEvent()方法,NativeInputEventReceiver也继承了LooperCallback类,实现了自己的handleEvent():

3.3 handleEvent

    // android_view_InputEventReceiver.cpp
    int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) {
        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;
        }
        if (events & ALOOPER_EVENT_OUTPUT) {
            const status_t status = processOutboundEvents();
            if (status == OK || status == WOULD_BLOCK) {
                return KEEP_CALLBACK;
            } else {
                return REMOVE_CALLBACK;
            }
        }
      	return KEEP_CALLBACK;
    }

handleEvent只是对事件类型进行区分,如果是ALOOPER_EVENT_INPUT类型,详细具体到处理流程在consumeEvents()方法。

3.4 consumeEvents

    status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,
            bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {
      	for (;;) {
            uint32_t seq;
          	// 1.InputConsumer从socket读取封装事件
            InputEvent* inputEvent;
            status_t status = mInputConsumer.consume(&mInputEventFactory,
                    consumeBatches, frameTime, &seq, &inputEvent);
          	
          	// 2.区分事件类型,封装为Java层的InputEvent对象
            jobject inputEventObj;
            switch (inputEvent->getType()) {
                case AINPUT_EVENT_TYPE_KEY:
                    inputEventObj = android_view_KeyEvent_fromNative(env,
                            static_cast<KeyEvent*>(inputEvent));
                    break;
    
                case AINPUT_EVENT_TYPE_MOTION: {
                    MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);
                    if ((motionEvent->getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) {
                        *outConsumedBatch = true;
                    }
                    inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);
                    break;
                }
            }
          	
          	// 3.调用Java层InputEventReceiver对象的dispatchInputEvent方法,参数为seq,inputEventObj
          	if (inputEventObj) {
              	env->CallVoidMethod(receiverObj.get(),
                            gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);
                env->DeleteLocalRef(inputEventObj);
            }
        
      	}
    }

consumeEvents()处理了事件接收的主要流程:

  1. 启动一个死循环,不断从InputConsumer获取事件,赋值到inputEvent引用,InputConsumer持有客户端InputChannel的引用,从客户端socket读取数据,将数据解析封装成InputEvent对象。
  2. 将InputEvent类型进行区分,具体共有AINPUT_EVENT_TYPE_KEY,AINPUT_EVENT_TYPE_MOTION,AINPUT_EVENT_TYPE_FOCUS,AINPUT_EVENT_TYPE_CAPTURE,AINPUT_EVENT_TYPE_DRAG,AINPUT_EVENT_TYPE_TOUCH_MODE六种类型,对于Motion事件,封装成Java层的MotionEvent对象。
  3. receiverObj指向NativeInputEventReceiver实例化时传递过来的Java层InputEventReceiver引用,调用Java层InputEventReceiver对象的dispatchInputEvent方法,参数为seq,inputEventObj,至此回到了Java层的处理流程。

在分析Java层的处理流程之前,先看下InputConsumer是如何读取socket数据解析成InputEvent对象的。

3.4.1 InputConsumer.consume

    // InputTransport.cpp
    status_t InputConsumer::consume(InputEventFactoryInterface* factory, bool consumeBatches,
                                    nsecs_t frameTime, uint32_t* outSeq, InputEvent** outEvent) {
        while (!*outEvent) {
            status_t result = mChannel->receiveMessage(&mMsg);
          	switch (mMsgmMsg.header.type) {
                case InputMessage::Type::KEY: {
                    KeyEvent* keyEvent = factory->createKeyEvent();
                    if (!keyEvent) return NO_MEMORY;
    
                    initializeKeyEvent(keyEvent, &mMsg);
                    *outSeq = mMsg.header.seq;
                    *outEvent = keyEvent;
                  	break;
                }
                case InputMessage::Type::MOTION: {
                    MotionEvent* motionEvent = factory->createMotionEvent();
                    if (!motionEvent) return NO_MEMORY;
    
                    updateTouchState(mMsg);
                    initializeMotionEvent(motionEvent, &mMsg);
                    *outSeq = mMsg.header.seq;
                    *outEvent = motionEvent;
                  	break;
                }
            }
        }
        return OK;
    }

mChannel是NativeInputEventReceiver传递过去的客户端InputChannel对象,InputConsumer从mChannel读取消息,在上述第二小节:InputChannel发送事件 可知,发送的InputMessage对象封装了mMsg.header.type等事件类型消息,这里通过事件类型消息进行区分,封装成具体对应的输入事件,如KeyEvent,MotionEvent等,赋值给outEvent。

InputChannel通过recv()方法接收socket消息:

    // InputTransport.cpp
    status_t InputChannel::receiveMessage(InputMessage* msg) {
        ssize_t nRead;
        do {
            nRead = ::recv(getFd(), msg, sizeof(InputMessage), MSG_DONTWAIT);
        } while (nRead == -1 && errno == EINTR);
    }

3.4.2 dispatchInputEvent

事件的读取解析流程到此结束了,接下来看看InputReceiver是如何分发事件的。

    // InputEventReceiver.java
    		// 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调用onInputEvent(event)开始处理事件的分发流程,上述3.1小节可知,onInputEvent方法在WindowInputEventReceiver类中被覆盖。Java层的事件分发流程参考:https://editor.csdn.net/md/?articleId=130476234

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android InputChannel事件发送接收系统分析 的相关文章

随机推荐

  • ubuntu 开机启动顺序配置(包括选择ubuntu内核)

    sudo gedit etc default grub 编辑grub配置文件 GRUB DEFAULT改为0 为默认以第一个启动 若想选择ubuntu内核 可以通过 GRUB DEFAULT 61 34 1 gt 2 34 选择第二个内核
  • C++出现“field has incomplete type“问题的解决

    出现错误的原因一般类似下面这种代码 xff1a span class token keyword struct span span class token class name Data span span class token punc
  • java 容器都有哪些?

    容器可以说是Java Core中比较重要的一部分了 数组 String java util下的集合容器 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61
  • 树莓派3B+ 镜像烧录以及环境设置

    写在前面的话 xff1a 没啥好说的 相关软件下载 xff1a xff08 SD卡格式化工具 win32diskimager Raspbian系统镜像 Xshell ssh工具 xff09 xff09 链接 xff1a https pan
  • 串口调试助手

    一 前言 串口操作流程 xff1a 步骤一 xff1a 设置串口参数 xff0c 如 xff1a 波特率 xff0c 数据位 xff0c 奇偶校验 xff0c 停止位 xff0c 数据流控制等 步骤二 xff1a 选择串口 xff0c 如w
  • 【51单片机】INT0及INT1中断计数

    前言 刚刚本着负责任的心 xff0c 把上次的博客补全 xff08 真的有点长 xff0c 不过都是干货 xff09 xff0c 再回来的时候本次编辑就消失了 xff0c 下次记得保存线上草稿 友情链接 xff1a xff11 51单片机实
  • How To Install and Configure VNC Server on Ubuntu 20.04

    From https tecadmin net install vnc server on ubuntu 20 04 text 61 1 20How 20To 20Install 20and 20Configure 20VNC 20Serv
  • putty+Xming在客户端显示服务器的图形程序界面

    Xming是一个在 Microsoft Windows 操作系统上运行 X Window System 的自由软件 下载地址 xff1a https sourceforge net projects xming Putty的使用方法之前总结
  • C++中的头文件(.h)和源文件(.cpp)都应该写什么?

    头文件 h xff1a 写定义和声明 写类的声明 xff08 包括类里面的成员和方法的声明 xff09 函数原型 define常数等 xff0c 但是一般来说不写具体的实现 注意 xff1a 1 在写头文件的时候需要注意 xff0c 在开头
  • heroku命令整理

    access 管理用户对应用的访问 addons 用于开发 xff0c 扩展和操作您的应用程序的工具和服务 apps 管理应用 auth heroku 认证 authorizations OAuth 认证 buildpacks 管理应用程序
  • 解决com.google.code.kaptcha 从maven中央仓库无法下载的解决方案

    1 首先下载源码包 xff1a http code google com p kaptcha downloads list 2 将解压后的文件中kaptcha version jar kaptcha 3 2 2 jar copy出来放到其他
  • ubuntu16.04创建普通用户、ssh连接

    0 环境 ubuntu16 04 mobaXterm 1 创建用户 创建用户 xff0c 只需要一个命令就可以了 xff1a adduser your username 例子 xff1a adduser yumo passwd your u
  • 【数据清洗】图像数据清洗之---去除相似度高的图像

    目的 xff1a 人工做数据清洗较为麻烦 xff0c 而且费事费力没成绩 xff0c 还拉拽整个项目的后腿 所以这里根据调研情况 xff0c 分析尝试一下 1 调研分析 1 百度EasyData 参考 xff1a 百度大脑自己的csdn说明
  • pip 命令,向指定的python环境中安装包

    在linux中 xff0c 进入anaconda的虚拟环境之后 xff0c 使用pip并不一定会安装在当前环境下 xff08 和windows不太一样 xff09 xff0c 而是安装在该pip对应的python版本里 xff0c pip对
  • 快速区分主键与外键

    主键与外键的区分 主键用来唯一标识一条记录 xff0c 不允许有重复 xff0c 不允许为空 作用 xff1a 用来保证数据的完整性 个数 xff1a only one 外键 xff0c 表的外键是另一个表的主键 xff0c 外键可以有重复
  • module.exports和exports、export和export default、require和import的详解

    一 分类 commonJS xff1a 导出 xff08 module exports和exports xff09 导入 xff08 require xff09 ES6 xff1a 导出 xff08 export和export defaul
  • 如何查看静态编译的依赖(所链接的库)

    如何查看静态编译的依赖 实际上 静态库不存在依赖 依赖是动态编译下被动态链接的库 可以使用ldd查看 静态链接的话 所有需要的静态库会被添加到文件中 库名在连接的过程中会被剥除 如果文件包含debug 信息 可以通过查看符号的方式 对比静态
  • Hadoop URL读取数据

    URL setURLStreamHandlerFactory 每个虚拟机只能调用一次这个方法 xff0c 因此通常在静态中调用这个方法 xff01 这个限制以为着如果程序其他的组件已经声明一个实例 xff0c 则将无法使用这个方法读取 1
  • 【随记】Mac 取消系统更新的红点

    1 打开 系统偏好设置 点击 软件更新 2 取消选择 自动保持我的Mac最新 3 然后点击 高级 按钮 xff0c 取消所有的勾选 4 通过上面步骤设置后 xff0c 发现底部的小红点还在 xff0c 则需打开终端 xff0c 执行如下2段
  • Android InputChannel事件发送接收系统分析

    本文基于Android12 InputChannel表示其他进程通过文件描述符传递输入事件到View的通道 xff0c 因为需要跨进程传输 xff0c 实现了Parcelable序列化接口 xff0c 所以也能够理解Java层的InputC