Android深入源码分析事件分发机制流程

news2024/11/24 14:03:22

前言

对于Android中的触摸事件即指手指触摸到屏幕时产生的点击事件;

类型如下:

  • MotionEvent.ACTION_DOWN
  • MotionEvent.ACTION_UP
  • MotionEvent.ACTION_MOVE
  • MotionEvent.ACTION_CANCEL

点击事件

Android事件处理流程

事件处理流程
主要涉及三个流程:事件采集事件中转以及事件分发

在Android中,Touch事件的分发分服务端和应用端。在服务端由WindowManagerService(借助InputManagerService)负责采集和分发的,在应用端则是由ViewRootImpl(内部有一个mView变量指向View树的根,负责控制View树的UI绘制和事件消息的分发)负责分发的。

当输入设备可用时,比如触屏,Linux内核会在dev/input中创建对应的设备节点,输入事件所产生的原始信息会被Linux内核中的输入子系统采集,原始信息由Kernel Space的驱动层一直传递到User Space的设备节点;

IMS所做的工作就是监听/dev/input/下的所有设备节点,当设备节点有数据时会将数据进行加工处理并找到合适的Window,将输入事件分发给它

Linux相关函数简介

IMS监听/dev/input设备节点,具体实现需要借助Linux相关函数,这里简单介绍下:

  • epoll函数【更多详解请参考:epoll详解】
  1. epoll_create:创建 epoll 对象,会占用一个fd值,在linux下可以通过查看/proc/进程id/fd/,可以看到具体的fd,在epoll使用完毕后,需要调用close()进行关闭;

  2. epoll_ctl:epoll的事件注册函数,它不同于select()函数是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型;

  3. epoll_wait:收集在epoll监控的事件中已经发送的事件;函数调用成功,返回对应I/O上已准备好的文件描述符数目。返回0表示已超时;

  • iNotify函数【更多详解请参考:Linux inotify详解】

它是一个内核用于通知用户空间程序文件系统变化的机制;

在用户态,inotify 通过三个系统调用和在返回的文件描述符上的文件 I/ 操作来使用,使用 inotify 的第一步是创建 inotify 实例:

int fd = inotify_init ();
每一个 inotify 实例对应一个独立的排序的队列。
文件系统的变化事件被称做 watches 的一个对象管理,每一个 watch 是一个二元组(目标,事件掩码),目标可以是文件或目录,事件掩码表示应用希望关注的 inotify 事件,每一个位对应一个 inotify 事件。Watch 对象通过 watch描述符引用,watches 通过文件或目录的路径名来添加。目录 watches 将返回在该目录下的所有文件上面发生的事件。

下面函数用于添加一个 watch:
int wd = inotify_add_watch (fd, path, mask);
fd 是 inotify_init() 返回的文件描述符,path 是被监视的目标的路径名(即文件名或目录名),mask 是事件掩码, 在头文件 linux/inotify.h 中定义了每一位代表的事件。可以使用同样的方式来修改事件掩码,即改变希望被通知的inotify 事件。Wd 是 watch 描述符。

下面的函数用于删除一个 watch:
int ret = inotify_rm_watch (fd, wd);
fd 是 inotify_init() 返回的文件描述符,wd 是 inotify_add_watch() 返回的 watch 描述符。Ret 是函数的返回值。

源码分析

从内核到IMS过程

我们从SystemServer.startOtherServices()方法出发:

#SystemServer.startOtherServices
			....
            inputManager = new InputManagerService(context);
            ...
            inputManager.setWindowManagerCallbacks(wm.getInputManagerCallback());
            inputManager.start();
     		...
            

这里会启动InputManagerService,我们看下InputManagerService的构造方法以及start方法都做了些什么?

    public InputManagerService(Context context) {
        this.mContext = context;
        this.mHandler = new InputManagerHandler(DisplayThread.get().getLooper());
        mStaticAssociations = loadStaticInputPortAssociations();
		...
        mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());//调用nativeInit方法
       ...
    }


    public void start() {
    	...
        nativeStart(mPtr);//调用nativeStart方法
		...
    }

我们找到InputManagerService.cpp文件里的nativeInit以及nativeStart方法:
InputManagerService
找到InputManager.cpp文件,对应构造方法以及start方法如下:
InputManager.cpp
可以看到InputManager主要做以下几件事:

  • 构造InputDispatcher对象;(用于后续事件分发处理)
  • 构造InputReader对象;(用于事件输入监听)
  • 调用InputDispatcher和InputReader的start()方法;

Q:那InputReader是如何监听事件输入的呢?

我们从InputReader::start方法入手
InputReader::start
我们看下loopOnce函数做了什么?

void InputReader::loopOnce() {
    int32_t oldGeneration;
    int32_t timeoutMillis;
    bool inputDevicesChanged = false;
    std::vector<InputDeviceInfo> inputDevices;
    { 
    ...
    size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE); //从EventHub中读取事件,需要注意内部使用epoll机制实现,调用了epoll_wait进行阻塞,当fd发生变化的时候执行后面的代码
    { 
    	....
        if (count) {
            processEventsLocked(mEventBuffer, count); //处理事件
        }

    ...

    // 当输入设备的描述更改时发送一个消息
    if (inputDevicesChanged) {
        mPolicy->notifyInputDevicesChanged(inputDevices);
    }

    //将事件传递给InputDispatcher
    mQueuedListener->flush();
}

先不着急分析 processEventsLocked方法是如何来处理事件的,我们先了解下EventHub

先看下EventHub的构造方法:


EventHub::EventHub(void)
      : mBuiltInKeyboardId(NO_BUILT_IN_KEYBOARD),
        mNextDeviceId(1),
        mControllerNumbers(),
        mOpeningDevices(nullptr),
        mClosingDevices(nullptr),
        mNeedToSendFinishedDeviceScan(false),
        mNeedToReopenDevices(false),
        mNeedToScanDevices(true),
        mPendingEventCount(0),
        mPendingEventIndex(0),
        mPendingINotify(false) {
    ensureProcessCanBlockSuspend();

    mEpollFd = epoll_create1(EPOLL_CLOEXEC);//调用epoll_create创建 eventpoll 对象
    mINotifyFd = inotify_init(); //调用inotify_init
    mInputWd = inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);  	//调用inotify_add_watch设置监听目标路径为DEVICE_PATH,即/dev/input/
 	...
    int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);//调用epoll_ctl注册监听事件
	...
}

再看下mEventHub->getEvents()方法:

size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {
  ...
    for (;;) {
     	...
        int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);//调用epoll_wait监听事件输入

        if (pollResult == 0) {
            // Timed out.
            mPendingEventCount = 0;
            break;
        }

        if (pollResult < 0) {
            // An error occurred.
            mPendingEventCount = 0;

            // Sleep after errors to avoid locking up the system.
            // Hopefully the error is transient.
            if (errno != EINTR) {
                ALOGW("poll failed (errno=%d)\n", errno);
                usleep(100000);
            }
        } else {
            // Some events occurred.
            mPendingEventCount = size_t(pollResult);
        }
    }

    return event - buffer;
}

可以看到EventHub内部使用epoll函数以及inotify函数实现事件监听回调

我们继续跟进事件处理函数:processEventsLocked(mEventBuffer, count)
processEventsLocked
继续跟进processEventsForDeviceLocked()
processEventsForDeviceLocked
可以看到会交给device-->process方法处理:
我们找到InputDevice.cpp文件对应的process函数
InputDevice.process
mapper.process会交给TouchInputMapper::process处理:
TouchInputMapper::process
紧接着调用processRawTouches方法
processRawTouches
调用cookAndDispatch方法,最终会调用TouchInputMapper::dispatchMotion方法
TouchInputMapper::dispatchMotion
最终会执行getListener()-->notifyMotion方法,那这里的getListener()是谁呢?

InputListenerInterface* InputReader::ContextImpl::getListener() {
    return mReader->mQueuedListener.get();
}

InputReader构造函数
InputManager构造方法
几者结合可以得出结论:getListener()即为InputDispatcher对象!

至此,InputReader完成事件的监听工作并后续交给InputDispatcher进行分发!

InputReader监听输入整体流程

InputReader监听输入整体流程

Q:InputDispatcher又是如何进行事件分发的呢?

我们先看下InputDispatcher::notifyMotion方法:

void InputDispatcher::notifyMotion(const NotifyMotionArgs* args) {
	...
    if (needWake) {
        mLooper->wake(); //会调用mLooper->wake()唤醒InputDispatcher线程
    }
}

InputReader类似,InputDispatcher::start方法同样会开启一个线程,并执行dispatchOnce()函数
InputDispatcher::start
dispatchOnce()函数

我们看下dispatchOnceInnnerLocked函数

void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
   
	...
    switch (mPendingEvent->type) {
    		...
    		//这里我们只关注motion事件
        case EventEntry::Type::MOTION: {
            MotionEntry* typedEntry = static_cast<MotionEntry*>(mPendingEvent);
            if (dropReason == DropReason::NOT_DROPPED && isAppSwitchDue) {
                dropReason = DropReason::APP_SWITCH;
            }
            if (dropReason == DropReason::NOT_DROPPED && isStaleEvent(currentTime, *typedEntry)) {
                dropReason = DropReason::STALE;
            }
            if (dropReason == DropReason::NOT_DROPPED && mNextUnblockedEvent) {
                dropReason = DropReason::BLOCKED;
            }
            done = dispatchMotionLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);
            break;
        }
    }
	...
    }
}

dispatchOnceInnerLocked函数中会调用dispatchMotionLocked方法

bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, MotionEntry* entry,
                                           DropReason* dropReason, nsecs_t* nextWakeupTime) {
   bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, MotionEntry* entry,
                                           DropReason* dropReason, nsecs_t* nextWakeupTime) {
    ATRACE_CALL();
  	...
    if (isPointerEvent) {
        // 1.findTouchedWindowTargetsLocked 负责找到要进行分发的window
        injectionResult =findTouchedWindowTargetsLocked(currentTime, *entry, inputTargets, nextWakeupTime,
                                               &conflictingPointerActions);
    } else {
        injectionResult = findFocusedWindowTargetsLocked(currentTime, *entry, inputTargets, nextWakeupTime);
    }
    if (injectionResult == INPUT_EVENT_INJECTION_PENDING) {
        return false;
    }

    setInjectionResult(entry, injectionResult);
  	...
  	//2.具体事件分发
    dispatchEventLocked(currentTime, entry, inputTargets);
    return true;
}
}

会调用dispatchEventLocked方法,后续又经过层层调用最终会调用InputDispatcher::startDispatchCycleLocked方法

void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,
                                               const sp<Connection>& connection) {
    	 ...
    	 case EventEntry::Type::MOTION: { //这里,只关注Motion事件
                MotionEntry* motionEntry = static_cast<MotionEntry*>(eventEntry);
                // Publish the motion event.
                status = connection->inputPublisher.publishMotionEvent(...);
                break;
            }
		...
    }
}

inputPublisher.publishMotionEvent会执行InputPublisher::publishMotionEvent方法

status_t InputPublisher::publishMotionEvent(
        uint32_t seq, int32_t eventId, int32_t deviceId, int32_t source, int32_t displayId,
        std::array<uint8_t, 32> hmac, int32_t action, int32_t actionButton, int32_t flags,
        int32_t edgeFlags, int32_t metaState, int32_t buttonState,
        MotionClassification classification, float xScale, float yScale, float xOffset,
        float yOffset, float xPrecision, float yPrecision, float xCursorPosition,
        float yCursorPosition, nsecs_t downTime, nsecs_t eventTime, uint32_t pointerCount,
        const PointerProperties* pointerProperties, const PointerCoords* pointerCoords) {
   
	...
    InputMessage msg;
    msg.header.type = InputMessage::Type::MOTION;
    msg.body.motion.seq = seq;
    msg.body.motion.eventId = eventId;
    msg.body.motion.deviceId = deviceId;
    msg.body.motion.source = source;
    msg.body.motion.displayId = displayId;
    msg.body.motion.hmac = std::move(hmac);
    msg.body.motion.action = action;
    msg.body.motion.actionButton = actionButton;
    msg.body.motion.flags = flags;
    msg.body.motion.edgeFlags = edgeFlags;
    msg.body.motion.metaState = metaState;
    msg.body.motion.buttonState = buttonState;
    msg.body.motion.classification = classification;
    msg.body.motion.xScale = xScale;
    msg.body.motion.yScale = yScale;
    msg.body.motion.xOffset = xOffset;
    msg.body.motion.yOffset = yOffset;
    msg.body.motion.xPrecision = xPrecision;
    msg.body.motion.yPrecision = yPrecision;
    msg.body.motion.xCursorPosition = xCursorPosition;
    msg.body.motion.yCursorPosition = yCursorPosition;
    msg.body.motion.downTime = downTime;
    msg.body.motion.eventTime = eventTime;
    msg.body.motion.pointerCount = pointerCount;
    for (uint32_t i = 0; i < pointerCount; i++) {
        msg.body.motion.pointers[i].properties.copyFrom(pointerProperties[i]);
        msg.body.motion.pointers[i].coords.copyFrom(pointerCoords[i]);
    }

    return mChannel->sendMessage(&msg);
}

status_t InputPublisher::publishFocusEvent(uint32_t seq, int32_t eventId, bool hasFocus,
                                           bool inTouchMode) {
    if (ATRACE_ENABLED()) {
        std::string message =
                StringPrintf("publishFocusEvent(inputChannel=%s, hasFocus=%s, inTouchMode=%s)",
                             mChannel->getName().c_str(), toString(hasFocus),
                             toString(inTouchMode));
        ATRACE_NAME(message.c_str());
    }

    InputMessage msg;
    msg.header.type = InputMessage::Type::FOCUS;
    msg.body.focus.seq = seq;
    msg.body.focus.eventId = eventId;
    msg.body.focus.hasFocus = hasFocus ? 1 : 0;
    msg.body.focus.inTouchMode = inTouchMode ? 1 : 0;
    return mChannel->sendMessage(&msg);
}

可以看到主要是将数据进行封装成InputMessage对象,并最终交给InputChannel进行发送;

InputDispatcher分发事件流程

InputDispatcher分发事件流程

从IMS到WMS过程

从上面可以知道:InputReaderThreadInputDispatcherThread是运行在SystemServer进程中的;
和用户进程并不在同一个进程中,那中间一定存在进程间通信机制;
InputChannel目的正是为了与用户进程进行通信,InputChannel是由WMS管理,触发构建生成;

  1. 首先,在ViewRootImpl中的setView函数中会调用mWindowSession.addToDisplayAsUser()方法;
    mWindowSession.addToDisplayAsUser()
    这里在java层面构建了一个InputChannel对象;

  2. addToDisplayAsUser会调用到WindowlessWindowManager.addToDisplay()方法;
    WindowlessWindowManager.addToDisplay()
    这里mRealWmIWindowSession,我们知道它其实是WMS的代理 mRealWm.grantInputChannel()方法实际上调用的是WindowManagerService.grantInputChannel()方法;
    WindowManagerService.grantInputChannel()
    openInputChannel方法
    InputChannel::openInputChannelPair
    InputManager::registerInputChannel
    SocketPair:Linux实现了一个socketpair调用支持在同一个文件描述符中进行读写功能
    简单理解下:InputChannel内部使用Linux的socketpair实现不同进程间通信机制;

  3. 继续回到setView中,在创建了InputChannel之后,就开启了对InputChannel中输入事件的监听:
    ViewRootImpl.setView
    WindowInputEventReceiver继承于InputEventReceiver
    WindowInputEventReceiver
    我们看下InputEventReceiver的构造方法:
    InputEventReceiver.javanativeInit
    setFdEvents
    NativeInputEventReceiver::setFdEvents
    最终会调用Looper::addFd()该方法会对传递的fd添加epoll监控,Looper会循环调用pollOnce 方法,会等待消息的到来,当消息到来后,会根据消息类型一些判断处理,然后调用对应的callback函数
    这里我们当前是对开启的socket进行监听,当有数据到来时,我们会执行对应的回调,这里对于InputChannel的回调对应NativeInputEventReceiver::handleEvent方法:
    NativeInputEventReceiver::handleEvent
    NativeInputEventReceiver::consumeEvents
     InputConsumer::consume
    CallVoidMethod
    mChannel->receiveMessage接收完毕后回到consumeEvent方法中,进行数据相关判断,最终会执行CallVoidMethod方法,回调JAVA函数:dispatchInputEvent
    dispatchInputEvent
    最终交给WindowInputEventReceiver.onInputEvent方法完成后续逻辑处理
    WindowInputEventReceiver.onInputEvent

IMS到WMS流程图

IMS到WMS流程图

从WMS到ViewRootImpl过程

这一块相关流程之前在Android WMS工作原理浅析(二)分析过,这里不在赘述;
ViewRootImpl事件分发

结语

如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )

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

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

相关文章

电子水尺的应用

品概述 本产品是一种采用微处理器芯片为控制器&#xff0c;内置通讯电路的数字式水位传感器&#xff0c;具备高的可靠性及抗干扰性能。适用于江、河、湖、水库及蓄水池、水渠等处的水位测量使用。 本产品使用不锈钢材料做壳体防护材料&#xff0c;内部用高性能的密封材料进行处…

11省政企单位密集调研实在智能数字员工

当前&#xff0c;数字中国建设迎来前所未有的发展机遇。五月&#xff0c;“数字中国”建设持续如火如荼&#xff0c;实在智能又迎来了新的“考察团路线”&#xff1a;西部新宁甘云中轴线晋豫鄂湘东部鲁苏闽&#xff0c;来自11省&#xff0c;20余个“数字化改革政企考察团”&…

【OS】Python模拟简单的操作系统

【OS】Python模拟简单的操作系统 Background 学习操作系统的时候&#xff0c;关注的对象是&#xff1a; 应用程序系统调用操作系统的内部实现 通常的学习思路&#xff1a; 真实的操作系统编译内核环境qemu模拟 但是&#xff0c;换个角度想一下&#xff0c;把上述的思路抽…

智能蓝牙防丢器

蓝牙防丢器&#xff0c;是采用蓝牙技术专门为智能手机设计的防丢器。其工作原理主要是通过距离变化来判断物品是否还控制在你的安全范围。主要适用于手机、钱包、钥匙、行李等贵重物品的防丢&#xff0c;也可用于防止儿童或宠物的走失。防丢器的原理是在手机和蓝牙之间建立连接…

Nginx一网打尽:动静分离、压缩、缓存、黑白名单、跨域、高可用、性能优化...

干货&#xff01;文章有点长&#xff0c;建议先收藏 目录索引 引言一、性能怪兽-Nginx概念深入浅出二、Nginx环境搭建三、Nginx反向代理-负载均衡四、Nginx动静分离五、Nginx资源压缩六、Nginx缓冲区七、Nginx缓存机制八、Nginx实现IP黑白名单九、Nginx跨域配置十、Nginx防盗链…

黑客必会的10个渗透测试工具

10个渗透测试工具&#xff0c;渗透测试&#xff0c;在之前&#xff0c;黑客攻击是很难的&#xff0c;需要大量的手工操作。然而&#xff0c;今天一套完整的自动化测试工具将黑客变成了半机械人&#xff0c;和以往相比&#xff0c;他们可以进行更多的测试。下面是一些渗透测试工…

Android Jetpack Compose之列表的使用

目录 概述实例解析1.实现简单的列表2.实现可滑动的菜单列表3.实现一个可滑动并且能快速滑动到指定位置的列表 总结 概述 在Android的传统View中&#xff0c;当我们需要展示大量的数据时&#xff0c;一般都会使用ListView或者是更高级的RecyclerView。在Compose中我们可以通过C…

openGauss Developer Day 2023 | 邀您参加南大通用分论坛

聚数成峰 共赢未来 面向数据库开发者的年度技术盛会 openGauss Developer Day 2023 将于5月25-26日在北京召开 GBASE南大通用将携创新数据库产品及行业解决方案亮相本届大会。 5月26日 &#xff0c;更设有南大通用 “多模多态分布式数据库助力数字化转型” 专场论坛&am…

SSM 如何使用 XA 机制实现分布式事务?

SSM 如何使用 XA 机制实现分布式事务&#xff1f; 分布式事务是现代分布式系统中必不可少的一部分&#xff0c;而 XA 机制是一种常用的分布式事务处理方式。在 SSM 框架中&#xff0c;我们可以使用 XA 机制来管理分布式事务。本文将介绍如何在 SSM 框架中使用 XA 机制实现分布…

Find My产品|苹果上架支持Find My功能的旅行保温杯

苹果美国官网近日上架了 Ember 的控温 Travel Mug 2 旅行杯&#xff0c;售价为 199.95 美元。该旅行杯最大的亮点就是支持“Find My”&#xff0c;丢失后可在 iPhone&#xff0c;iPad 和 Mac 上定位找回。 Travel Mug 2 旅行杯和此前推出的 Travel Mug 2 旅行杯在功能方面完全相…

统计软件与数据分析Lesson15----梯度下降(Gradient Descent)过程可视化

梯度下降 Gradient Descent 1.预备知识1.1 什么是机器学习&#xff1f;1.2 几个专业术语 2. 前期准备2.1 加载包2.2 定义模型2.3 生成模拟数据2.4 分割训练集验证集2.5 原始数据可视化 3. 模型训练Step 0: 随机初始化待估参数Step 1: 计算模型预测值Step 2: 计算预测误差&#…

ORB-LSAM2:ComputeKeyPointsOctTree()提取特征:maxY = iniY + hCell + 6 为怎么是+6而不是+3?

如标题所示&#xff0c;本博客主要讲述 void ORBextractor::ComputeKeyPointsOctTree(vector<vector<KeyPoint>> &allKeypoints){}函数中maxY iniY hCell 6 为怎么是6而不是3&#xff1f; 为了连续性&#xff0c;会介绍一下ComputeKeyPointsOctTree函数&a…

【Git】git仓库的 .git 下各个目录注释

解释&#xff1a; .git 目录是Git版本控制系统的核心&#xff0c;它包含了Git所需要的所有信息&#xff0c;包括版本历史、分支、标签、配置等。下面是一些常见的 .git 目录下的文件和目录的说明&#xff1a; HEAD&#xff1a;指向当前分支的最新提交。config&#xff1a;包含…

告别重复工作,用Python实现办公自动化,提高工作效率

996 一直是互联网老生常谈的话题了&#xff0c;但抛开其他只谈工作本身&#xff0c;你有没有想过&#xff0c;下班晚、加班&#xff0c;有时候可能是因为自己工作比较低效&#xff1f; 先给你分享一个案例&#xff1a; 场景是在维护日活超过 3 亿用户的微博私信平台&#xff…

GIT合并分支的三种方法

一、使用merge命令合并分支 1、目标&#xff1a;将dev分支合并到master分支 1.1、首先切换到master分支上 git checkout master1.2、如果是多人开发的话 需要把远程master上的代码pull下来 git pull origin master //如果是自己一个开发就没有必要了&#xff0c;为了保险期…

ospf的rip和ospf互通以及配置stub区域和totally stub

1. ospf与rip如何互通 我们需要在两台路由器上互相引入,如上图 AR5和AR6运行了rip,但AR5也运行了ospf要想路由器能够互相学习到路由,就需要在AR5上配置路由协议引入 什么是stub区域如何配置stub区域 Stub区域的功能&#xff1a;过滤4类LSA和5类LSA&#xff0c;对外产生缺省的…

新星计划2023【网络应用领域基础】-------------Day2

计算机网络基础讲解 目录 计算机网络基础讲解​编辑​编辑 前言&#xff1a; 一&#xff0c;OSI参考模型​编辑 在说osi模型之前我先说说一些常见的标准机构&#xff1a; 这里对应用层一个小科普​编辑 二&#xff0c;TCP/IP协议​编辑 TCP/IP和OSI模型的主要区别在哪里&…

openGauss Developer Day 2023 | 邀您参加云和恩墨分论坛

5月25-26日&#xff0c; openGauss Developer Day 2023 将于 北京昆泰嘉瑞文化中心 举办&#xff0c;云和恩墨将携三款数据库创新产品亮相本次大会&#xff0c;并将在 26日下午 于 2F时代厅2厅 举办主题为“ 耕获菑畬&#xff0c;继往开来 ”的数据库技术创…

C++设计模式学习(二)

模板方法 GOF-23模式分类 从目的来看: 创建型(Creational)模式:将对象的部分创建工作延迟到子类或者其他对象,从而应对需求变化为对象创建时具体类型实现引来的冲击。结构型(Structural)模式:通过类继承或者对象组合获得更灵活的结构,从而应对需求变化为对象的结构带来的冲击…

python+django家庭个人理财收支管理系统5x6nf

根据收支管理系统的功能需求&#xff0c;进行系统设计。 用户功能&#xff1a;用户进入系统可以实现每日收入、每日支出等功能进行操作&#xff1b; 管理员功能&#xff0c;管理员功能包括用户管理、收入分类、支出分类、每日收入、每日支出等功能管理&#xff1b; 决当前的问题…