Android 消息机制Handler完全解析(一)

news2024/11/15 17:41:02

提到Handler相信即使你是刚入行的android开发也会用过,可能你会觉得很简单,但是Handler中包含的内容真的不是你理解的那么简单,可能有些工作3-5年的同学也没有对其有很深入的了解。但Handler在android中的地位非常重要,并且几乎是面试必问问题,鉴于此我决定写一个系列全面的讲解Handler的相关知识,相信通过本系列的学习足以应对日常的工作以及面试。

什么?你说我对Handler了解不深?可能有些同学表示不服,那么我们先来几个大厂真实的面试题,如果你都能很清晰的回答,那你可以跳过本系列,说明你对Handler的了解还是比较深入的,不多逼逼先看面试题

  • 1.一个线程中有几个Handler
  • 2.一个线程有几个Looper?如何保证
  • 3.Handler是怎么进行线程间通讯的,原理是什么?
  • 4.Handler的callback存在但返回true,handleMessage是否会执行?
  • 5.既然多个Handler往MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?
  • 6.Handler内存泄漏的原因?
  • 7.Looper死循环为什么不会导致应用卡顿?
  • 8.IdleHandler是什么?怎么使用?能解决什么问题?
  • 9.ThreadLocal的原理,以及在Looper中是如何应用的?
  • 10.请你谈谈消息屏障
  • 11.对epoll机制有了解吗?

。。。。。。。。

上面的问题你都能对答如流吗?相信通过本系列的学习你对Handler定会有更深的认识

1. Handler介绍

Handler是Android消息机制的上层接口,这使得在开发过程中只需要和Handler交互即可。Handler的使用过程很简单,通过它可以轻松地将一个任务切换到Handler所在的线程去执行。很多人认为Handler的作用是更新UI,这的确没错,但是更新UI仅仅是Handler的一个特殊使用场景。具体来说是这样的:有时候需要在子线程中进行耗时的I/O操作,可能是读取文件或者访问网络等,当耗时操作完成以后可能需要在UI上做一些改变,由于Android开发规范的限制,我们并不能在子线程中访问UI控件,否则就会触发异常,这个时候通过Handler就可以将更新UI的操作切换到主线程执行。因此,本质上来说,Handler并不是专门用于更新UI的,它只是常被开发者用来更新UI。

在Android源码中ViewRootImpl中对UI操作做了验证

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

Only the original thread that created a view hierarchy can touch its views.这个异常我相信很多初学者都遇到过,这个报错的意思就是:只有创建视图层次结构的原始线程才能接触其视图,创建视图层次的线程就是主线程,也就是说只有在主线程中才能修改UI。

大家有没有想过为什么不允许在子线程中访问UI呢?这是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期状态,那为什么系统不对UI控件的访问加上锁机制呢?缺点有两个

  • 加上锁机制会让UI访问的逻辑变得复杂
  • 锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。

所以最简单且高效的方法就是采用单线程模型来处理UI操作,对于开发者来说也不是很麻烦,只需要通过Handler切换一下UI访问的执行线程即可。

2. Android消息机制相关的几个对象

Message:消息体

Handler:消息处理器,发送、处理消息

MessageQueue:消息队列

Looper:循环器,整个机制的动力

3.Handler机制源码解析

在此之前先来看下整体的运行流程,以下图片来自享学课堂

在这里插入图片描述

我们一般用Handler的时候一般是使用handler.post或handler.send系列的方法发送一条消息,此时这条消息会被加入到MessageQueue,MessageQueue中的消息随着时间的流逝会被消费掉即调用handler.dispatchMessage方法进行分发,有点类似于生产者和消费者模式,handler.post和handler.send系列的方法发送消息相当于生产者,handler.dispatchMessage相当于消费者,那么接下来我们从handler.post和handler.send开始分别对Handler、MessageQueue、Looper的原理进行讨论之后再把这三者串联起来充分理解Handler消息的机制。

3.1 Handler相关源码解析

我们先从源头开起,即Handler对象的post和send系列方法,有了发送才会有接下来的一系列流程

Handler post系列相关源码

    public final boolean post(@NonNull Runnable r) {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }

可以看到调用post方法时会调用Handler的sendMessageDelayed方法

    public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

sendMessageAtTime方法的源码如下

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

可以看到调用了sendMessageAtTime方法,sendMessageAtTime方法里具体做了什么我们先不管。

send相关的方法我们就看一个sendMessage

    public final boolean sendMessage(@NonNull Message msg) {
        return sendMessageDelayed(msg, 0);
    }

可以看到它也是调用了sendMessageDelayed方法最终调用sendMessageAtTime方法。

总结:无论我们调用handler的send系列的相关方法(sendMessage、)还是调用post系列的相关方法,最终都会调用到sendMessageAtTime方法

我们来看下Handler中的sendMessageAtTime的源码

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

然后看下Handler中的enqueueMessage方法

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=this即把当前的handler对象赋值给msg的target,最终会调用MessageQueue的enqueueMessage方法。

在上述sendMessageAtTime这个方法里有个mQueue,这个mQueue是哪里来的呢?看Handler的构造函数可以看到,当我们new Handler的时候构造函数传递的参数可以分为两种(废弃的就不看了):一种带Looper,一种不带Looper

在这里插入图片描述

当参数中有Looper时,其实最终会调用到如下方法(注意这个方法是隐藏的)

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

也就是说这个mQueue其实是Looper中的MessageQueue对象,如果不传Looper对象最终会进入到如下方法(此方法也是隐藏的)

    @hide
    public Handler(@Nullable Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
    public static @Nullable Looper myLooper() {
            return sThreadLocal.get();
        }

从构造函数中可以看出,这里首先会调用Looper.myLooper()方法获取到Looper对象,然后再将mLooper的MessageQueue对象赋值给Handler的MessageQueue对象

总结:无论我们通过Handler发送的何种消息最终都会调用sendMessageAtTime方法,并最终调用MessageQueue中的enqueueMessage方法

3.2 MessageQueue源码

接下来我们看看MessageQueue的方法的源码,上述我们看到handler发送的消息会走到MessageQueue中的enqueueMessage方法,首先来看下这个方法的源码

 boolean enqueueMessage(Message msg, long when) {
            。。。。。。省略部分代码
            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.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

从这里我们看出MessageQueue其实不是一个队列,它是一个单链表,为什么用队列不行而用单链表呢?分析完这个方法你就会明白,我们拆开来看

(1) 先看if语句

if (p == null || when == 0 || when < p.when) {
    // New head, wake up the event queue if blocked.
    msg.next = p;
    mMessages = msg;
    needWake = mBlocked;
} 

这里其实就是把最新的消息插入到链表的表头,先看第一个条件因为

Message p = mMessages;

  • 如果p==null,说明当前单链表中没有元素,此时的结构如下

在这里插入图片描述

执行语句之后的格式如下

在这里插入图片描述

即新的消息的next指向null,并把此消息赋值给mMessages

也就是说单链表中插入了一条最新的数据此时最新的数据指向null

  • 如果when==0 说明要插入的消息delay的时间是0,此时肯定为第一个要执行的消息所以也要放到单链表的表头,它的数据结构的形式的变化可能如下

    在这里插入图片描述

    这也不难理解,因为我消息的延时为0所以肯定要排到最前面。

  • 如果when < p.when,说明要插入的消息的执行的时间点比较早,所以要插入到mMessages这个消息的前面,图跟上面这个差不多,我就不再画了

(2)接着看下else分支的代码

else分支代码如下,不要想着每一行代码都要搞懂,这一点很很很重要,我们看源码就是要了解核心流程,如果太钻牛角尖,看源码会很浪费时间,我们只看主流程的代码

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.next
        prev.next = msg;
    }

    // We can assume mPtr != 0 because mQuitting is false.
    if (needWake) {
        nativeWake(mPtr);
    }
}

这个是一个典型的单链表的插入操作,我们来看下它具体的执行流程

首选定义一个Message prev,代表链表的前驱节点

接着开启一个for循环,for循环里的各条件都是空的说明这个循环会一直执行,直到它被打断(比如break return等),

首先执行prev = p,这里的prev就代表要插入节点的前驱节点,p最开始的时候代表链表的第一个节点

然后执行p = p.next,让p指向它的下一个节点,接着重点来了,开启了一个if判断

if (p == null || when < p.when) {
    break;
}

这个语句的作用是什么呢?我们知道Handler发送消息时可以设置延时消息,正常情况下具体的消息的执行顺序是按照时间进行排序的(暂不考虑消息屏障)这个if语句的意思就是根据将要插入的消息A的执行时间跟链表中的数据挨个比较,直到找到晚于A执行的第一条消息B,说明消息A应该插入到消息B的前面,此时就找到了A要插入的位置,然后break退出for循环。接下来就是将消息A插入到消息B前面的过程,这也是典型的单链表的插入。如果有点看不明白,我画个图你就清楚了,假如目前的MessageQueue的情况如下
在这里插入图片描述

此时要插入一条如下的消息

在这里插入图片描述

首先第一步找到新消息要插入的位置,首先执行prev = p ,p = p.next执行之后如下

在这里插入图片描述

然后判断p== null || when < p.when

因为新消息的when为1500,而p.when为1008,这里的when可以理解为什么时候执行,可以这么理解新消息要在1500这个时刻执行,而p指向的这消息需要在1008这个时刻执行,即p指向的消息要先执行所以新插入消息的位置还要继续往后找,执行下一轮循环

在这里插入图片描述

if (p == null || when < p.when) {
    break;
}

此时在执行这个语句时会发现新消息的执行时刻(1500)小于P指向的消息的执行时刻(2000),满足if条件找到了新消息的插入位置,此时break退出for循环执行如下语句

msg.next = p; 
prev.next = msg;

即将新消息插入过程如下图所示

在这里插入图片描述

此时就按照执行的时间顺序将新的消息插入到了单链表中。
在开始讨论MessageQueue我问了一个问题为什么MessageQueue的数据结构不用队列而用单链表呢?
这里答案应该很明显:
(1)队列不能满足业务需要,因为涉及到数据的插入操作而队列只能先进先出,每次只能将新的数据放到队列末尾,它不支持将数据插入到某个位置这种操作

(2)单链表的插入操作效率很高时间复杂度为O(1),只要找到合适的插入位置就能迅速将心的消息插入到链表中。

有插入消息的方法就有与之对应的取消息方法,MessageQueue除了enqueueMessage方法之外存储方法,还有一个取消息的方法next(),我们来看下它的源码

为了看起来更加方便我删除了一些无用的代码

 @UnsupportedAppUsage
    Message next() {
        。。。。。。。
        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;
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
			。。。。。。。。。
        }
    }

首先可以看到调用next()方法会开启一个死循环for(;;)然后会去取一个消息,先看if

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语句

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

进入到这个if语句说明msg != null && msg.target != 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);
}

此时会把MessageQueue中最前面的那个Message.when与当前时刻做比较如果now<msg.when说明链表表头的那个消息还未到执行时间

在这里插入图片描述

也就是说这里msg1还未到执行的时间,此时需要设置一个阻塞的时间,msg.when - now得到的值就是单链表中表头的那个Message再过多久会执行。举个例子

当前时间是16:13:15,单链表表头的元素的when=16:13:30,循环到此之后发现此消息的执行时刻还没到,所以要等待,等多久呢?16:13:30 - 16:13:15 = 15s即等15s后处理此消息。

else {
    // Got a message.
    mBlocked = false;
    if (prevMsg != null) {
        prevMsg.next = msg.next;
    } else {
        mMessages = msg.next;
    }
    msg.next = null;
    return msg;
}

这里分两种情况一种是prevMsg != null 一种是prevMsg == null, prevMsg等于null很好理解因为mMessages是指向表头的,mMessages = msg.next这个就是单链表的删除操作把当前节点删除

在这里插入图片描述

这里其实是就是把当前链表中最先执行的消息也就是(单链表表头的那个消息)取出并返回,prevMsg!=null的情况跟消息屏障有关,后面再详细看

总结:MessageQueue的底层实现是一个单链表,主要包含两个操作
(1)插入 会把新消息按照时间顺序(p.when)插入到单链表中

(2)取消息 因为MessageQueue是按照时间顺序排序的,表头的消息是最先执行的,所以每次取MessageQueue的第一个消息进行执行(屏障消息除外)

总结一下:本篇主要讲解了Handler和Message的相关源码,Handler主要用来发送消息主要有post和send系列的相关方法,无论采用哪种形式最终都会调用sendMessageAtTime,在sendMessageAtTime中会调用MessageQueue的enqueueMessage方法此方法会按照执行的时间的顺序将Message进行排序,MessageQueue的next方法会根据执行的时间取消息,如果链表的第一个节点执行时间还未到则会进行阻塞等待,等到执行的时间点到达时取出链表的第一个消息返回。

篇幅原因,这篇就先写到这里吧,后面会详细讲解每一个知识点确保满足工作和面试的需要,文中有错误欢迎留言讨论我会在第一时间改正。锁定本台下节更精彩。

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

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

相关文章

图解Kafka | 5张图讲透Kafka 消费者交付语义

Kafka 消费者交付语义指的是 Kafka 消费者在处理消息时如何保证消息的可靠性和一致性。这涉及到消息是否被丢失、重复处理或者按顺序消费。 Kafka消费者交付语义有三种&#xff0c;即&#xff1a; 最多一次至少一次精确一次 当消费者组/消费者从 Kafka 消费数据时&#xff0…

昂科烧录器支持Airoha达发科技的蓝牙音频芯片AB1568

芯片烧录行业领导者-昂科技术近日发布最新的烧录软件更新及新增支持的芯片型号列表&#xff0c;其中Airoha达发科技的蓝牙音频芯片AB1568已经被昂科的通用烧录平台AP8000所支持。 AB1568是一款获得蓝牙5.3和LE音频认证的单芯片解决方案&#xff0c;包含一个ARM Cortex-M4F应用…

香蕉梨:自然的甜蜜宝藏

在水果的缤纷世界里&#xff0c;有一种独特的存在&#xff0c;它融合了香蕉的软糯与梨子的清甜&#xff0c;那便是令人惊艳的香蕉梨。 食家巷香蕉梨&#xff0c;外形圆润可爱&#xff0c;色泽金黄中带着一抹清新的嫩绿&#xff0c;宛如大自然精心雕琢的艺术品。当你拿起一个香蕉…

使用Java进行中小学违规教育培训数据采集实践-以某城市为例

目录 前言 一、违规教育信息 1、内容管理 2、转换后的内容 二、数据库设计 1、空间数据库 三、字符地址位置转换空间信息 1、实现时序图 2、后台实体类的设计与实现 3、数据持久化操作 四、总结 前言 时间来到2024年8月24日&#xff0c;时间过得很快&#xff0c;2024…

PowerShell | git log 中文乱码问题解决

总结一下: 乱码核心问题就是对不上编码.改成对应编码即可. 明白‌LESSCHARSET环境变量‌是用来设置less命令的字符集编码的。当在命令行中使用less命令查看文件时&#xff0c;如果文件包含非ASCII字符&#xff08;如中文&#xff09;&#xff0c;可能会出现乱码问题。通过设置…

SpringBoot+Vue实现大文件上传(断点续传-前端控制)

SpringBootVue实现大文件上传&#xff08;断点续传&#xff09; 1 环境 SpringBoot 3.2.1&#xff0c;Vue 2&#xff0c;ElementUI 2 问题 在前一篇文章&#xff0c;我们写了分片上传来实现大文件上传&#xff0c;存在一个问题就是&#xff0c;中间失败的话需要重新上传&#…

QT WIN11 FluentUI APP开发

代码 import QtQuick import QtQuick.Controls import FluentUIItem {property bool autoPlay: trueproperty int loopTime: 2000property var modelproperty Component delegateproperty bool showIndicator: trueproperty int indicatorGravity : Qt.AlignBottom | Qt.Align…

【MySQL】一文带你理清<行级锁>(行锁,间隙锁,临键锁)

前言 大家好吖&#xff0c;欢迎来到 YY 滴MySQL系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C Linux的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的…

STM32(六):定时器——输出比较实验

PWM驱动呼吸灯 源码&#xff1a; #include "stm32f10x.h" // Device headervoid PWM_Init(void) {RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//开启时钟TIM_InternalClockConfig(TIM2);//选择时基单元的时钟TIM_TimeBaseInitTypeDef TI…

怎么管控终端电脑上的移动端口

管控终端电脑上的移动端口&#xff0c;尤其是USB等移动端口&#xff0c;是确保企业数据安全和提升网络管理效率的重要手段。 一、使用注册表编辑器禁用USB端口&#xff08;适用于Windows系统&#xff09; 打开注册表编辑器&#xff1a; 同时按下“WinR”组合键&#xff0c;打…

【C++从小白到大牛】C++智能指针的使用、原理和分类

目录 1、我们为什么需要智能指针&#xff1f; 2、内存泄露 2.1 什么是内存泄漏&#xff0c;内存泄漏的危害 2.2如何避免内存泄漏 总结一下: 3.智能指针的使用及原理 3.1 RAII 3.2关于深拷贝和浅拷贝更深层次的理解&#xff1a; 3.3 std::auto_ptr 3.4 std::unique_pt…

《黑神话:悟空》登顶全球:游戏行业投资新风向与投资洞察

目录 引言 一、原创IP的崛起&#xff1a;文化共鸣与市场潜力 1《黑神话:悟空》的原创IP魅力 2 原创IP在游戏行业中的重要性 3 原创IP成为新的投资热点 4 文化共鸣的关键作用 二、高质量内容为王&#xff1a;技术与创新的双重驱动 1 《黑神话:悟空》的高质量内容展示 2…

Java接口interface(内含练习)

为什么有接口&#xff1f; 接口就是一种规则&#xff0c;更侧向是一种行为 接口的定义和使用 接口用关键字interface来定义 public interface 接口名{} 接口不能实例化 接口和接口之间是实现关系&#xff0c;通过implements关键字表示 public class 类名 implements 接口…

浅谈线性表——链表

文章目录 一、ArrayList的缺陷二、什么是链表&#xff1f;三、自我实现一个单向不带头非循环结构的链表3.1、实现代码3.2、代码解析 四、自我实现一个双向不带头非循环结构的链表4.1、实现代码 一、ArrayList的缺陷 前面学习了顺序表&#xff0c;顺序表在知道下标时可以快速的…

python应用之random模块(居然还有那么多的随机算法函数)

random 是 Python 的一个常用的内置模块&#xff0c;模块提供了生成随机数的功能&#xff0c;包含了多种生成随机数的函数&#xff0c;比如生成随机整数、随机浮点数、从序列中随机选择元素等。 使用 random模块 要使用 random模块&#xff0c;直接导入它即可。 import rand…

spring揭秘09-aop03-aop织入器织入横切逻辑与自动织入

文章目录 【README】【1】spring aop的织入【1.1】使用ProxyFactory 作为织入器【1.2】基于接口的代理&#xff08;JDK动态代理&#xff0c;目标类实现接口&#xff09;【补充】 【1.2】基于类的代理&#xff08;CGLIB动态代理&#xff0c;目标类没有实现接口&#xff09;【1.2…

Nginx: 配置项之autoIndex模块与Nginx变量

autoIndex模块 autoindex模块它所实现的一个基本功能&#xff0c;是当用户请求以 / 结尾式的URL&#xff0c;它会列出对应的目录结构比如说, 在实际的生态环境中&#xff0c;内部系统可能经常需要为用户提供一些下载功能。可能需要列出来某一个磁盘上的一个文件&#xff0c; 比…

【D-DCVRP】求解DCVRP改进贪婪算法(三)

一、Held-Harp模型 海尔德和卡尔普在1970年提出景点模型,用于求解TSP问题的最优解下界 该模型同样可以用于DCVRP问题,既有定理1成立。 定理1:根据Held-Karp模型使用向量 π = ( 0 , π 1 , π 2 , ⋯   , π n ) \pi=(0,\pi_1,\pi_2,\cdots,\pi_n) π=(0,π1​,π2​,⋯…

Datawhale第五期夏令营-CV竞赛

CV竞赛 0.赛事报名租用4090 1.开始运行下载文件提交结果 2.内容解释赛题背景赛题目标社会价值评分规则baseline精读代码什么是YOLO 主要代码内容精读使用Ultraalytics运行代码 0.赛事报名 赛事官网:https://www.marsbigdata.com/competition/details?id3839107548872 租用40…

【Redis】RDB和AOF持久化

RDB和AOF持久化 一、什么是持久化&#xff1f;二、RDB三、AOF 一、什么是持久化&#xff1f; 数据一般写在内存上&#xff0c;但是当重新启动计算机内存数据是会丢失的&#xff0c;而硬盘中的数据是不会丢失的&#xff0c;所以&#xff0c;当我们把数据从内存放到硬盘中的话就…