【Android Handler】从源码出发,一步步窥探Handler在Java层的数据结构关系和执行原理

文章目录

  • Example for use
    • Looper
    • Handler
  • sendMessage
    • msg插入链表头
      • Message p = mMessages;
      • msg.next = p;
      • mMessages = msg;
    • msg插入链表中间
      • Message p = mMessages;
      • prev = p;
      • p = p.next;
      • msg.next = p;
      • prev.next = msg;
  • Looper
    • prepare
    • loop
    • loopOnce
    • looper和message的关系
      • next
    • message和handler的关系
  • 总结

Handler作为消息机制在整个Android系统里面起到了无可替代的作用,我们今天来探讨下这个无比重要的机制的实现细节。

Example for use

首先需要了解的是它的总体设计和使用。

Looper

下面注释摘自源码:

Class used to run a message loop for a thread. Threads by default do not have a message loop associated with them; to create one, call prepare in the thread that is to run the loop, and then loop to have it process messages until the loop is stopped.
Most interaction with a message loop is through the Handler class.
This is a typical example of the implementation of a Looper thread, using the separation of prepare and loop to create an initial Handler to communicate with the Looper.

注释条理很清晰,意思是:
用于为线程运行消息循环的类。默认情况下,线程不具有与其关联的消息循环;要创建一个消息循环,需要在要运行该循环的线程中调用prepare,然后循环以使其处理消息,直到循环停止。
大多数与消息循环的交互都是通过Handler类进行的。
这是使用准备和循环分离创建与Looper通信的初始Handler的典型实现示例。

下面是代码:

class LooperThread extends Thread {public Handler mHandler;public void run() {Looper.prepare();mHandler = new Handler(Looper.myLooper()) {public void handleMessage(Message msg) {// process incoming messages here}};Looper.loop();}}

很简洁的代码,通过注释我们大概能知道的是,整体来说,它就是在一个线程里面跑消息循环,然后消息和handler进行通信。

Handler

A Handler allows you to send and process Message and Runnable objects associated with a thread’s MessageQueue. Each Handler instance is associated with a single thread and that thread’s message queue. When you create a new Handler it is bound to a Looper. It will deliver messages and runnables to that Looper’s message queue and execute them on that Looper’s thread.
There are two main uses for a Handler: (1) to schedule messages and runnables to be executed at some point in the future; and (2) to enqueue an action to be performed on a different thread than your own.
Scheduling messages is accomplished with the post, postAtTime(Runnable, long), postDelayed, sendEmptyMessage, sendMessage, sendMessageAtTime, and sendMessageDelayed methods. The post versions allow you to enqueue Runnable objects to be called by the message queue when they are received; the sendMessage versions allow you to enqueue a Message object containing a bundle of data that will be processed by the Handler’s handleMessage method (requiring that you implement a subclass of Handler).
When posting or sending to a Handler, you can either allow the item to be processed as soon as the message queue is ready to do so, or specify a delay before it gets processed or absolute time for it to be processed. The latter two allow you to implement timeouts, ticks, and other timing-based behavior.
When a process is created for your application, its main thread is dedicated to running a message queue that takes care of managing the top-level application objects (activities, broadcast receivers, etc) and any windows they create. You can create your own threads, and communicate back with the main application thread through a Handler. This is done by calling the same post or sendMessage methods as before, but from your new thread. The given Runnable or Message will then be scheduled in the Handler’s message queue and processed when appropriate.

介绍比较长,就直接用AI翻译了:

Handler允许您发送和处理与线程的MessageQueue相关联的消息和Runnable对象。每个Handler实例与单个线程及其消息队列相关联。当您创建一个新的Handler时,它会绑定到一个Looper。它将将消息和可运行项传递到该Looper的消息队列,并在该Looper的线程上执行它们。
Handler有两个主要用途:

(1)安排消息和可运行项在将来的某个时间点执行;

(2)将操作排队以在与您自己不同的线程上执行。

通过post、postAtTime(Runnable, long)、postDelayed、sendEmptyMessage、sendMessage、sendMessageAtTime和sendMessageDelayed方法来实现消息的调度。post版本允许您将Runnable对象排队,以在收到它们时由消息队列调用;sendMessage版本允许您将包含一组数据的Message对象排队,该数据将由Handler的handleMessage方法处理(这要求您实现Handler的子类)。
当向Handler发布或发送消息时,您可以选择允许该项在消息队列准备好时立即进行处理,或者指定在进行处理之前的延迟或绝对时间。后两种方法允许您实现超时、时钟间隔和其他基于时间的行为。
当为您的应用程序创建一个进程时,其主线程专用于运行一个消息队列,负责管理顶级应用程序对象(活动、广播接收器等)及其创建的任何窗口。您可以创建自己的线程,并通过Handler与主应用程序线程进行通信。这是通过从新线程调用与之前相同的post或sendMessage方法来完成的。然后,给定的Runnable或Message将被安排在Handler的消息队列中,并在适当时进行处理。

sendMessage

这里从handler.sendMessage()这一方法作为起点,一步步深入分析,去探索Android里的handler机制的来龙去脉。

android/os/Handler.java

public final boolean sendMessage(@NonNull Message msg) {return sendMessageDelayed(msg, 0);
}
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {MessageQueue queue = mQueue;if (queue == null) {RuntimeException e = new RuntimeException(this + " sendMessageAtTime() called with no mQueue");Log.w("Looper", e.getMessage(), e);return false;}return enqueueMessage(queue, msg, uptimeMillis);
}
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);
}

这里把msg的target设置为当前的handler对象。

需要强调的是,相当于当一个handler对象执行sendMessage的时候,被发送的msg对象的target就是当前handler对象本身。

android/os/MessageQueue.java

    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;}

这里首先判断msg的target是否为空,说明每个Message都有个“目标”,看下代码:

android/os/Message.java

@UnsupportedAppUsage
/*package*/ Handler target;

target是handler,但是到这里还还不足以知道他们的联系是怎么样的。

接着看代码,先判断msg是否正在使用中,再判断接收这个msg的handler的线程已经quit。

我们可以很明显看出来,所有的message是个链表的数据结构,整个方法做的事情是把msg插入到链表中。

但是他们怎么插入的呢?

我们下面可以通过可视化的方式展示这个过程,整体分为两种情况,
第一种是插入链表头,第二个是插入链表中间。

msg插入链表头

下面画图统一使用双横线指代引用。

代码中判断条件有三个,还有一种情况是p==null,这种情况比较好理解,就不画图了。

下面画图的是当前链表不为空的情况,也就是当前有后续的message需要处理。

Message p = mMessages;

在这里插入图片描述

msg.next = p;

在这里插入图片描述
msg就是作为入参数的message。

mMessages = msg;

在这里插入图片描述
数据结构操作到这里,相当于msg就是当前的链表头了。下一次执行的时候就执行它。

msg插入链表中间

Message p = mMessages;

在这里插入图片描述

prev = p;

在这里插入图片描述

p = p.next;

在这里插入图片描述

msg.next = p;

在这里插入图片描述

prev.next = msg;

在这里插入图片描述

到这里,可以很清晰看出来,msg此时此刻插入到了链表的中间。但是别忘了,prev = pp = p.next的动作是在一个循环里面的,也就是说,插入到在链表中合适的位置上,而影响的因素有

  • 时间: p.when
  • 是否需要唤醒:needWake
  • 是否为异步:p.isAsynchronous()

我们知道MessageQueue是进行了插入链表的操作,但是后续又做了什么呢?

这里不得不提的是消息处理机制中的Looper。

俗话说:“talk is cheap ,show me the code”。我们还是从代码出发,看它做了什么。

Looper

prepare

从 Looper.prepare();入手

    /** Initialize the current thread as a looper.* This gives you a chance to create handlers that then reference* this looper, before actually starting the loop. Be sure to call* {@link #loop()} after calling this method, and end it by calling* {@link #quit()}.*/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));}

注释提示,退出的时候记得调用quit().方法。

这里判断sThreadLocal.get()是否为空,假如不为空,就要抛出异常。如果为空,就new出一个Lopper对象并设置到sThreadLocal里面,我们知道ThreadLocal的作用是线程隔离,而prepare方法的执行是在线程上,这充分说明了每个线程只允许有一个Lopper对象,而且一旦设置了,就不能重新设置。

看下looper的构造函数:

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

new出一个MessageQueue对象,可以看出MessageQueue是伴随Looper的构造而构造的,也就是说MessageQueue在消息机制中,不需要我们操作。

loop

再看loop方法:

  /*** Run the message queue in this thread. Be sure to call* {@link #quit()} to end the loop.*/@SuppressWarnings("AndroidFrameworkBinderIdentity")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;// Make sure the identity of this thread is that of the local process,// and keep track of what that identity token actually is.Binder.clearCallingIdentity();final long ident = Binder.clearCallingIdentity();// Allow overriding a threshold with a system prop. e.g.// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'final int thresholdOverride =SystemProperties.getInt("log.looper."+ Process.myUid() + "."+ Thread.currentThread().getName()+ ".slow", 0);me.mSlowDeliveryDetected = false;for (;;) {if (!loopOnce(me, ident, thresholdOverride)) {return;}}}/*** Return the Looper object associated with the current thread.  Returns* null if the calling thread is not associated with a Looper.*/public static @Nullable Looper myLooper() {return sThreadLocal.get();}

通过sThreadLocal获取到当前线程的Looper对象,然后开始不断轮询。看下loopOnce方法:

loopOnce

 /*** Poll and deliver single message, return true if the outer loop should continue.*/@SuppressWarnings("AndroidFrameworkBinderIdentity")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);}// Make sure the observer won't change while processing a transaction.final Observer observer = sObserver;final long traceTag = me.mTraceTag;long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;if (thresholdOverride > 0) {slowDispatchThresholdMs = thresholdOverride;slowDeliveryThresholdMs = thresholdOverride;}final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);final boolean needStartTime = logSlowDelivery || logSlowDispatch;final boolean needEndTime = logSlowDispatch;if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {Trace.traceBegin(traceTag, msg.target.getTraceName(msg));}final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;final long dispatchEnd;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 (logSlowDelivery) {if (me.mSlowDeliveryDetected) {if ((dispatchStart - msg.when) <= 10) {Slog.w(TAG, "Drained");me.mSlowDeliveryDetected = false;}} else {if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",msg)) {// Once we write a slow delivery log, suppress until the queue drains.me.mSlowDeliveryDetected = true;}}}if (logSlowDispatch) {showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);}if (logging != null) {logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);}// Make sure that during the course of dispatching the// identity of the thread wasn't corrupted.final long newIdent = Binder.clearCallingIdentity();if (ident != newIdent) {Log.wtf(TAG, "Thread identity changed from 0x"+ Long.toHexString(ident) + " to 0x"+ Long.toHexString(newIdent) + " while dispatching to "+ msg.target.getClass().getName() + " "+ msg.callback + " what=" + msg.what);}msg.recycleUnchecked();return true;}

执行了一些日志和其它的操作,这里先不深入这部分。来看下我们关注的:

Message msg = me.mQueue.next(); // might block

这里可以推测出来消息队列在获取下一个消息。

msg.target.dispatchMessage(msg);

把消息进行分发,target其实是handler。

到这里也许我们从这两个点出发,找到message、looper和handler的关系。

looper和message的关系

先看下looper和message的关系:

android/os/Looper.java

  final MessageQueue mQueue;

mQueue是Looper的一个成员变量。

android/os/MessageQueue.java

    @UnsupportedAppUsageMessage mMessages;

而Message是MessageQueue的成员变量。
那么来看看MessageQueue的next方法:

next

   @UnsupportedAppUsageMessage 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();}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;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.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;}// 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.if (pendingIdleHandlerCount < 0&& (mMessages == null || now < mMessages.when)) {pendingIdleHandlerCount = mIdleHandlers.size();}if (pendingIdleHandlerCount <= 0) {// 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 {keep = 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.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;}}

这个方法很长,我们要这里关注的是循环体里面的代码。而循环里面有个同步块,第一个判断,假如msg不是空,而msg的target是空,就去线性查找一个异步消息。
篇幅原因,异步消息在这里就不继续展开。

下面接着判断,假如msg不为空,但是有target的话,就进入判断当前msg的时间属性是否满足条件,满足的话拿出这个msg并返回。mMessages也是链表的数据结构。

后面就是关于空闲handler的处理了。

到这里我们可以很清楚了,looper从MessageQueue里面不断取出消息,取消息的时候,根据当前msg的when属性进行先后排序,另外还有一个特殊处理,就是判断当前是否有异步消息,异步消息可以得到优待,提前被looper拿出来处理。

message和handler的关系

接下来继续通过msg.target.dispatchMessage(msg);来探究下message和handler的关系

android/os/Handler.java

    /*** Handle system messages here.*/public void dispatchMessage(@NonNull Message msg) {if (msg.callback != null) {handleCallback(msg);} else {if (mCallback != null) {if (mCallback.handleMessage(msg)) {return;}}handleMessage(msg);}}private static void handleCallback(Message message) {message.callback.run();}

这里处理了msg的callback,msg的callback其实是个runnable。

android/os/Message.java

    @UnsupportedAppUsage/*package*/ Runnable callback;

也就是说假如msg有任务不为空,则执行任务。

而mCallback是handler里面的一个成员变量,

   final Callback mCallback;public Handler(@Nullable Callback callback, boolean async) {...}

就是我们使用Handler的时候通过构造函数传进去的那个回调函数。

到这里,我们也可以知道dispatchMessage执行的时候假如msg里面有runnable,则执行runnable,没有runnable的话则执行handleMessage(msg)

总结来说,loop执行的时候,msg会循环分发到对应的handler对象中被处理。

但是到这里我们可能觉得还没有弄清楚handler和msg的联系,那是因为我们忘记了前面的浏览过的代码,还记得前面sendMessage 那个段落吗?

handler调用sendMessage的时候,就把msg的target设置为handler对象自身了。

下面我们可以通过绘图的方式,把前面所有的源码分析进行可视化表达。

总结

综述上文所有的分析, 可以绘制出这样的关系图:

在这里插入图片描述
到这里,我想我想我们对于Android的handler消息机制有了一个更高维度的理解了,这无论对于正确使用handler来开发高质量的APP,抑或是对于Android系统内部的运作原理的理解都会有巨大的帮助。

注:创作本文的时候完全没有参考其他资料,仅仅靠源码的阅读和理解,限制于本人水平有限,如有错误或者不严谨的地方,还请指出修正。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

修改 RabbitMQ 默认超时时间

MQ客户端正常运行&#xff0c;突然就报连接错误&#xff0c; 错误信息写的很明确&#xff0c;是客户端连接超时。 不过很疑虑&#xff0c;为什么会出现连接超时呢&#xff1f;代码没动过&#xff0c;网络也ok&#xff0c;也设置了心跳和重连机制。 最终在官网中找到了答案&am…

AIGC重塑金融 | 大模型在金融行业的应用场景和落地路径

作者&#xff1a;林建明 来源&#xff1a;IT阅读排行榜 本文摘编自《AIGC重塑金融&#xff1a;AI大模型驱动的金融变革与实践》&#xff0c;机械工业出版社出版 目录 01 大模型在金融领域的 5 个典型应用场景 02 大模型在金融领域应用所面临的风险及其防范 03 AIGC 技术的科…

SAP gui 组服务器 提示 Error service sapmsPRD unknown

/etc/hosts 追加IP地址和域名的配对关系 /etc/services 追加 sapms[sid] 3601/tcp

ctf-36C3解析

一、环境 unbentu 这道题给了docker环境&#xff0c;gethub上面自己找 一个好用的linux全局切换梯子proxychains 二、开始解析 2.1初始 2.2编译 docker build . 2.3代理设置完毕 我试了一下代理还是不行&#xff0c;我们换源尝试一下 RUN sed -i s/deb.debian.org/mirro…

7 AOP事务管理

文章目录 6.1 Spring事务简介6.1.1 相关概念介绍6.1.2 转账案例-需求分析6.1.3 转账案例-环境搭建步骤1:准备数据库表步骤2:创建项目导入jar包步骤3:根据表创建模型类步骤4:创建Dao接口步骤5:创建Service接口和实现类步骤6:添加jdbc.properties文件步骤7:创建JdbcConfig配置类步…

element plus的el-image图片发布到nginx不显示

问题&#xff1a; <el-image alt""src"/img/month-b.png" class"card-icon"style"width: 89px;height: 89px;right: -7px;top: -5px;"/> 部署到nginx二级路由访问地址是&#xff1a; http://192.168.1.207/divided/# 这时候使用…

R语言赋值符号<-、=、->、<<-、->>的使用与区别

R语言的赋值符号有&#xff1c;-、、-&#xff1e;、&#xff1c;&#xff1c;-、-&#xff1e;&#xff1e;六种&#xff0c;它们的使用与区别如下: <-’&#xff1a;最常用的赋值符号。它将右侧表达式的值赋给左侧的变量&#xff0c;像一个向左的箭头。例如&#xff0c;x …

EasyExcel 复杂表头的导出(动态表头和静态表头)

问题&#xff1a;如图&#xff0c;1部分的表头是动态的根据日期变化&#xff0c;2部分是数据库对应的字段&#xff0c;静态不变的&#xff1b; 解决方案&#xff1a;如果不看1的部分&#xff0c;2部分内容可以根据实体类注解的方式导出&#xff0c;那么我们是不是可以先将动态表…

UKP3d,AutoPDMS出轴测图时的焊点设置

焊点的设置是关联元件库里的连接方式&#xff08;焊点设置不成功&#xff0c;请查看元件的连接方式&#xff09;&#xff0c;看元件的连接方式如下&#xff1a; 转到两次查看元件连接类型

蓝桥杯23年第十四届省赛真题-三国游戏|贪心,sort函数排序

题目链接&#xff1a; 1.三国游戏 - 蓝桥云课 (lanqiao.cn) 蓝桥杯2023年第十四届省赛真题-三国游戏 - C语言网 (dotcpp.com) 虽然这道题不难&#xff0c;很容易想到&#xff0c;但是这个视频的思路理得很清楚&#xff1a; [蓝桥杯]真题讲解&#xff1a;三国游戏&#xff0…

计算机网络——数据链路层(差错控制)

计算机网络——数据链路层&#xff08;差错控制&#xff09; 差错从何而来数据链路层的差错控制检错编码奇偶校验码循环冗余校验&#xff08;CRC&#xff09;FCS 纠错编码海明码海明距离纠错流程确定校验码的位数r确定校验码和数据位置 求出校验码的值检错并纠错 我们今年天来继…

Spring用到了哪些设计模式?

目录 Spring 框架中⽤到了哪些设计模式&#xff1f;工厂模式单例模式1.饿汉式&#xff0c;线程安全2.懒汉式&#xff0c;线程不安全3.懒汉式&#xff0c;线程安全4.双重检查锁&#xff08;DCL&#xff0c; 即 double-checked locking&#xff09;5.静态内部类6.枚举单例 代理模…