[Framework] Android Handler 工作原理

news/2024/10/9 11:23:17/

Android 中的 Handler 都被人说烂了,但是还是想多说一次,因为在 Android 的系统中它真的非常重要而且它的机制并没有很复杂,无论是新手和老手都可以好好学习下,这对理解 Android 系统很重要,所以说学习的性价比非常高。
Android 中 IPC 跨进程通信主要依靠 Binder , 而同一个进程的通信就主要依靠 Handler

这里先简单描述下 Handler 的工作流程:
首先在 Handler 处理任务的线程需要调用 Looper#prepare() 方法,来为当前线程创建一个 Looper 实例,这个实例通过 ThreadLocal 存放,也就是一个线程只能有一个这个实例;Looper 构造函数中会初始化一个 MessageQueue 对象,MessageQueue 对象就是用来封装任务的队列,它是用的链表实现,而任务队列的 Item 就是 Message 对象,也就是单个的任务;任务处理线程调用 Looper#prepare() 方法完成后,会调用 Looper#loop() 方法,这个时候任务线程就会阻塞去处理对应 Looper 中的 MessageQueue 中的任务,处理的方式就是一个死循环从 MessageQueue 中去获取最新的 Message 获取到后,然后调用对应的 Handler#handleMessage() 方法去执行它,执行完成后又去获取下一个任务,如果没有新的任务就会陷入阻塞,等有新的任务来的时候会唤醒这个阻塞(这个唤醒机制是用的 Linux 中的 epoll 机制),继续执行新的任务。

上面大致描述了 LooperMessageQueue 如何从队列中获取新的任务和执行任务,这里再简单描述下怎么插入任务,首先要自定一个 Handler 对象,构造函数中需要传入上面所创建的 Looper 对象,其中自定义的 Handler#handleMessage 方法就是用来执行任务的方法,其他的线程需要向对应的 Looper 线程添加任务时,就调用上面 Handler 实例的 sendMessage 方法来添加任务,也就是向 MessageQueue 队列中添加任务,最终会在 Looper 所对应的线程执行,这样就完成了一次跨线程的通信。
下面是一个简单的 Handler 使用的代码:


override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)var handlerLooper: Looper? = nullval handlerThread = object : Thread() {override fun run() {super.run()Looper.prepare()handlerLooper = Looper.myLooper()Looper.loop()}}handlerThread.start()while (handlerLooper == null) {// 等待 HandlerThread 初始化完成}val handler = object : Handler(handlerLooper!!) {override fun handleMessage(msg: Message) {super.handleMessage(msg)if (msg.what == 0) {// TODO: 处理任务,该方法最终工作在 HandlerThread 线程}}}val msg = handler.obtainMessage(0)// 发送的代码工作在主线程handler.sendMessage(msg)
}

Android 中有现成的 HandlerThread,不用自定义,我这里是为了展示这个过程。

Tips: 我后续的源码分析都是基于 andorid 31

Looper

Looper 初始化

// ...
public static void prepare() {prepare(true);
}private static void prepare(boolean quitAllowed) {if (sThreadLocal.get() != null) {throw new RuntimeException("Only one Looper may be created per thread");}sThreadLocal.set(new Looper(quitAllowed));
}
// ...

// ...
private Looper(boolean quitAllowed) {mQueue = new MessageQueue(quitAllowed);mThread = Thread.currentThread();
}
// ...

初始化的过程非常简单,如果当前线程已经有 Looper 对象就直接报错,如果没有新建一个 Looper 对象放在 ThreadLocal 中,在 Looper 的构造函数中,创建了 MessageQueue 实例和保存了当前的 Thread 对象。

Looper 任务处理

// ...
public static void loop() {final Looper me = myLooper();if (me == null) {throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");}if (me.mInLoop) {Slog.w(TAG, "Loop again would have the queued messages be executed"+ " before this one completed.");}me.mInLoop = true;// ..me.mSlowDeliveryDetected = false;for (;;) {if (!loopOnce(me, ident, thresholdOverride)) {return;}}
}
// ...

Looper#loop() 方法也非常简单,修改了当前的 Looper 的一些状态,然后在死循环中无限调用 loopOnce() 方法去执行任务,如果该方法返回 false 就表示该 Looper 已经退出,就跳出循环。


private static boolean loopOnce(final Looper me,final long ident, final int thresholdOverride) {Message msg = me.mQueue.next(); // might blockif (msg == null) {// No message indicates that the message queue is quitting.return false;}// This must be in a local variable, in case a UI event sets the loggerfinal Printer logging = me.mLogging;if (logging != null) {logging.println(">>>>> Dispatching to " + msg.target + " "+ msg.callback + ": " + msg.what);}// ...Object token = null;if (observer != null) {token = observer.messageDispatchStarting();}long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);try {msg.target.dispatchMessage(msg);if (observer != null) {observer.messageDispatched(token, msg);}dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;} catch (Exception exception) {if (observer != null) {observer.dispatchingThrewException(token, msg, exception);}throw exception;} finally {ThreadLocalWorkSource.restore(origWorkSource);if (traceTag != 0) {Trace.traceEnd(traceTag);}}if (logging != null) {logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);}// ...msg.recycleUnchecked();return true;
}

直接调用 MessageQueue#next() 方法去获取 Message 对象,如果返回空就表示 Looper 已经退出了。这里有一个 Printer 对象,在 Message 执行前后都会打印对应的日志,可以通过调用 Looper#setMessageLogging() 方法来自定义这个对象,根据打印的日志内容我们就可以分辨是开始前还是开始后,在 APM 中经常通过这个方法来判断主线程的任务执行是否超时。执行任务会调用的是 handler.target#dispatchMessage() 方法,其实这个 target 对象就是 Handler,后续再分析这个方法。
这次看代码还有新发现,这里还多了一个 Observer 对象,我记得以前的旧版本是没有的,可以通过 Looper#setObserver() 方法来设置,它的接口如下:


public interface Observer {/*** Called right before a message is dispatched.** <p> The token type is not specified to allow the implementation to specify its own type.** @return a token used for collecting telemetry when dispatching a single message.*         The token token must be passed back exactly once to either*         {@link Observer#messageDispatched} or {@link Observer#dispatchingThrewException}*         and must not be reused again.**/Object messageDispatchStarting();/*** Called when a message was processed by a Handler.** @param token Token obtained by previously calling*              {@link Observer#messageDispatchStarting} on the same Observer instance.* @param msg The message that was dispatched.*/void messageDispatched(Object token, Message msg);/*** Called when an exception was thrown while processing a message.** @param token Token obtained by previously calling*              {@link Observer#messageDispatchStarting} on the same Observer instance.* @param msg The message that was dispatched and caused an exception.* @param exception The exception that was thrown.*/void dispatchingThrewException(Object token, Message msg, Exception exception);
}

Observer 可以监听任务开始,结束和异常,看上去它可以替换 Printer,不过它是对应用层隐藏的,需要骚操作。

Handler

Handler 初始化


public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {mLooper = looper;mQueue = looper.mQueue;mCallback = callback;mAsynchronous = async;
}

其中 LooperMessageQueue 就不用多说了;其中 mCallback 对象就是可以优先于 Handler#handleMessage 方法处理任务,也可以拦截任务不让 Handler#handleMessage 执行;async 是表示发送的消息是否是异步消息,这个和消息屏障关系密切,后续会再分析。

Handler 发送消息

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,long uptimeMillis) {msg.target = this;msg.workSourceUid = ThreadLocalWorkSource.getUid();if (mAsynchronous) {msg.setAsynchronous(true);}return queue.enqueueMessage(msg, uptimeMillis);
}

无论是调用 Handler 的哪个方法发送消息,最终都会到 enqueueMessage() 方法里面去,在 Looper 中我们说到 target 就是 Handler 对象,在这里就得到验证了,如果 Handler 的构造函数中设置为异步,Message 也会被设置为异步,然后直接将处理好的 Message 通过 MessageQueue#enqueueMessage() 方法添加到队列中。

Handler 处理消息

Looper 下发消息时说到最后会调用 HandlerdispatchMessage() 方法:


public void dispatchMessage(@NonNull Message msg) {if (msg.callback != null) {handleCallback(msg);} else {if (mCallback != null) {if (mCallback.handleMessage(msg)) {return;}}handleMessage(msg);}
}

如果 Message 中有设置 Callback 就直接在对应的 Callback 中处理,不继续下发; 如果 Message 中没有设置 Callback,会先检查 Handler 是否有设置 mCallback,如果有设置,调用 mCallback 处理,mCallbackhandleMessage 返回 true 就表示要拦截该消息,如果不拦截就交由 HandlerhandleMessage() 方法处理。

MessageQueue

MessageQueue 插入任务

在讲 Handler 的时候说到插入消息会直接调用 MessageQueue#enqueueMessage() 方法:


boolean enqueueMessage(Message msg, long when) {if (msg.target == null) {throw new IllegalArgumentException("Message must have a target.");}synchronized (this) {if (msg.isInUse()) {throw new IllegalStateException(msg + " This message is already in use.");}if (mQuitting) {IllegalStateException e = new IllegalStateException(msg.target + " sending message to a Handler on a dead thread");Log.w(TAG, e.getMessage(), e);msg.recycle();return false;}msg.markInUse();msg.when = when;Message p = mMessages;boolean needWake;if (p == null || when == 0 || when < p.when) {// New head, wake up the event queue if blocked.msg.next = p;mMessages = msg;needWake = mBlocked;} else {// Inserted within the middle of the queue.  Usually we don't have to wake// up the event queue unless there is a barrier at the head of the queue// and the message is the earliest asynchronous message in the queue.needWake = mBlocked && p.target == null && msg.isAsynchronous();Message prev;for (;;) {prev = p;p = p.next;if (p == null || when < p.when) {break;}if (needWake && p.isAsynchronous()) {needWake = false;}}msg.next = p; // invariant: p == prev.nextprev.next = msg;}// We can assume mPtr != 0 because mQuitting is false.if (needWake) {nativeWake(mPtr);}}return true;
}

如果 Message 没有设置 Handler 直接抛出异常;如果 Message 已经是 inUse 状态,直接抛出异常;如果 MessageQueue 已经退出,就直接回收 Message;按照任务的触发时间由小到大,插入到 mMessage 链表中,最后如果需要唤醒 next() 方法中的 nativePollOnce() 方法,就会调用 nativeWake 方法,这是一个 C++ 实现的方法(内部实现其实是向一个管道 fd 中写入了一个数字 1,通过 epoll 机制,会监听到管道 fd 有数据写入,然后会唤醒 nativePollOnce() 方法,然后去读取新的 Message)。

MessageQueue 读取任务

在讲 Looper 时讲到最后获取任务是通过 MessageQueue#next() 方法:


Message next() {// Return here if the message loop has already quit and been disposed.// This can happen if the application tries to restart a looper after quit// which is not supported.final long ptr = mPtr;if (ptr == 0) {return null;}int pendingIdleHandlerCount = -1; // -1 only during first iterationint nextPollTimeoutMillis = 0;for (;;) {if (nextPollTimeoutMillis != 0) {Binder.flushPendingCommands();}// 等待 epoll 唤醒,唤醒是等待 nativeWake() 方法调用或者超时(超时时间为 nextPollTimeoutMillis)nativePollOnce(ptr, nextPollTimeoutMillis);synchronized (this) {// Try to retrieve the next message.  Return if found.final long now = SystemClock.uptimeMillis();Message prevMsg = null;Message msg = mMessages;// target 为空,就表示这是一个屏障的消息if (msg != null && msg.target == null) {// Stalled by a barrier.  Find the next asynchronous message in the queue.// 找到屏障消息后的第一个异步消息do {prevMsg = msg;msg = msg.next;} while (msg != null && !msg.isAsynchronous());}if (msg != null) {if (now < msg.when) {// Next message is not ready.  Set a timeout to wake up when it is ready.// 当前消息的时间大于当前的时间,表示还没有到达执行的时间,计算间隔,等待重新进入 nativePollOnce 方法nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);} else {// 获取消息成功,把它从队列中移除,然后返回方法// Got a message.mBlocked = false;if (prevMsg != null) {prevMsg.next = msg.next;} else {mMessages = msg.next;}msg.next = null;if (DEBUG) Log.v(TAG, "Returning message: " + msg);msg.markInUse();return msg;}} else {// No more messages.nextPollTimeoutMillis = -1;}// Process the quit message now that all pending messages have been handled.if (mQuitting) {dispose();return null;}// 后续就是在没有消息时回调 IdleHandler// If first time idle, then get the number of idlers to run.// Idle handles only run if the queue is empty or if the first message// in the queue (possibly a barrier) is due to be handled in the future.// 如果 pendingIdleHandlerCount 小于 0 就表示当前的这一次 next() 方法调用,还没有执行过 IdleHandler,就需要去读取 IdleHandler 的数量,在一次 next() 方法调用时最多执行一次 IdleHandler。if (pendingIdleHandlerCount < 0&& (mMessages == null || now < mMessages.when)) {pendingIdleHandlerCount = mIdleHandlers.size();}if (pendingIdleHandlerCount <= 0) {// 这里就表示 IdleHandler 为空,或者已经执行过了。// No idle handlers to run.  Loop and wait some more.mBlocked = true;continue;}if (mPendingIdleHandlers == null) {mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];}mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);}// Run the idle handlers.// We only ever reach this code block during the first iteration.for (int i = 0; i < pendingIdleHandlerCount; i++) {final IdleHandler idler = mPendingIdleHandlers[i];mPendingIdleHandlers[i] = null; // release the reference to the handlerboolean keep = false;try {// 执行 idleHandlerkeep = idler.queueIdle();} catch (Throwable t) {Log.wtf(TAG, "IdleHandler threw exception", t);}// 如果不需要保留就把它移出if (!keep) {synchronized (this) {mIdleHandlers.remove(idler);}}}// Reset the idle handler count to 0 so we do not run them again.// 这里设置为 0 后,IdleHandler 就不会再次执行,需要等待下次的 next() 方法调用pendingIdleHandlerCount = 0;// While calling an idle handler, a new message could have been delivered// so go back and look again for a pending message without waiting.nextPollTimeoutMillis = 0;}
}

通过 nativePollOnce() 方法等待唤醒(通过调用 nativeWake() 方法)或者超时(超时时间是 nextPollTimeoutMillis),唤醒后直接获取最新的一条 Message(先跳过同步消息逻辑),然后判断任务执行的时间,如果大于当前时间就继续等待,直到可以执行,如果小于当前时间直接返回,同时把这条消息从队列中移除(先跳过 IdleHandler 逻辑)。

同步消息和消息屏障

如果 target 对象为空就表示该消息是一个同步屏障,如果是同步屏障的话他就会重新再去取该同步屏障消息后面的最新一个异步消息去执行,如果没有异步消息就会去等待。
通过以下方法添加屏障消息:


private int postSyncBarrier(long when) {// Enqueue a new sync barrier token.// We don't need to wake the queue because the purpose of a barrier is to stall it.synchronized (this) {final int token = mNextBarrierToken++;final Message msg = Message.obtain();msg.markInUse();msg.when = when;msg.arg1 = token;Message prev = null;Message p = mMessages;if (when != 0) {while (p != null && p.when <= when) {prev = p;p = p.next;}}if (prev != null) { // invariant: p == prev.nextmsg.next = p;prev.next = msg;} else {msg.next = p;mMessages = msg;}return token;}
}

其实就是在消息队列中添加了一个 target 为空的消息,同时在 Messagearg1 对象中添加 token 对象,然后这个 token 也会返回给调用方,这个 token 在移除屏障时需要用到。

通过以下方法可以移除这个消息屏障:


public void removeSyncBarrier(int token) {// Remove a sync barrier token from the queue.// If the queue is no longer stalled by a barrier then wake it.synchronized (this) {Message prev = null;Message p = mMessages;while (p != null && (p.target != null || p.arg1 != token)) {prev = p;p = p.next;}if (p == null) {throw new IllegalStateException("The specified message queue synchronization "+ " barrier token has not been posted or has already been removed.");}final boolean needWake;if (prev != null) {prev.next = p.next;needWake = false;} else {mMessages = p.next;needWake = mMessages == null || mMessages.target != null;}p.recycleUnchecked();// If the loop is quitting then it is already awake.// We can assume mPtr != 0 when mQuitting is false.if (needWake && !mQuitting) {nativeWake(mPtr);}}
}

我之前在 [Framework] Android Choreographer 工作原理有简单介绍过,在绘制的时候就会用到同步消息和消息屏障来提高绘制相关的 Message 的优先级。

请求绘制最终会到 ViewRootImpl#scheduleTraversals() 方法中,在这个方法中就会添加一个屏障:


void scheduleTraversals() {if (!mTraversalScheduled) {mTraversalScheduled = true;mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);notifyRendererOfFramePending();pokeDrawLockIfNeeded();}
}

Choregorapher 收到 Vsync 信号后会发送一个 Handler 的异步 Message:


public void onVsync(long timestampNanos, long physicalDisplayId, int frame,VsyncEventData vsyncEventData) {// ..Message msg = Message.obtain(mHandler, this);msg.setAsynchronous(true);mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);// ..
}

在绘制完成后会调用 ViewRootImpl#unscheduleTraversals() 方法移除这个屏障:

void unscheduleTraversals() {if (mTraversalScheduled) {mTraversalScheduled = false;mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);mChoreographer.removeCallbacks(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);}}
IdleHandler

IdleHandler 就是在 next() 方法获取不到消息时会执行一次,next() 方法调用一次最多只能执行一次 IdleHandler,执行 IdleHandler 的时候就表示当前的 Looper 比较空闲,IdleHandler 的数量最多为 4 个 (在 ActivityonDestroy() 回调就是在 IdleHandler 中执行的,可能写这部分代码的人,认为 onDestroy() 相对其他的生命周期没有那么重要,但是由于 IdleHandler 的特性也会引发一些问题,后续有时间会分析这个问题) 。
可以通过以下方法添加 IdleHandler

    public void addIdleHandler(@NonNull IdleHandler handler) {if (handler == null) {throw new NullPointerException("Can't add a null IdleHandler");}synchronized (this) {mIdleHandlers.add(handler);}}

epoll 在 Handler 中的工作方式

MessageQueue 的构造函数中会通过 Native 方法 nativeInit() 初始化 epoll,初始化流程会先通过 epoll_create() 方法创建一个 EpollFd,然后构建一个管道的 fd 用于 epoll 来监听它的读写状态,这个 fd 被命名为 WakeFd,然后通过 epoll_ctl() 方法将 WakeFd 添加到 EpollFd 中,以供后续通过 EpollFd 能够监听 WakeFd 的状态。
MessageQueuenext() 方法中会通过 Native 方法 nativePollOnce() 方法去监听上面所提到 WakeFd 的状态,Native 层是通过 epoll_wait() 方法去监听状态,这个方法会等到超时或者 WakeFd 有数据写入就会返回。
MessageQueueenqueueMessage() 方法中插入 Message 后,会判断是否需要唤醒 next() 中的 nativePollOnce() 方法,如果需要唤醒就会调用 Native 方法 nativeWake() 方法,Native 层就会向 WakeFd 中写入一个数字 1,然后 nativePollOnce() 间听到这个 WakeFd 写状态后就会返回,也就解除 next() 方法的阻塞去读取新的 Message

最后聊一聊主线程

在 Android Framework 中有两个主要的主线程 Handler,一个是 ActivityThread 中的 H,它主要来处理四大组建的各种生命周期;还有一个是 Choreographer 中的 FrameHandler,它主要负责绘制,动画,输入等操作。这两个 Handler 负责的工作和用户体验都极为密切,主线程也可以说非常忙。

在项目中我经常发现很多的代码都会在主线程执行,明明有的操作不需要主线程执行。比如说在一个网络请求的通用方法中,在网络完成的回调中就会主动切换到主线程,明明很多地方的代码不需要用到主线程,我个人的观点是一定需要切换主线程时再切换,在一定程度上能够缓解应用卡顿的问题。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap


http://www.ppmy.cn/news/1150129.html

相关文章

spark集群环境下,实现人口平均年龄计算

文章目录 任务目标0. 版本信息1. 计算生成renkou.txt2. 文件上传至spark3. 上传文件时&#xff0c;可能出现的常见错误4. 编写spark文件5. 上传集群6. 集群环境下提交任务 任务目标 在虚拟机上部署spark集群&#xff0c;给定renkou.txt文件&#xff0c;输出平均年龄 renkou.t…

FPGA面试题(1)

一.FPGA内部结构 可编程I/OPLL锁相环&#xff08;其作用为&#xff1a;分频&#xff0c;倍频&#xff0c;相位调节、占空比&#xff09;逻辑阵列块LAB&#xff08;每个LAB由16个逻辑器件LE组成&#xff0c;每个LE包括一个查找表LUT和一个RAM构成。Cyclone IV EP4CE6F17C8中包含…

【0227】smgr设计机制之新建一个磁盘表文件

相关阅读: 【0222】存储管理器smgr设计机制,及SMgrRelation、SMgrRelationData的作用(1) 【0223】源码剖析smgr底层设计机制(3) 【0224】smgr设计机制之通过RelFileNode访问磁盘表文件(2) 【0225】源码分析postgres磁盘块(disk block)定义 【0226】smgr设计机制中,s…

Qt编程,事件过滤器、绘图

3 事件过滤器 1) 安装事件过滤器 le->installEventFilter(this); //从QObject继承出来的对象&#xff0c;可以注册一个事件过滤器&#xff0c;所有的事件先交给事件过滤器处理 2) 重写事件过滤器 /* 参数&#xff1a; watched: 对象 event: 事件 返回值&#xff1a; true:…

UE4和C++ 开发-头文件(.h) 和实现文件(.cpp)区别

.h文件和.cpp文件是C程序中的两种不同类型的文件。 .h文件通常包含类、函数和变量的声明&#xff0c; 而.cpp文件包含这些声明的实现。 .h文件中的声明通常是公共的&#xff0c;可以被其他文件包含和使用。.cpp文件中的实现通常是私有的&#xff0c;只能在该文件中使用。 在…

ARM 10.12

设置按键中断&#xff0c;按键1按下&#xff0c;LED亮&#xff0c;再按一次&#xff0c;灭 按键2按下&#xff0c;蜂鸣器响。再按一次&#xff0c;不响 按键3按下&#xff0c;风扇转&#xff0c;再按一次&#xff0c;风扇停 src/key.c #include"key.h"//按键3的配…

Linux网络编程:UDP协议和TCP协议

目录 一. 对于端口号的理解 1.1 网络通信五元组 1.2 端口号的划分策略 二. 网络通信中常用的指令 2.1 netstat指令 2.2 pidof指令 三. udp协议 3.1 udp的概念及特点 3.2 udp协议端格式 3.3 对于面向数据报及应用层发送与读取数据的理解 四. tcp协议的概念及特点 五.…

C++课程总复习

一、c的第一条程序 1.cout cout >输出类对象&#xff0c;用来输出的&#xff0c;可以自动识别类型&#xff0c;所以不需要加格式符号 << 插入符&#xff08;输出符号&#xff09; endl 换行>\n #include <iostream> //#预处理 //include 包含 相应的头…